About gimp
A skill for controlling GIMP 3 through its built-in Script-Fu server, intended for users automating image creation, inspection, and export. It runs Python scripts in the active GUI and includes API references and working recipes.
Key features
- 01Send Python scripts through Script-Fu
- 02Control images, layers, selections, and text
- 03Apply editable GEGL filters
- 04Call PDB procedures and export files
- 05Create thumbnails to inspect composites
Use cases
- 01Build a poster and export XCF and PNG
- 02Edit an image already open in GIMP
- 03Process folders or export layers individually
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ jablonkai/ skills --skill gimpAfter installing, type/gimpin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/api-reference.mdrecipes.mdscripts/example-poster.pygimp_client.pygimp-send.shgimp-start.shDetails
From the project READMEGIMP Control
GIMP (/Applications/GIMP.app) embeds Python 3 and exposes its whole document model
through libgimp over GObject Introspection (gi.repository.Gimp), with GEGL for
pixels and filters. Drive it through GIMP's own Script-Fu server — a TCP endpoint
GIMP ships with, so there is nothing to install — which hands each script to GIMP's
python-fu-eval. The script runs against the live GIMP: the canvas updates, the layer
stack is the one the user is looking at, and nothing happens headless. Every call in
this skill was run against GIMP 3.2.6.
- scripts/gimp-send.sh — send a
.pyfile (or-c 'inline code') and print its captured output;--pingchecks the connection,--schemeevaluates raw Script-Fu. - scripts/gimp-start.sh — launch the GIMP GUI with the Script-Fu server listening, and wait until it answers.
- scripts/gimp_client.py — the wire protocol, used by both.
- scripts/example-poster.py — a complete build → verify → export script to copy the shape from.
- references/api-reference.md — the version-checked libgimp API (images, layers, selections, drawing, text, GEGL filters, PDB calls, pixel readback, export). Read it before writing anything past the cheatsheet below.
- references/recipes.md — working on the user's own image, batch folders, per-layer export, masks, animation, and what the API cannot do.
The control loop
-
Check the connection:
bash scripts/gimp-send.sh --ping→{"ok": true, "bridge": "gimp", "port": 10008, "reply": "3.2.6 | open images: 2"}.If nothing answers, there are two different situations and they need different moves:
- GIMP is not running →
bash scripts/gimp-start.shstarts the GUI with the server switched on and waits for it. This opens a window on the user's screen, so say that you are doing it. - GIMP is running without the server → you cannot switch it on remotely, and you
must not start a second instance to get around that: two GIMPs share one config
directory and the user's unsaved work is in the one already open. Ask them for one
click: Filters ▸ Development ▸ Script-Fu ▸ Start Server…, listening on
127.0.0.1, port10008. (Development stays English on a localised GIMP; on a Hungarian one the path reads Szűrők ▸ Development ▸ Script-Fu ▸ Kiszolgáló indítása….)
- GIMP is not running →
-
Write a build script to the scratchpad and send it:
OUT=/path/to/outdir bash scripts/gimp-send.sh /path/build.py.OUTarrives both as a global and asos.environ["OUT"]— and it is the only variable that crosses into GIMP:SRC=... bash gimp-send.shgives the script aKeyError, because it runs in GIMP's process, not your shell's. Write input paths into the script as absolute literals.GIMP_SEND_TIMEOUT=600(seconds) covers batches and big exports. -
Read the feedback. The sender returns the script's captured stdout/stderr, and on failure the Python traceback, exiting non-zero — so
print(...)is your channel back. Nothing else reports: GIMP's own error console stays inside the app. -
Look at the result.
image.get_thumbnail(w, h, Gimp.PixbufTransparency.SMALL_CHECKS)renders the composited image straight to a PNG you can Read. Do this at the end of every build script — one line, and it catches what no assertion will. -
Iterate, then deliver. Fix the script and re-send. Scripts must be re-runnable: create a fresh image per version rather than mutating the one already open, so a re-send cannot stack layers onto the previous attempt.
Each send is a fresh python-fu-eval process, so nothing persists between sends except
what is in GIMP (open images) or on disk. Round-trip is roughly a second — cheap enough
to probe with a throwaway send rather than guess an API.
Scripting cheatsheet
import gi
gi.require_version("Babl", "0.1"); gi.require_version("Gegl", "0.4")
gi.require_version("Gimp", "3.0")
from gi.repository import Babl, Gegl, Gimp, Gio
Gegl.init(None)
Image and layer stack
image = Gimp.Image.new(1000, 1400, Gimp.ImageBaseType.RGB)
layer = Gimp.Layer.new(image, "background", 1000, 1400,
Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(layer, None, 0) # parent=None -> top level; 0 = topmost
layer.set_opacity(70.0) # 0–100 float, not 0–255
layer.set_mode(Gimp.LayerMode.SCREEN)
Gimp.Display.new(image) # show it — the user watches the build happen
Gimp.displays_flush() # nothing repaints without this
To work on what the user already has: Gimp.get_images()[0], most recently active
first. Duplicate it before destructive work — see
recipes.md.
Colour: sRGB in, sRGB out
Gegl.Color stores linear RGB, so the constructor and the setter are not the same
thing and the difference is a visibly washed-out image:
Gegl.Color.new("#1a73e8") # parses sRGB — use this
colour.set_rgba(0.10, 0.45, 0.85, 1.0) # LINEAR components — rarely what you meant
list(colour.get_bytes(Babl.format("R'G'B'A u8")).get_data()) # [26,115,232,255]
Gimp.context_set_foreground(Gegl.Color.new("#ffb703"))
Selections, fills, drawing
image.select_rectangle(Gimp.ChannelOps.REPLACE, x, y, w, h) # also ellipse, polygon,
layer.edit_fill(Gimp.FillType.FOREGROUND) # round_rectangle, color
Gimp.Selection.none(image) # every later edit respects the selection
layer.fill(Gimp.FillType.FOREGROUND) # whole layer, ignores the selection
Gimp.context_set_gradient_fg_bg_rgb()
layer.edit_gradient_fill(Gimp.GradientType.LINEAR, 0.0, False, 1.0, 0.0, True,
x1, y1, x2, y2)
Gimp.context_set_brush(Gimp.Brush.get_by_name("2. Hardness 100"))
Gimp.context_set_brush_size(20.0)
Gimp.paintbrush_default(layer, [40.0, 40.0, 200.0, 200.0]) # ONE drawable, flat x/y list
Text
font = Gimp.context_get_font() # safer than naming one: get_by_name
text = Gimp.TextLayer.new(image, "GIMP", font, 150.0, Gimp.Unit.pixel())
image.insert_layer(text, None, 0)
text.set_color(Gegl.Color.new("#f8f9fa"))
text.set_justification(Gimp.TextJustification.CENTER)
text.set_offsets((image.get_width() - text.get_width()) // 2, 300) # measure AFTER
Filters — prefer non-destructive
GIMP 3 filters stay live and editable, which is the right default on someone else's artwork:
blur = Gimp.DrawableFilter.new(layer, "gegl:gaussian-blur", "glow blur")
config = blur.get_config()
config.set_property("std-dev-x", 90.0); config.set_property("std-dev-y", 90.0)
blur.update() # push config into the filter
layer.append_filter(blur) # or layer.merge_filter(blur) to bake it in
Discover ops and property names instead of guessing — Gegl.list_operations() and
Gegl.Operation.list_properties("gegl:gaussian-blur"). The legacy plug-in-gauss
family is gone in GIMP 3.
Anything without a binding: the PDB
proc = Gimp.get_pdb().lookup_procedure("script-fu-drop-shadow") # None if absent
config = proc.create_config()
config.set_property("run-mode", Gimp.RunMode.NONINTERACTIVE)
config.set_property("image", image)
config.set_core_object_array("drawables", [layer]) # NOT set_property
result = proc.run(config)
Export
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image,
Gio.File.new_for_path(OUT + "/poster.xcf"), None) # .png/.jpg/... too
flat = image.duplicate(); flat.flatten() # flatten a copy, not the original
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, flat,
Gio.File.new_for_path(OUT + "/poster.png"), None)
flat.delete()
.xcf keeps every layer, mask and live filter editable for the user; ship it alongside
the flat export unless they said otherwise. Format options (JPEG quality, GIF timing)
come from calling the export plug-in through the PDB — see
api-reference.md.
Verification
- Returned stdout is the immediate signal —
print(...)comes straight back and a failure returns the traceback with a non-zero exit. - A thumbnail PNG is the visual check: save
image.get_thumbnail(500, 700, Gimp.PixbufTransparency.SMALL_CHECKS)into$OUTand Read it. It is the composited image, so it is honest about blend modes and filters in a way a layer-by-layer description is not. - Numbers:
image.pick_color([...], x, y, True, False, 0.0)sampled throughBabl.format("R'G'B'A u8")proves a colour landed exactly;layer.histogram(...)gives mean/median on a 0–255 scale. Dump size, layer names, offsets, modes and attached filters asmetrics.jsonand Read that rather than eyeballing the stack. - Hand-off: the exported PNG/JPEG opens anywhere; the
.xcfkeeps the work editable.
Gotchas
Gegl.Color.set_rgbais linear,Gegl.Color.new("#hex")is sRGB. Mixing them up silently lightens every colour; read values back throughget_bytes(Babl.format("R'G'B'A u8")), notget_rgba().set_core_object_arrayfor arrays of GIMP objects.config.set_property( "drawables", [layer])raisescould not convert [...] to type 'GimpCoreObjectArray', and reading one back needsresult.get_core_object_array(i)—result.index(i)returns aGBoxedthat will not iterate.- Paint calls take one drawable, not a list (
Gimp.paintbrush_default(layer, pts)), whilepick_colortakes a list of drawables. The asymmetry is real. Gimp.Font.get_by_namereturnsNonefor a missing font, and the failure only surfaces insideTextLayer.newasArgument 2 does not allow None as a value. UseGimp.context_get_font()unless the user named a font, and check the return.- Text layers resize themselves — centre and position them after setting text, size and spacing, or you are measuring the previous layout.
Gimp.displays_flush()after edits, or the user stares at a stale canvas and you both think the script did nothing.- Success flags come first in
get_offsets,pick_color,histogramandselection.bounds. Unpack the tuple; don't index into it blindly. - Resource names are localised — on a Hungarian GIMP the gradients are
Előtérből háttérbe (RGB), notFG to BG (RGB). List them at runtime (Gimp.gradients_get_list("")and siblings) instead of hard-coding English names. image.delete()discards unsaved changes without asking. Call it only on images your own script created; never on the user's.- Group edits with
image.undo_group_start()/undo_group_end()so one Ctrl+Z undoes the whole build rather than four hundred individual operations. - No state survives a send — each one is a new
python-fu-evalprocess, and the script gets a fresh namespace with onlyOUTin it: importos,jsonand thegirepositories yourself. Version your images ("poster_v2") so re-runs don't fight the previous attempt. - A failed send leaves its half-built image open. Call
image.set_file(Gio.File.new_for_path(OUT + "/poster_v2.xcf"))right after creating it; the next send can then find its own leftovers inGimp.get_images()byget_file()anddelete()them, without touching anything the user opened. flatten()fills transparency with the context background colour, which is whatever an earlier fill left there. Set it (Gimp.context_set_background) before flattening, or usemerge_visible_layers(Gimp.MergeType.CLIP_TO_IMAGE)to keep alpha.--batchneeds--batch-interpreterin GIMP 3; without it GIMP prints a list of interpreters and quits.gimp-start.shalready passes it.- A dead GIMP can leave the port bound. The
script-fu-serverplug-in is its own process and outlives a quitting GIMP for a while, so a bare port check says "up" when nothing is behind it.--pingsees through this (the connection closes mid-reply, and the orphan then exits) — if it reports that, just start GIMP again. - GIMP 2.10 tutorials do not port.
gimpfu,pdb.gimp_*, RGB tuples and theplug-in-gaussfilters are all gone, and surviving calls changed shape —edit_bucket_fill(fill_type, x, y)now takes its threshold fromGimp.context_set_sample_threshold. Checklookup_procedureforNonebefore trusting a procedure name you read somewhere. - Tools are not scriptable — no transform widget, no free select, no menu-action trigger. Fills, paint calls, selections and GEGL filters are the whole vocabulary; see recipes.md.
Security
There is nothing to install here, but starting the Script-Fu server still turns GIMP into a code-execution server. Say so before asking for that click.
- The server listens on
127.0.0.1:10008and hands whatever arrives topython-fu-eval— arbitrary Python with the user's privileges, over the user's open images. It carries no authentication: every local process, and every other user on a shared machine, can drive GIMP through it. - Always 127.0.0.1. The Start Server dialog accepts any listen address, and a server
bound to
0.0.0.0or a LAN address is remote code execution for the whole network.gimp-start.shpins loopback; if the user starts it by hand, tell them the address. - It is not HTTP — the protocol is length-prefixed with a
Gmagic byte, so a browser's request is rejected at the first byte. That is a side effect of the wire format, not an origin check. - To stop it, quit GIMP. The port can stay bound briefly after GIMP exits while the
plug-in process winds down, so a port check is not proof anything is live —
--pingis (gimp-send.sh --ping).