Python-Fu Scripting in GIMP - Automate with Python
Write Python scripts to batch-process hundreds of images, build custom GIMP workflows, and register your own menu commands - No Scheme knowledge required.
Python-Fu vs Script-Fu - When to Choose Python
GIMP supports two scripting languages: Script-Fu (a dialect of Scheme/Lisp) and Python-Fu (Python 3.x). Both expose the same PDB (Procedure Database) of GIMP operations, but Python offers significant advantages for anything beyond simple single-image macros.
Choose Script-Fu when:
- Writing a quick one-off macro
- Following an existing Script-Fu tutorial
- Working on a system without Python installed
- The task is 5–20 lines of simple operations
Choose Python-Fu when:
- Batch processing files or folders
- Reading/writing CSV, JSON, or databases
- Using standard library modules (os, pathlib, re)
- Building complex, maintainable scripts
- Integrating with other Python tools
Opening the Python-Fu Console
The Python-Fu Console is an interactive Python REPL (Read-Eval-Print Loop) with the GIMP module pre-imported. It is the fastest way to test Python-Fu commands before writing a full script file.
-
1Open via menu: Filters → Script-Fu → Python-Fu Console. (In some GIMP builds it appears as Filters → Python-Fu → Console.) A window opens with an input field at the bottom and output area above.
-
2Test a basic command: Type the following in the input field and press Run:
image = gimp.image_list()[0]
print("Image width:", image.width, "Height:", image.height)
The output area should print the dimensions of the currently open image. If you see an error, ensure an image is open in GIMP first.
Python Basics for GIMP
Python-Fu exposes GIMP functionality through two main interfaces: the gimp module (object-oriented, Pythonic) and the pdb object (direct access to GIMP's PDB procedures, same as Script-Fu). You can manipulate layers, apply color adjustments, and export files - All from Python.
Core objects and their properties
# Access image list
images = gimp.image_list()
image = images[0] # first open image
# Image properties
print(image.width) # canvas width in pixels
print(image.height) # canvas height in pixels
print(image.filename) # full file path
print(image.colormap) # color mode info
# Get layers
layers = image.layers # list of all layers
active = image.active_layer # currently selected layer
# Drawable (layer or channel) operations
drawable = image.active_drawable
print(drawable.name) # layer name
print(drawable.opacity) # 0.0 to 100.0
# PDB access (equivalent to Script-Fu pdb calls)
pdb.gimp_image_scale_full(image, 800, 600, INTERPOLATION_LINEAR)
pdb.gimp_displays_flush()
Useful built-in constants
# Interpolation types
INTERPOLATION_NONE # no interpolation (pixelated)
INTERPOLATION_LINEAR # bilinear - Good for photos
INTERPOLATION_CUBIC # bicubic - Best quality for scaling
INTERPOLATION_NOHALO # sharp resize, less halo
# Layer blend modes (common)
LAYER_MODE_NORMAL
LAYER_MODE_MULTIPLY
LAYER_MODE_SCREEN
LAYER_MODE_OVERLAY
LAYER_MODE_GRAIN_MERGE
# Export file types (for pdb.file_png_save etc.)
# Use pdb.file_jpeg_save or pdb.file_png_save
Your First Python Script - Open, Resize, Export
This script opens a single image, scales it to a maximum width of 1200 px while preserving aspect ratio, and exports it as a JPEG. It can be run from the Python-Fu Console or saved as a .py file.
# Single image: open, resize to max 1200px wide, export as JPEG
# Run in Filters -> Script-Fu -> Python-Fu Console
# Or save as a .py file and register as a menu item
import os
def resize_and_export(input_path, output_path, max_width=1200, quality=88):
"""
Open an image, scale to max_width (preserving aspect ratio), export JPEG.
"""
# Load the image
image = pdb.gimp_file_load(RUN_NONINTERACTIVE, input_path, os.path.basename(input_path))
drawable = pdb.gimp_image_get_active_drawable(image)[0]
# Get current dimensions
orig_width = image.width
orig_height = image.height
# Calculate new dimensions (only scale down, never up)
if orig_width > max_width:
scale_factor = max_width / orig_width
new_width = max_width
new_height = int(orig_height * scale_factor)
pdb.gimp_image_scale_full(image, new_width, new_height, INTERPOLATION_CUBIC)
# Flatten the image (required for JPEG export)
flat_layer = pdb.gimp_image_flatten(image)[0]
# Export as JPEG
pdb.file_jpeg_save(
RUN_NONINTERACTIVE,
image,
flat_layer,
output_path,
os.path.basename(output_path),
quality / 100.0, # GIMP uses 0.0-1.0 for JPEG quality
0, # smoothing
1, # optimize
1, # progressive
'', # comment
0, # subsmp (0=4:2:0, 1=4:2:2, 2=4:4:4)
1, # baseline-jpeg
0, # restart-markers
0 # dct (0=int, 1=fixed, 2=float)
)
# Delete image from GIMP memory
pdb.gimp_image_delete(image)
return output_path
# Example usage from the console:
# resize_and_export("/Users/me/photos/input.jpg", "/Users/me/output/resized.jpg")
Batch Processing with Python (os.walk + GIMP Loop)
The following script walks an entire folder tree, finds all JPEG and PNG files, and applies the resize-and-export function to each. This is the core pattern for all batch GIMP automation. For a more detailed look at blend modes you can apply programmatically, see that dedicated guide.
# Batch resize all JPG/PNG files in a folder
# Paste into Python-Fu Console or save as a plug-in .py file
import os
INPUT_DIR = "/Users/me/photos/originals" # Change to your input folder
OUTPUT_DIR = "/Users/me/photos/resized" # Change to your output folder
MAX_WIDTH = 1200
QUALITY = 88
# Supported extensions
EXTENSIONS = ('.jpg', '.jpeg', '.png', '.tif', '.tiff')
def batch_resize(input_dir, output_dir, max_width, quality):
"""
Recursively process all images in input_dir, preserving subfolder structure.
"""
os.makedirs(output_dir, exist_ok=True)
processed = 0
errors = 0
for root, dirs, files in os.walk(input_dir):
# Recreate subfolder structure in output dir
rel_path = os.path.relpath(root, input_dir)
out_root = os.path.join(output_dir, rel_path)
os.makedirs(out_root, exist_ok=True)
for filename in files:
if not filename.lower().endswith(EXTENSIONS):
continue
in_path = os.path.join(root, filename)
# Output always as JPEG
out_name = os.path.splitext(filename)[0] + '.jpg'
out_path = os.path.join(out_root, out_name)
try:
image = pdb.gimp_file_load(
RUN_NONINTERACTIVE, in_path, filename)
orig_width = image.width
if orig_width > max_width:
scale = max_width / orig_width
pdb.gimp_image_scale_full(
image,
max_width,
int(image.height * scale),
INTERPOLATION_CUBIC)
flat = pdb.gimp_image_flatten(image)[0]
pdb.file_jpeg_save(
RUN_NONINTERACTIVE, image, flat,
out_path, out_name,
quality / 100.0, 0, 1, 1, '', 0, 1, 0, 0)
pdb.gimp_image_delete(image)
processed += 1
print("OK:", out_path)
except Exception as e:
errors += 1
print("ERROR:", in_path, str(e))
print(f"\nDone. Processed: {processed}, Errors: {errors}")
# Run the batch
batch_resize(INPUT_DIR, OUTPUT_DIR, MAX_WIDTH, QUALITY)
Class-Based Script Template
For complex workflows, a class structure keeps configuration and logic together and makes scripts easier to maintain and extend.
# Class-based GIMP Python-Fu template
# Save as my_gimp_tool.py in GIMP plug-ins folder
import os
import gimp
from gimpfu import *
class GimpBatchTool:
"""Reusable base class for GIMP batch operations."""
def __init__(self, input_dir, output_dir):
self.input_dir = input_dir
self.output_dir = output_dir
self.processed = 0
self.errors = 0
def process_image(self, image, drawable):
"""Override this method with your per-image operations."""
raise NotImplementedError("Subclass must implement process_image()")
def run(self):
os.makedirs(self.output_dir, exist_ok=True)
for filename in os.listdir(self.input_dir):
if not filename.lower().endswith(('.jpg', '.jpeg', '.png')):
continue
in_path = os.path.join(self.input_dir, filename)
out_path = os.path.join(self.output_dir, filename)
try:
image = pdb.gimp_file_load(RUN_NONINTERACTIVE, in_path, filename)
drawable = pdb.gimp_image_get_active_drawable(image)[0]
self.process_image(image, drawable)
flat = pdb.gimp_image_flatten(image)[0]
pdb.file_jpeg_save(RUN_NONINTERACTIVE, image, flat,
out_path, filename, 0.88, 0, 1, 1, '', 0, 1, 0, 0)
pdb.gimp_image_delete(image)
self.processed += 1
except Exception as e:
self.errors += 1
gimp.message("Error: " + in_path + " - " + str(e))
gimp.message(f"Batch complete. OK: {self.processed}, Errors: {self.errors}")
class WatermarkTool(GimpBatchTool):
"""Add a text watermark to every image."""
def __init__(self, input_dir, output_dir, watermark_text="(c) My Name"):
super().__init__(input_dir, output_dir)
self.watermark_text = watermark_text
def process_image(self, image, drawable):
# Create text layer
text_layer = pdb.gimp_text_fontname(
image, None,
image.width - 260, image.height - 40,
self.watermark_text, 0, TRUE, 18, UNIT_PIXEL,
"Sans Bold")
# Set opacity and blend mode
pdb.gimp_layer_set_opacity(text_layer, 60)
pdb.gimp_layer_set_mode(text_layer, LAYER_MODE_NORMAL)
Key PDB Procedures for Python-Fu
All GIMP procedures are accessible via pdb.procedure-name() in Python-Fu, with hyphens replaced by underscores. The following are the most commonly used in batch scripts.
| PDB Procedure | Python-Fu Call | Description |
|---|---|---|
| gimp-file-load | pdb.gimp_file_load(run_mode, filename, raw_filename) | Load any supported image file; returns image object |
| gimp-image-scale-full | pdb.gimp_image_scale_full(image, w, h, interpolation) | Scale image to exact pixel dimensions |
| gimp-image-flatten | pdb.gimp_image_flatten(image) | Merge all layers; required before JPEG export |
| file-png-save | pdb.file_png_save(run_mode, image, drawable, filename, ...) | Export as PNG with compression control |
| file-jpeg-save | pdb.file_jpeg_save(run_mode, image, drawable, filename, ...) | Export as JPEG; quality 0.0-1.0 |
| gimp-image-delete | pdb.gimp_image_delete(image) | Free image from memory after processing |
| gimp-curves-spline | pdb.gimp_curves_spline(drawable, channel, num_controlpoints, control_pts) | Apply Curves adjustment programmatically |
| gimp-brightness-contrast | pdb.gimp_brightness_contrast(drawable, brightness, contrast) | Adjust brightness (-127 to 127) and contrast |
| plug-in-unsharp-mask | pdb.plug_in_unsharp_mask(run_mode, image, drawable, radius, amount, threshold) | Sharpening via unsharp mask filter |
| gimp-text-fontname | pdb.gimp_text_fontname(image, drawable, x, y, text, border, antialias, size, size_type, fontname) | Add text to an image programmatically |
Lines of Code: Script-Fu vs Python-Fu for the Same Task
Debugging Python-Fu
Python-Fu errors appear in the Python-Fu Console output area or in GIMP's Error Console (Windows → Dockable Dialogs → Error Console).
# Enable verbose error output in Python-Fu Console
import traceback
try:
image = pdb.gimp_file_load(RUN_NONINTERACTIVE, "/path/to/file.jpg", "file.jpg")
# ... your operations ...
except Exception as e:
gimp.message("Full traceback:\n" + traceback.format_exc())
# Use gimp.message() for status output visible in the GIMP GUI
gimp.message("Processing: " + filename)
# Use print() for output that appears in the Python-Fu Console output area
print("Image dimensions:", image.width, "x", image.height)
# Inspect PDB procedure signatures in the console:
# pdb.gimp_file_load.__doc__ (shows parameter list)
print(pdb.gimp_image_scale_full.__doc__)
- ProcedureError: Usually means wrong number of arguments or wrong type. Print the procedure's
.__doc__to see the expected signature. - RuntimeError from GIMP: Often means the image or drawable no longer exists (deleted, or you called a procedure in the wrong order). Check that the image was loaded successfully with
if image is not None. - Script does not appear in Filters menu: The
register()function call must be correct and the file must be in the GIMP plug-ins folder. Check for Python syntax errors by running the script in a standalone Python interpreter first (outside GIMP).
Registering Scripts as Menu Items
Python-Fu scripts saved as .py files in the GIMP plug-ins folder with a register() call and main() call at the bottom appear as menu items in GIMP.
#!/usr/bin/env python3
# Save as: ~/.config/GIMP/3.0/plug-ins/my_watermark/my_watermark.py
# Make executable: chmod +x my_watermark.py (macOS/Linux)
from gimpfu import *
def python_watermark(image, drawable, text, opacity):
"""Add a text watermark to the active image."""
pdb.gimp_image_undo_group_start(image)
text_layer = pdb.gimp_text_fontname(
image, None,
image.width - 280, image.height - 50,
text, 0, TRUE, 24, UNIT_PIXEL, "Sans Bold Italic")
pdb.gimp_layer_set_opacity(text_layer, opacity)
pdb.gimp_image_flatten(image)
pdb.gimp_image_undo_group_end(image)
pdb.gimp_displays_flush()
register(
"python-fu-watermark", # unique procedure name
"Add text watermark", # blurb (short description)
"Adds a semi-transparent text watermark to the bottom-right corner.",
"Your Name", # author
"Your Name", # copyright
"2026", # year
"Add Watermark...", # menu label
"RGB*, GRAY*", # image type (RGB, RGBA, greyscale)
[ # input parameters
(PF_IMAGE, "image", "Input image", None),
(PF_DRAWABLE,"drawable", "Input drawable",None),
(PF_STRING, "text", "Watermark text","(c) 2026"),
(PF_SLIDER, "opacity", "Opacity", 50, (0, 100, 1)),
],
[], # output parameters (none)
python_watermark, # function to call
menu="/Filters/My Scripts" # menu location
)
main()
After saving the file and restarting GIMP (or refreshing plug-ins via Filters → Script-Fu → Refresh Scripts), the watermark script appears under Filters → My Scripts → Add Watermark.
Python-Fu vs CLI (gimp --batch-interpreter)
For fully automated, headless batch processing (on servers, in CI/CD pipelines, or overnight jobs), GIMP can be run from the command line without any GUI. Use the --batch-interpreter flag with Python-Fu.
# Run GIMP headless with Python-Fu batch command
# (macOS/Linux terminal - Adjust GIMP path as needed)
gimp-3.0 \
--no-interface \
--batch-interpreter python-fu-eval \
--batch "
import os, sys
sys.path.insert(0, '/path/to/your/scripts')
def process(in_path, out_path):
image = pdb.gimp_file_load(RUN_NONINTERACTIVE, in_path, os.path.basename(in_path))
pdb.gimp_image_scale_full(image, 800, 600, INTERPOLATION_CUBIC)
flat = pdb.gimp_image_flatten(image)[0]
pdb.file_jpeg_save(RUN_NONINTERACTIVE, image, flat, out_path, os.path.basename(out_path), 0.88, 0, 1, 1, '', 0, 1, 0, 0)
pdb.gimp_image_delete(image)
for f in os.listdir('/input/folder'):
if f.endswith('.jpg'):
process('/input/folder/' + f, '/output/folder/' + f)
pdb.gimp_quit(0)
"
# Windows equivalent (adjust path):
# "C:\Program Files\GIMP 3\bin\gimp-3.0.exe" --no-interface ...
The --no-interface flag suppresses the GUI completely. pdb.gimp_quit(0) at the end ensures GIMP exits cleanly after the batch completes.