GIMP Plugin Development
Complete guide for developing GIMP 3.0+ plugins with Python 3.
Overview
GIMP (GNU Image Manipulation Program) supports plugins written in Python 3. GIMP 3.0 introduced significant API changes, GEGL-based image processing, and modern Python integration.
Key Features:
- Python 3 scripting (no longer Python 2)
- GEGL (Generic Graphics Library) for image operations
- New procedure registration system
- GTK 3 dialog support
- Access to all GIMP internal procedures
GIMP 3.0+ Requirements
# Check GIMP version
gimp --version # GIMP 3.0.0 or higher
# Python 3 is bundled with GIMP
# Plugins use the Python interpreter included with GIMP
Plugin Locations
# User plugins (preferred)
~/.config/GIMP/3.0/plug-ins/
# System plugins
/usr/lib/gimp/3.0/plug-ins/
# Windows
%APPDATA%\GIMP\3.0\plug-ins\
# macOS
~/Library/Application Support/GIMP/3.0/plug-ins/
Plugin File Structure
my_plugin/
├── my_plugin.py # Main plugin file (must be executable on Linux)
└── __pycache__/ # Python cache (auto-generated)
# Make plugin executable (Linux/macOS)
chmod +x ~/.config/GIMP/3.0/plug-ins/my_plugin.py
Basic Plugin Structure
Minimal Plugin
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
from gi.repository import GObject
from gi.repository import GLib
def my_plugin(procedure, run_mode, image, n_drawables, drawables, args, data):
"""Main plugin function."""
Gimp.message("Hello from my plugin!")
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
class MyPlugin(Gimp.PlugIn):
## Gimp.PlugIn virtual methods ##
def do_query_procedures(self):
"""Return list of procedure names."""
return ["plug-in-my-plugin"]
def do_create_procedure(self, name):
"""Create procedure definition."""
procedure = Gimp.ImageProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
my_plugin, None
)
procedure.set_image_types("RGB*")
procedure.set_documentation(
"My Plugin Description",
"Detailed help text",
name
)
procedure.set_menu_label("My Plugin")
procedure.add_menu_path("<Image>/Filters/Custom/")
procedure.set_attribution("Author", "Author", "2024")
return procedure
Gimp.main(MyPlugin.__gtype__, sys.argv)
Plugin with Parameters
#!/usr/bin/env python3
import gi
gi.require_version('Gimp', '3.0')
gi.require_version('Gegl', '0.4')
from gi.repository import Gimp, Gegl, GObject, GLib
def my_filter(procedure, run_mode, image, n_drawables, drawables, args, data):
"""Apply filter with user parameters."""
# Get parameters
blur_amount = args.index(0)
opacity = args.index(1)
# Get active drawable (layer)
drawable = drawables[0]
# Apply GEGL operation
Gimp.drawable_filter_new(drawable, "gegl:gaussian-blur")
# Update image
Gimp.displays_flush()
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
class MyFilterPlugin(Gimp.PlugIn):
def do_query_procedures(self):
return ["plug-in-my-filter"]
def do_create_procedure(self, name):
procedure = Gimp.ImageProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
my_filter, None
)
procedure.set_image_types("RGB*, GRAY*")
procedure.set_documentation(
"Apply custom blur filter",
"Applies a configurable blur to the image",
name
)
procedure.set_menu_label("My Blur Filter")
procedure.add_menu_path("<Image>/Filters/Blur/")
# Add parameters
procedure.add_argument_from_property(
GObject.Value(GObject.TYPE_DOUBLE),
"blur-amount",
"Blur Amount",
"Radius of the blur",
0.0, 100.0, 5.0
)
procedure.add_argument_from_property(
GObject.Value(GObject.TYPE_DOUBLE),
"opacity",
"Opacity",
"Filter opacity (0-100)",
0.0, 100.0, 100.0
)
return procedure
Gimp.main(MyFilterPlugin.__gtype__, sys.argv)
Procedure Registration
Procedure Types
# Image procedure (operates on image)
procedure = Gimp.ImageProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
callback, data
)
# Load procedure (file import)
procedure = Gimp.LoadProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
callback, data
)
# Save procedure (file export)
procedure = Gimp.SaveProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
callback, data
)
# Brush procedure (create brushes)
procedure = Gimp.BrushProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
callback, data
)
Menu Registration
procedure.add_menu_path("<Image>/Filters/MyFilters/")
procedure.add_menu_path("<Image>/Edit/") # Edit menu
procedure.add_menu_path("<Image>/Select/") # Select menu
procedure.add_menu_path("<Image>/View/") # View menu
procedure.add_menu_path("<Image>/Image/") # Image menu
procedure.add_menu_path("<Image>/Layer/") # Layer menu
procedure.add_menu_path("<Image>/Colors/") # Colors menu
procedure.add_menu_path("<Image>/Tools/") # Tools menu
procedure.add_menu_path("<Filters>/") # Filters menu
procedure.add_menu_path("<Toolbox>/Xtns/") # Extensions
procedure.add_menu_path("<Image>/Filters/Custom/My Plugin")
Image Types
procedure.set_image_types("RGB*") # RGB images (any alpha)
procedure.set_image_types("RGBA") # RGB with alpha only
procedure.set_image_types("RGB,GRAY") # RGB or grayscale
procedure.set_image_types("*") # All image types
procedure.set_image_types("INDEXED*") # Indexed images
Parameters
# Boolean
procedure.add_argument(
GObject.param_spec_boolean(
"preview",
"Preview",
"Show preview",
True, # default
GObject.ParamFlags.READWRITE
)
)
# Integer
procedure.add_argument(
GObject.param_spec_int(
"radius",
"Radius",
"Blur radius in pixels",
1, # min
100, # max
5, # default
GObject.ParamFlags.READWRITE
)
)
# Float/Double
procedure.add_argument(
GObject.param_spec_double(
"amount",
"Amount",
"Effect amount (0-100)",
0.0, # min
100.0, # max
50.0, # default
GObject.ParamFlags.READWRITE
)
)
# String
procedure.add_argument(
GObject.param_spec_string(
"text",
"Text",
"Text to render",
"", # default
GObject.ParamFlags.READWRITE
)
)
# Enum
from gi.repository import Gimp
procedure.add_argument(
Gimp.param_spec_enum(
"blend-mode",
"Blend Mode",
"Layer blend mode",
Gimp.LayerMode.__gtype__,
Gimp.LayerMode.NORMAL,
GObject.ParamFlags.READWRITE
)
)
# Color
procedure.add_argument(
Gimp.param_spec_rgb(
"color",
"Color",
"Foreground color",
True, # has alpha
Gimp.RGBA(1.0, 0.0, 0.0, 1.0), # default red
GObject.ParamFlags.READWRITE
)
)
Image Operations
Accessing Image and Drawable
def my_plugin(procedure, run_mode, image, n_drawables, drawables, args, data):
# Get current image
width = image.get_width()
height = image.get_height()
base_type = image.get_base_type() # RGB, GRAY, INDEXED
# Get active drawable (layer or mask)
drawable = drawables[0]
drawable_width = drawable.get_width()
drawable_height = drawable.get_height()
has_alpha = drawable.has_alpha()
bpp = drawable.get_bpp() # Bytes per pixel
# Get selection bounds
non_empty, x1, y1, x2, y2 = Gimp.selection_bounds(image, drawable)
if non_empty:
# Selection exists
selection_width = x2 - x1
selection_height = y2 - y1
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
Layer Operations
def layer_operations(image):
# Get active layer
layer = image.get_selected_layer()
# Create new layer
new_layer = Gimp.Layer.new(
image,
"New Layer",
image.get_width(),
image.get_height(),
Gimp.ImageType.RGBA_IMAGE,
100.0, # opacity
Gimp.LayerMode.NORMAL
)
# Add layer to image
image.insert_layer(new_layer, None, 0) # parent, position
# Layer properties
layer.set_opacity(50.0)
layer.set_mode(Gimp.LayerMode.MULTIPLY)
layer.set_visible(True)
layer.set_name("Renamed Layer")
# Duplicate layer
duplicate = Gimp.Layer.copy(layer)
image.insert_layer(duplicate, None, 0)
# Delete layer
image.remove_layer(layer)
# Merge layers
merged = image.merge_down(layer, Gimp.MergeType.EXPAND_AS_NECESSARY)
# Flatten image
image.flatten()
Selection Operations
def selection_operations(image, drawable):
# Select all
Gimp.selection_all(image)
# Select none
Gimp.selection_none(image)
# Rectangle selection
Gimp.image_select_rectangle(
image,
Gimp.ChannelOps.REPLACE, # REPLACE, ADD, SUBTRACT, INTERSECT
10, 10, 100, 100 # x, y, width, height
)
# Ellipse selection
Gimp.image_select_ellipse(
image,
Gimp.ChannelOps.ADD,
50, 50, 200, 150
)
# Color selection (fuzzy select)
Gimp.image_select_color(
image,
Gimp.ChannelOps.REPLACE,
drawable,
100, 100, # x, y
15.0, # threshold
True, # select-transparent
False, # sample-merged
False, # sample-criterion
False, # sample-threshold-int
)
# Invert selection
Gimp.selection_invert(image)
# Float selection
floating = Gimp.selection_float(drawable, 0, 0)
# Selection bounds
non_empty, x1, y1, x2, y2 = Gimp.selection_bounds(image, drawable)
Pixel Access
def pixel_operations(drawable):
# Get pixel at position
pixel = drawable.get_pixel(100, 100)
# pixel is a GeglBuffer or bytes
# Set pixel (using GEGL buffer)
buffer = drawable.get_buffer()
# For more complex operations, use GEGL
# or GIMP's drawable procedures
GEGL Operations
Using GEGL Filters
import gi
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
def apply_gegl_blur(drawable, radius):
"""Apply GEGL gaussian blur."""
# Create GEGL node
graph = Gegl.Node()
# Source node (from drawable)
src = graph.create_child("gegl:buffer-source")
src.set_property("buffer", drawable.get_buffer())
# Blur node
blur = graph.create_child("gegl:gaussian-blur")
blur.set_property("std-dev-x", radius)
blur.set_property("std-dev-y", radius)
# Output node
sink = graph.create_child("gegl:buffer-sink")
# Connect nodes
src.connect_to("output", blur, "input")
blur.connect_to("output", sink, "input")
# Process
sink.process()
def apply_gegl_brightness_contrast(drawable, brightness, contrast):
"""Apply brightness-contrast adjustment."""
graph = Gegl.Node()
src = graph.create_child("gegl:buffer-source")
src.set_property("buffer", drawable.get_buffer())
bc = graph.create_child("gegl:brightness-contrast")
bc.set_property("brightness", brightness) # -1.0 to 1.0
bc.set_property("contrast", contrast) # -1.0 to 1.0
sink = graph.create_child("gegl:write-buffer")
sink.set_property("buffer", drawable.get_buffer())
src.connect_to("output", bc, "input")
bc.connect_to("output", sink, "input")
sink.process()
Common GEGL Operations
# Available GEGL operations include:
GEGL_OPERATIONS = {
# Blur
"gegl:gaussian-blur": {"std-dev-x": 5.0, "std-dev-y": 5.0},
"gegl:box-blur": {"radius": 5},
"gegl:motion-blur": {"length": 10, "angle": 0},
# Color Adjustments
"gegl:brightness-contrast": {"brightness": 0.0, "contrast": 0.0},
"gegl:levels": {"in-low": 0.0, "in-high": 1.0, "out-low": 0.0, "out-high": 1.0},
"gegl:curves": {},
"gegl:color-enhance": {},
# Artistic
"gegl:cartoon": {"mask-radius": 7.0, "pct-black": 0.2},
"gegl:oilify": {"mask-radius": 4, "exponent": 8},
"gegl:photocopy": {"mask-radius": 8.0, "sharpness": 0.5},
"gegl:softglow": {"glow-radius": 10.0, "brightness": 0.4},
# Edge Detection
"gegl:edge": {"algorithm": "sobel"},
"gegl:edge-sobel": {},
"gegl:edge-laplace": {},
# Distort
"gegl:lens-distortion": {"main": 0.0, "edge": 0.0},
"gegl:ripple": {"amplitude": 25.0, "period": 200.0},
"gegl:waves": {"amplitude": 10.0, "wavelength": 50.0},
"gegl:whirl-pinch": {"whirl": 90.0, "pinch": 0.0},
# Noise
"gegl:noise-hsv": {"holdness": 2, "hue-distance": 0.1},
"gegl:noise-rgb": {"correlated": False},
"gegl:noise-solid": {},
# Sharpen
"gegl:unsharp-mask": {"std-dev": 5.0, "scale": 0.5},
"gegl:focus-blur": {"radius": 5.0},
# Stylize
"gegl:emboss": {"azimuth": 30.0, "elevation": 45.0, "depth": 20},
"gegl:tile-glass": {"tile-width": 25, "tile-height": 25},
"gegl:mosaic": {"tile-size": 15, "tile-height": 4},
# Effects
"gegl:dropshadow": {"x": 5.0, "y": 5.0, "radius": 10.0},
"gegl:vignette": {"radius": 1.0, "softness": 0.5},
"gegl:fractal-explorer": {},
}
PDB (Procedural Database)
Calling GIMP Procedures
def call_pdb_procedures(image, drawable):
# Get procedure
pdb = Gimp.get_pdb()
# List all procedures
procedures = pdb.query_procedures("", "", "", "", "", "", "")
# Call procedure by name
# Using Gimp procedures
Gimp.context_set_foreground(Gimp.RGBA(1.0, 0.0, 0.0, 1.0))
Gimp.edit_fill(drawable, Gimp.FillType.FOREGROUND)
# Using PDB directly
config = Gimp.ProcedureConfig.new(pdb.lookup_procedure("plug-in-gauss"))
config.set_property("run-mode", Gimp.RunMode.NONINTERACTIVE)
config.set_property("image", image)
config.set_property("drawable", drawable)
config.set_property("horizontal", 5.0)
config.set_property("vertical", 5.0)
result = pdb.run_procedure("plug-in-gauss", config)
Common PDB Procedures
# Gaussian blur
Gimp drawable_filter operations for blur
# Color tools
Gimp.desaturate(image, drawable, Gimp.DesaturateMode.LIGHTNESS)
Gimp.invert(drawable)
Gimp.histogram(drawable, Gimp.HistogramChannel.VALUE, 0.0, 1.0)
# Transform
Gimp.item_transform_flip_simple(drawable, Gimp.OrientationType.HORIZONTAL, True, 0.0)
Gimp.item_transform_rotate_simple(drawable, Gimp.RotationType.ROTATE_90, True, 0, 0)
Gimp.item_transform_scale(drawable, 0, 0, 100, 100, False)
# Edit operations
Gimp.edit_copy(drawable)
Gimp.edit_paste(drawable, False)
Gimp.edit_clear(drawable)
User Interface
Simple Input Dialog
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
def show_dialog(procedure, config, image, drawable):
"""Show a simple dialog for plugin settings."""
dialog = Gtk.Dialog(
title="My Plugin Settings",
parent=None,
flags=Gtk.DialogFlags.MODAL,
buttons=(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK
)
)
dialog.set_default_size(300, 200)
# Content area
box = dialog.get_content_area()
# Grid layout
grid = Gtk.Grid()
grid.set_column_spacing(10)
grid.set_row_spacing(10)
grid.set_margin_top(10)
grid.set_margin_bottom(10)
grid.set_margin_start(10)
grid.set_margin_end(10)
# Radius spin button
label = Gtk.Label(label="Blur Radius:")
grid.attach(label, 0, 0, 1, 1)
spin = Gtk.SpinButton()
spin.set_range(1, 100)
spin.set_value(5)
grid.attach(spin, 1, 0, 1, 1)
# Preview checkbox
preview = Gtk.CheckButton(label="Preview")
preview.set_active(True)
grid.attach(preview, 0, 1, 2, 1)
box.pack_start(grid, True, True, 0)
dialog.show_all()
response = dialog.run()
radius = spin.get_value()
do_preview = preview.get_active()
dialog.destroy()
if response == Gtk.ResponseType.OK:
return True, radius, do_preview
return False, None, None
Color Picker
def color_picker_dialog():
"""Show color selection dialog."""
dialog = Gtk.ColorChooserDialog(
title="Select Color",
parent=None
)
response = dialog.run()
if response == Gtk.ResponseType.OK:
color = dialog.get_rgba()
dialog.destroy()
return Gimp.RGBA(color.red, color.green, color.blue, color.alpha)
dialog.destroy()
return None
File Dialog
def file_open_dialog():
"""Show file open dialog."""
dialog = Gtk.FileChooserDialog(
title="Open Image",
parent=None,
action=Gtk.FileChooserAction.OPEN,
buttons=(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK
)
)
# Add file filter
filter_image = Gtk.FileFilter()
filter_image.set_name("Image files")
filter_image.add_pattern("*.png")
filter_image.add_pattern("*.jpg")
filter_image.add_pattern("*.jpeg")
filter_image.add_pattern("*.tif")
dialog.add_filter(filter_image)
response = dialog.run()
filename = dialog.get_filename() if response == Gtk.ResponseType.OK else None
dialog.destroy()
return filename
Progress Bar
def long_operation_with_progress(image, drawable):
"""Show progress during long operation."""
Gimp.progress_init("Processing image...")
total = 100
for i in range(total):
# Do work...
# Update progress
Gimp.progress_update(i / total)
# Check for user cancellation
if Gimp.user_interrupt():
Gimp.message("Operation cancelled by user")
return
Gimp.progress_update(1.0)
Gimp.progress_end()
File Operations
Opening Files
def open_image(filepath):
"""Open an image file."""
# Using GIMP's file load
image = Gimp.file_load(
Gimp.RunMode.NONINTERACTIVE,
None, # file
filepath
)
return image
Saving Files
def save_image(image, drawable, filepath, file_type="png"):
"""Save image to file."""
if file_type == "png":
procedure = Gimp.get_pdb().lookup_procedure("file-png-save")
elif file_type == "jpeg":
procedure = Gimp.get_pdb().lookup_procedure("file-jpeg-save")
elif file_type == "tiff":
procedure = Gimp.get_pdb().lookup_procedure("file-tiff-save")
config = Gimp.ProcedureConfig.new(procedure)
# PNG specific options
if file_type == "png":
config.set_property("compression", 9)
config.set_property("interlaced", False)
Gimp.file_save(
Gimp.RunMode.NONINTERACTIVE,
image,
drawable,
None, # file
filepath
)
Export with Options
def export_as_jpeg(image, drawable, filepath, quality=0.85):
"""Export image as JPEG with quality setting."""
procedure = Gimp.get_pdb().lookup_procedure("file-jpeg-save")
config = Gimp.ProcedureConfig.new(procedure)
config.set_property("quality", quality)
config.set_property("smoothing", 0.0)
config.set_property("optimize", True)
config.set_property("progressive", False)
config.set_property("baseline", True)
config.set_property("sub-sampling", 2) # 0=4:4:4, 1=4:2:2, 2=4:2:0
config.set_property("restart", 0)
config.set_property("dct", 1) # 0=int, 1=float, 2=fast-int
result = procedure.run(config)
Complete Plugin Example
Batch Resize Plugin
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Batch Resize Plugin for GIMP 3.0+
Resizes all open images to specified dimensions
"""
import gi
gi.require_version('Gimp', '3.0')
gi.require_version('Gegl', '0.4')
gi.require_version('Gtk', '3.0')
from gi.repository import Gimp, Gegl, GObject, GLib, Gtk
import sys
def batch_resize(procedure, run_mode, image, n_drawables, drawables, args, data):
"""Resize all open images."""
# Get parameters
width = args.index(0)
height = args.index(1)
maintain_aspect = args.index(2)
# Get all open images
images = Gimp.image_list()
Gimp.progress_init("Batch resizing images...")
for i, img in enumerate(images):
Gimp.progress_set_text(f"Processing {img.get_name()}")
if maintain_aspect:
# Calculate aspect ratio
orig_width = img.get_width()
orig_height = img.get_height()
aspect = orig_width / orig_height
if width / height > aspect:
new_width = int(height * aspect)
new_height = height
else:
new_width = width
new_height = int(width / aspect)
else:
new_width = width
new_height = height
# Scale image
Gimp.image_scale(img, new_width, new_height,
Gimp.InterpolationType.CUBIC)
# Update progress
Gimp.progress_update((i + 1) / len(images))
Gimp.progress_end()
Gimp.displays_flush()
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
class BatchResizePlugin(Gimp.PlugIn):
## Gimp.PlugIn virtual methods ##
def do_query_procedures(self):
return ["plug-in-batch-resize"]
def do_create_procedure(self, name):
procedure = Gimp.ImageProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
batch_resize, None
)
procedure.set_image_types("*")
procedure.set_documentation(
"Batch resize all open images",
"Resizes all open images to the specified dimensions",
name
)
procedure.set_menu_label("Batch Resize")
procedure.add_menu_path("<Image>/Image/Resize/")
procedure.set_attribution("Author", "Author", "2024")
# Parameters
procedure.add_argument(
GObject.param_spec_int(
"width",
"Width",
"Target width in pixels",
1, 10000, 800,
GObject.ParamFlags.READWRITE
)
)
procedure.add_argument(
GObject.param_spec_int(
"height",
"Height",
"Target height in pixels",
1, 10000, 600,
GObject.ParamFlags.READWRITE
)
)
procedure.add_argument(
GObject.param_spec_boolean(
"maintain-aspect",
"Maintain Aspect Ratio",
"Keep original aspect ratio",
True,
GObject.ParamFlags.READWRITE
)
)
return procedure
Gimp.main(BatchResizePlugin.__gtype__, sys.argv)
Artistic Filter Plugin
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Watercolor Effect Plugin for GIMP 3.0+
Creates a watercolor painting effect
"""
import gi
gi.require_version('Gimp', '3.0')
gi.require_version('Gegl', '0.4')
from gi.repository import Gimp, Gegl, GObject, GLib
import sys
def watercolor_effect(procedure, run_mode, image, n_drawables, drawables, args, data):
"""Apply watercolor effect."""
drawable = drawables[0]
# Get parameters
brush_size = args.index(0)
color_simplification = args.index(1)
gradient_smoothing = args.index(2)
Gimp.progress_init("Applying watercolor effect...")
Gimp.progress_update(0.1)
# Create working layer
layer_copy = Gimp.Layer.copy(drawable)
image.insert_layer(layer_copy, None, 0)
Gimp.progress_update(0.2)
# Step 1: Apply oilify effect
graph = Gegl.Node()
src = graph.create_child("gegl:buffer-source")
src.set_property("buffer", layer_copy.get_buffer())
oilify = graph.create_child("gegl:oilify")
oilify.set_property("mask-radius", int(brush_size))
oilify.set_property("exponent", int(color_simplification))
sink = graph.create_child("gegl:write-buffer")
sink.set_property("buffer", layer_copy.get_buffer())
src.connect_to("output", oilify, "input")
oilify.connect_to("output", sink, "input")
sink.process()
Gimp.progress_update(0.5)
# Step 2: Apply slight blur
blur = graph.create_child("gegl:gaussian-blur")
blur.set_property("std-dev-x", gradient_smoothing)
blur.set_property("std-dev-y", gradient_smoothing)
src.set_property("buffer", layer_copy.get_buffer())
src.connect_to("output", blur, "input")
blur.connect_to("output", sink, "input")
sink.process()
Gimp.progress_update(0.8)
# Step 3: Add paper texture (optional)
# This could be done with a noise overlay
Gimp.progress_update(1.0)
Gimp.progress_end()
Gimp.displays_flush()
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
class WatercolorPlugin(Gimp.PlugIn):
def do_query_procedures(self):
return ["plug-in-watercolor"]
def do_create_procedure(self, name):
procedure = Gimp.ImageProcedure.new(
self, name,
Gimp.PDBProcType.PLUGIN,
watercolor_effect, None
)
procedure.set_image_types("RGB*")
procedure.set_documentation(
"Watercolor Effect",
"Transform photo into watercolor painting",
name
)
procedure.set_menu_label("Watercolor Effect")
procedure.add_menu_path("<Image>/Filters/Artistic/")
procedure.set_attribution("Author", "Author", "2024")
procedure.add_argument(
GObject.param_spec_int(
"brush-size",
"Brush Size",
"Size of brush strokes",
1, 50, 8,
GObject.ParamFlags.READWRITE
)
)
procedure.add_argument(
GObject.param_spec_int(
"color-simplification",
"Color Simplification",
"Reduce color complexity",
1, 20, 10,
GObject.ParamFlags.READWRITE
)
)
procedure.add_argument(
GObject.param_spec_double(
"gradient-smoothing",
"Gradient Smoothing",
"Smooth color gradients",
0.0, 10.0, 1.5,
GObject.ParamFlags.READWRITE
)
)
return procedure
Gimp.main(WatercolorPlugin.__gtype__, sys.argv)
Debugging
Logging and Messages
# Show message in GIMP console
Gimp.message("Debug message")
# Log to terminal
print("Debug output", file=sys.stderr)
# Show in error console
Gimp.message("Error occurred!")
# Critical message (shows dialog)
Gimp.critical("Critical error in plugin")
Testing Plugin
# Run GIMP from terminal to see debug output
gimp
# Run with verbose output
G_MESSAGES_DEBUG=all gimp
# Check Python console in GIMP
# Filters -> Python-Fu -> Console
Best Practices
1. Always Use Non-Interactive Mode for Batch
if run_mode == Gimp.RunMode.NONINTERACTIVE:
# No dialogs, use default values
pass
elif run_mode == Gimp.RunMode.INTERACTIVE:
# Show dialog for user input
pass
2. Clean Up Resources
def my_plugin(procedure, run_mode, image, n_drawables, drawables, args, data):
try:
# Do work
pass
finally:
Gimp.progress_end()
Gimp.displays_flush()
3. Undo Groups
def with_undo(image):
# Start undo group
Gimp.image_undo_group_start(image)
try:
# Do operations
pass
finally:
# End undo group
Gimp.image_undo_group_end(image)
4. Handle Exceptions
def my_plugin(procedure, run_mode, image, n_drawables, drawables, args, data):
try:
# Plugin logic
return procedure.new_return_values(
Gimp.PDBStatusType.SUCCESS,
GLib.Error()
)
except Exception as e:
Gimp.message(f"Error: {str(e)}")
return procedure.new_return_values(
Gimp.PDBStatusType.EXECUTION_ERROR,
GLib.Error.new_literal(Gimp.PlugIn.error_quark(), str(e), 0)
)
References
- GIMP Documentation: https://docs.gimp.org/
- GIMP Python Documentation: https://www.gimp.org/docs/python/
- GEGL Operations Reference: https://gegl.org/operations/
- GTK 3 Tutorial: https://python-gtk-3-tutorial.readthedocs.io/
- GIMP Developer Wiki: https://wiki.gimp.org/