Python Plug-Ins
[Theory] Introduction
Both our libgimp
and libgimpui
libraries are introspected thanks to
the GObject-Introspection
project. It means that the Python API is actually nearly exactly the
same as the C library, except it follows Python language idiosyncrasies.
For instance, the signature of
gimp_layer_get_blend_space()
in C is:
GimpLayerColorSpace gimp_layer_get_blend_space (GimpLayer* layer);
Here it is in Python:
In [2]: Gimp.Layer.get_blend_space.__doc__
Out[2]: 'get_blend_space(self) -> Gimp.LayerColorSpace'
Now where it gets interesting is when you have several return values. In
C, this would be implemented as pointers to values. In Python, we can
actually have several return values. Therefore while
gimp_drawable_get_offsets()
’s
signature looks like this in C (with 3 return values: a boolean
success
and 2 integer offsets):
gboolean gimp_drawable_get_offsets (GimpDrawable *drawable,
gint *offset_x,
gint *offset_y);
This is in Python:
In [3]: Gimp.Drawable.get_offsets.__doc__
Out[3]: 'get_offsets(self) -> bool, offset_x:int, offset_y:int'
It typically means that if you had a GimpDrawable
variable named
drawable
, you’d call:
success, x_offset, y_offset = drawable.get_offsets()
Notice also how we don’t call Gimp.Drawable.get_offsets(drawable)
but
drawable.get_offsets()
, i.e. that get_offsets()
is really a method
to the drawable
object (of type Gimp.Drawable
).
This makes for a very Python-style interface!
Not only this, it is also full-featured. Only very few libgimp
or
libgimpui
functions are not available in bindings, and only because
this is not supported. For instance the varargs
functions
(variable-length arguments à-la printf
) don’t have a Python version.
Instead though, non-varargs versions always exist so that bindings can
still do absolutely everything which the C API can do.
This all makes the C API reference very usable even to develop Python plug-ins. Nevertheless if you would prefer a reference specifically dedicated to the Python binding, there exists some third-party documentation:
[Theory] What about Other Bindings?
This tutorial will only propose a Python 3 version, but we theoretically
support all languages bindable with GObject-Introspection
.
Additionally to Python 3, we also have demo code for:
They all implement the same as this C demo plug-in.
Note however that we have memory issues with the Lua binding (which is why we disable it by default) and that the Javascript binding is not currently enabled on Windows because packaging an interpreter turned out to be quite a challenge. As for the Vala binding, as far as we know, it works well, except that the generated code outputs many annoying warnings. Moreover it is a compiled language and we believe that most people doing plug-ins are more interested into interpreted script languages for quicker development.
This is why we are mostly focusing on the Python 3 API only for now.
[Code] Reimplementing Hello World in Python 3
I am going to assume you read at least the [Theory]
sections of the
previous tutorials and will reimplement the whole demo plug-in we made
in C at once in Python 3. Here is what it would look like:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('GimpUi', '3.0')
from gi.repository import GimpUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gtk
import sys
plug_in_proc = "plug-in-zemarmot-py3-demo-hello-world"
plug_in_binary = "py3-hello-world"
def hello_world_run(procedure, run_mode, image, drawables, config, data):
if len(drawables) > 1:
return procedure.new_return_values (Gimp.PDBStatusType.CALLING_ERROR,
GLib.Error(f"Procedure '{plug_in_proc}' works with zero or one layer."))
elif len(drawables) == 1:
if not isinstance(drawables[0], Gimp.Layer):
return procedure.new_return_values (Gimp.PDBStatusType.CALLING_ERROR,
GLib.Error(f"Procedure '{plug_in_proc}' works with layers only."))
parent = drawables[0].get_parent ()
position = image.get_item_position (drawables[0])
if run_mode == Gimp.RunMode.INTERACTIVE:
GimpUi.init(plug_in_binary)
dialog = GimpUi.ProcedureDialog.new(procedure, config, "Hello World")
box = dialog.fill_box("size-box", ["font-size", "font-unit"]);
box.set_orientation (Gtk.Orientation.HORIZONTAL);
dialog.fill_frame("size-frame", "compute-size", False, "size-box");
dialog.fill(["text", "font", "size-frame"]);
if not dialog.run():
dialog.destroy()
return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, None)
else:
dialog.destroy()
text = config.get_property('text')
font = config.get_property('font')
compute_size = config.get_property('compute-size')
size = config.get_property('font-size')
unit = config.get_property('font-unit')
image.undo_group_start()
text_layer = Gimp.TextLayer.new (image, text, font, size, unit);
image.insert_layer (text_layer, parent, position);
if compute_size:
image_width = image.get_width()
layer_width = text_layer.get_width()
size = size * (image_width - 1) / layer_width
text_layer.set_font_size(size, Gimp.Unit.pixel());
while size > 1:
layer_width = text_layer.get_width()
if layer_width < image_width:
break
size -= 1
text_layer.set_font_size(size, Gimp.Unit.pixel())
image.undo_group_end()
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
class HelloWorld (Gimp.PlugIn):
def do_query_procedures(self):
return [ plug_in_proc ]
def do_create_procedure(self, name):
procedure = None
if name == plug_in_proc:
procedure = Gimp.ImageProcedure.new(self, name,
Gimp.PDBProcType.PLUGIN,
hello_world_run, None)
procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.DRAWABLE |
Gimp.ProcedureSensitivityMask.NO_DRAWABLES)
procedure.set_menu_label("_Python 3 Hello World")
procedure.set_attribution("Jehan", "Jehan, ZeMarmot project", "2025")
procedure.add_menu_path ("<Image>/Hell_o Worlds")
procedure.set_documentation ("Official Hello World Tutorial in Python 3",
"Some longer text to explain about this procedure. " + \
"This is mostly for other developers calling this procedure.",
None)
procedure.add_string_argument ("text", "Text", None, "Hello World!",
GObject.ParamFlags.READWRITE)
procedure.add_font_argument ("font", "Font", None, False, None, True,
GObject.ParamFlags.READWRITE)
procedure.add_boolean_argument ("compute-size", "Compute Ideal Size",
"This option will compute a font size " + \
"so that the text optimally fills the whole canvas",
False, GObject.ParamFlags.READWRITE)
procedure.add_int_argument ("font-size", "Font Size", None,
1, 1000, 20, GObject.ParamFlags.READWRITE)
procedure.add_unit_argument ("font-unit", "Font Unit", None,
True, False, Gimp.Unit.pixel(),
GObject.ParamFlags.READWRITE)
return procedure
Gimp.main(HelloWorld.__gtype__, sys.argv)
[Theory] Studying the Python Hello World
Interpreter and Encoding
The first lines are very standard:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
We start our script with a shebang
so that your OS kernel (or through
cross-platform
rules)
finds the interpreter. In fact GIMP has its own infrastructure and may
also override the interpreter in some cases. As a general rule, just
always set your standard shebang line.
The second line is quite a standard encoding
declaration.
Though it is not mandatory, it is quite a good practice, especially as
various GLib
or libgimp
functions expects input to be proper UTF-8.
Modules
In place of including libgimp
and libgimpui
, you import
the 2
modules, respectively named Gimp
and GimpUi
from the gi
module.
Even though we only have a ‘3.0’ version right now, you should always
set the version.
A few more modules are needed because we will use explicit API from
these: GObject
, GLib
, Gtk
also from gi
and the sys
module from
standard library.
import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('GimpUi', '3.0')
from gi.repository import GimpUi
gi.require_version('Gegl', '0.4')
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gtk
import sys
Subclassing in Python
Now same as in C, we
create the HelloWorld
class as a subclass of Gimp.PlugIn
, except it
uses Python subclassing. You will note also that all the abstract
methods which you are expected to implement are prefixed by do_
. Apart
from this, it works pretty much the same:
class HelloWorld (Gimp.PlugIn):
def do_query_procedures(self):
return [ plug_in_proc ]
def do_create_procedure(self, name):
procedure = None
if name == plug_in_proc:
procedure = Gimp.ImageProcedure.new(self, name,
Gimp.PDBProcType.PLUGIN,
hello_world_run, None)
procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.DRAWABLE |
Gimp.ProcedureSensitivityMask.NO_DRAWABLES)
procedure.set_menu_label("_Python 3 Hello World")
procedure.set_attribution("Jehan", "Jehan, ZeMarmot project", "2025")
procedure.add_menu_path ("<Image>/Hell_o Worlds")
procedure.set_documentation ("Official Hello World Tutorial in Python 3",
"Some longer text to explain about this procedure. " + \
"This is mostly for other developers calling this procedure.",
None)
procedure.add_string_argument ("text", "Text", None, "Hello World!",
GObject.ParamFlags.READWRITE)
procedure.add_font_argument ("font", "Font", None, False, None, True,
GObject.ParamFlags.READWRITE)
procedure.add_boolean_argument ("compute-size", "Compute Ideal Size",
"This option will compute a font size " + \
"so that the text optimally fills the whole canvas",
False, GObject.ParamFlags.READWRITE)
procedure.add_int_argument ("font-size", "Font Size", None,
1, 1000, 20, GObject.ParamFlags.READWRITE)
procedure.add_unit_argument ("font-unit", "Font Unit", None,
True, False, Gimp.Unit.pixel(),
GObject.ParamFlags.READWRITE)
return procedure
Reimplementing the Core Processing
The hello_world_run()
function is also quite similar, except that it
uses Python idiosyncrasies:
drawables
is a standard Python list ofGimp.Drawable
objects.- Its length can therefore be verified with the generic
len()
function. - C
NULL
is replaced by PythonNone
. - Checking for real type of an object in python works well with
isinstance()
(all theGIMP_IS_
macros from C don’t exist in Python).
You will also notice that the various config
properties must be
requested one by one with config.get_property()
. This is because of
what I was saying in the introduction about
variable-length arguments functions which are among the few cases of
non-bindable API.
The Gimp.main()
function
Finally the Python plug-in ends with a call to
Gimp.main()
.
As explained in the C basic tutorial,
the GIMP_MAIN
macro is C-only. You must pass the GType
of your
custom Gimp.PlugIn
class as first parameter, which in Python is the
__gtype__
argument of the class name. The second argument is the list
of arguments passed to this executable, which is why we also imported
sys
:
Gimp.main(HelloWorld.__gtype__, sys.argv)
Apart from this, the whole code is pretty similar in C and Python. It is also a lot shorter because most of the C boilerplate code doesn’t exist in the Python 3 version.
[Theory] File Architecture
First of all, installing any plug-in is identical in GIMP, which means
you must create a folder in your plug-ins/
directory
and put your Python file in this folder with the same name (only adding
the .py
extension).
For instance, if you write your code in a file named py3-hello-world.py
,
install it in a directory named py3-hello-world/
.
Then make sure your script file is executable, in the case where you are on a platform where this matters (which is probably any OS but Windows):
chmod u+x py3-hello-world.py
Now if you restart GIMP, it should pick up your plug-in.
Calling a PDB Procedure in Python
Calling a PDB procedure from a Python (or other bindings) plug-ins is slightly longer than
in C, again for
the same reason of non-bindable functions with variable-length
arguments. This makes
gimp_procedure_run()
not bindable, and instead replaced by
gimp_procedure_run_config()
which is renamed to
Gimp.Procedure.run()
in Python.
Furthermore, since we cannot use
g_object_set()
in Python (still for the same reason), we must set properties with
multiple commands. Nevertheless it stays quite simple to call a PDB
procedure. And the equivalent to the C code is:
procedure = Gimp.get_pdb().lookup_procedure('plug-in-zemarmot-c-demo-hello-world')
config = procedure.create_config()
config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE)
config.set_property('image', image)
config.set_property('text', 'Hello Universe!')
config.set_property('compute-size', True)
result = procedure.run(config)
if result.index(0) == Gimp.PDBStatusType.SUCCESS:
# Do something in case of success!
If anyone has a very keen eye for details, you may have noted that I called “plug-in-zemarmot-c-demo-hello-world” which is the procedure name for the C Hello World. I did this on purpose to really make clear that PDB procedures are absolutely language-agnostic.
You may call a C plug-in procedure from a Python plug-in, a Python plug-in procedure from a C plug-in, and obviously a Python procedure from a Python plug-in or a C procedure from a C plug-in. You can further mix by calling Javascript plug-in procedures, Lua, Script-Fu… anything! It simply doesn’t matter. Once a procedure has been registered in the Procedural DataBase, it is a neutral interface with a name and various arguments. That’s all!
Conclusion
And that’s about it. If you followed the C tutorial first, the Python
tutorial should be pretty straightforward as all the concepts are the
same. The Gimp
and GimpUi
modules are nearly a perfect mapping of
the C libraries.
You will also notice how there is absolutely no styling difference with Python plug-ins in the graphical interface and how no features are missing. Basically from the point of view of people using your plug-ins, it makes not a single difference in which language the plug-in is made. They can’t even know (apart by checking the code, of course).
- Previous tutorial: C plug-ins - Procedure DataBase
- Next tutorial: Script-Fu plug-ins