Skip to content

Instantly share code, notes, and snippets.

@hnbdr
Last active August 9, 2025 06:00
Show Gist options
  • Select an option

  • Save hnbdr/d4aa13f830b104b23694a5ac275958f8 to your computer and use it in GitHub Desktop.

Select an option

Save hnbdr/d4aa13f830b104b23694a5ac275958f8 to your computer and use it in GitHub Desktop.
Gimp 3 python migration guide

Introduction

Gimp 3 was recently released, with a huge number of breaking changes, so almost all plugins for gimp written in python stopped working. My plugin is no exception. Due to the lack of any sensible instructions on migration on the Internet, I had to spend half a day to figure it out. I hope this article will save you some time.

Attention

The entire guide is written for windows, but I am sure that users of other systems are quite capable of adapting it to their needs.

Resources

Here you can find api for libgimp and python, in version 3.0 they are structurally very similar, python bindings almost mirror their implementation in libgimp.
https://developer.gimp.org/api/3.0/

Official plugin tutorial.
https://testing.docs.gimp.org/3.0/en/gimp-using-python-plug-in-tutorial.html

Another official tutorial. Lots of useful information on the new implementation of Python plugins.
https://developer.gimp.org/resource/writing-a-plug-in/tutorial-python/

It's already out of date, but you can still learn something interesting.
https://z-uo.medium.com/create-python3-plugin-for-gimp-the-basics-94ede94e9d1f

QOL

I decided that this part should be first because writing scripts for embed interpreters is always a pain in the ass and I would kill for such recommendations in every single case.

Gimp

Start gimp with the --verbose flag, this will show additional information about errors and other things.

Debugger

I don't think life without a debugger is complete, but in many embedded environments it is impossible to connect it, fortunately this does not apply to Gimp.

Debugger configuration for vscode

localRoot and remoteRoot should point to the directory with plugins.

{
    "name": "Attach to GIMP Python",
    "type": "python",
    "request": "attach",
    "port": 5678,  // The port for the debug server
    "host": "localhost",
    "pathMappings": [
        {
        "localRoot": "d:\\my-plugins-folder",
        "remoteRoot": "d:\\my-plugins-folder"
        }
    ]
},

Installing debugpy

By default, gimp does not have debugpy, so you need to add it manually. In my case, the path with the libraries was c:\Users\User\AppData\Local\Programs\GIMP 3\lib\python3.12\ To avoid the hassle, I simply downloaded the whl file and unpacked it into this directory (it's a simple zip archive)

Connecting debugpy

import debugpy
debugpy.listen(("localhost", 5678))
debugpy.wait_for_client()

How it works

wait_for_client implies that the script execution is completely frozen until the debugger is connected. There are some interesting issues that require further study. Gimp remembers its state after the initial launch of a new version of the plugin and wait_for_client in the root of the file no longer works, so make changes to the plugin file to start a new debugging session. After performing the python registration, the interpreter also often dies from inactivity, so there is nothing to connect to, so use debugpy.wait_for_client() in all places when the interpreter is restarted, for example in the run method. In a simple case, after changes in the code, you need to restart the entire gimp, how to improve this is written below.

Modules and quick restart

At least since version 3.0.4 it seems the main plugin module is re-read when the procedure is called, but I'll leave this chapter just in case.

You can split your project into modules, useful for quick reloading when you can change the plugin code without restarting gimp entirely.

Directory structure

plugin_folder
    modules
        __init__.py - empty file
        module.py - file from which we can import
    main.py - main plugin file

main.py

import inspect
import os
import sys
import importlib
currentframe = os.path.dirname(inspect.getfile(inspect.currentframe()))
sys.path.append(currentframe)

import modules.module

class MyFirstPlugin (Gimp.PlugIn):
    # ...
    def run(self, procedure, run_mode, image, drawables, config, run_data):
        debugpy.wait_for_client()
        # Now after each run of this procedure the entire module modules/module.py will be reloaded
        importlib.reload(modules.module)
        modules.module.my_function(image)

        return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())

Logging

import inspect
import os
import sys
currentframe = os.path.dirname(inspect.getfile(inspect.currentframe()))
sys.path.append(currentframe)

sys.stderr = open(os.path.join(currentframe, "errors.txt"), 'w', buffering=1)
sys.stdout = open(os.path.join(currentframe, "log.txt"), 'w', buffering=1)

Now errors.txt and log.txt will be located in the same folder as the plugin file. print() also outputs to log.txt

Standard logging in Gimp is performed using the Gimp.message and Gimp.message_set_handler methods.

# Send message to status bar.
Gimp.message_set_handler(Gimp.MessageHandlerType.MESSAGE_BOX)
# Send a message to the terminal (in Windows, this is the window that appears when you run it with the --verbose flag)
Gimp.message_set_handler(Gimp.MessageHandlerType.CONSOLE)
# Send a message to the "Error Console" dialog.
Gimp.message_set_handler(Gimp.MessageHandlerType.ERROR_CONSOLE)

Gimp.message('My message!')

Migration

General tips

In any unclear situation, use inspection in the debugger to understand what properties of objects are available. I also recommend reading the tutorials from the Resources section, there is a lot of useful information there that is not reflected in this article.

Paths

Previously, you could put script files directly in the root of the extensions directory. But in Gimp 3, each individual plugin's code must be in a subdirectory. The plugin directory must have the same name as the main file (myplugin/myplugin.py). For Linux and MacOS the file must be executable.

Structure

The main structure of the plugin is described here, don't worry, it's practically gimpfu.register, only in a new wrapper. https://testing.docs.gimp.org/3.0/en/gimp-using-python-plug-in-tutorial.html

Modules

import gimp
import gobject

...etc

Should be translated to

from gi.repository import Gimp
from gi.repository import GObject

The full list is from gi.repository, but it will obviously change.

Atk
Babl
GLib
GModule
GObject
Gdk
GdkPixbuf
Gegl
Gimp
Gio
Gtk
HarfBuzz
Pango
cairo
freetype2

To select a specific version of a module, use the require_version function, for example gi.require_version('Gimp', '3.0'). Most likely, it will be useful in the future, when module versions start changing, but your plugin will still work, and you can migrate it to a new version of the module at your own pace. This means you should always request a specific version before importing.

The gimpfu, pdb module is no longer used. pdb still exists (Gimp.get_pdb()), but no longer contains the procedure-reflecting methods, details below.

Procedures

pdb.<procedure_name> commands are no longer present. A common call is used for this:

# search for a procedure in the database
procedure = Gimp.get_pdb().lookup_procedure('gimp-image-get-channels');

# Create arguments
config = procedure.create_config();
config.set_property('image', image);

# Result as a ValueArray
result = procedure.run(config);
# First element - success indication
success = result.index(0);
# Subsequent elements - returned values
channels = result.index(1)

You can also get examples of calling a procedure by selecting Browse in the Python Fu console. Look carefully at what methods for adding parameters to the config are offered in the procedure call examples, sometimes you may need not just set_property, but set_core_object_array or set_color_array. I also wrote a small helper function, maybe it will be useful to you.

Calling procedures is often not required. Since they are almost always implemented as object methods. For example, as you can see in the libgimp documentation, the "gimp-item-is-group" operation reflects the declaration of the function gimp_item_is_group which is respectively expressed in the python api as Gimp.Item.is_group.

Properties

All properties of objects are now replaced with getter methods, I think all of them, so if you see that an object's property is read, rewrite it to a method and check with the api. For example, image.width has turned into image.get_width()

print

Of course the most famous difference between python 2 and python 3 is print, don't forget to replace print "something" with print("something")

Files

Some commands require a File instance to read or write, it can be created by the Gio.File.new_for_path factory

file = Gio.File.new_for_path(image_path)
img = Gimp.file_load(Gimp.RunMode.NONINTERACTIVE, file)

Also the information about the path to the image file has moved to the File instance, you will most likely encounter this in other places as well.

img.get_file().get_path()

Other

layer.get_offsets() now returns a boolean as the first element, be careful. Other methods may also start returning unusual results, check in the api documentation.

Conclusion

Well, that's all the problems I encountered while converting my not-so-small plugin to Gimp 3. But you will definitely have your own problems. I hope this information will help you not to get lost. The article will remain here, any comments and additions are welcome.

@opist-vein
Copy link

cool , I'll definitely let you know my own problems too in case it's helpful to you
and thanks again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment