GIMP
A model drives GIMP 3.0.4 through the Hub to draw a banner on two layers and export it as a PNG. This page gives the text it was given, its script, the Hub’s answers and the image, with the error its first script hid: it ran without a failure and exported one flat colour.

On this page
Start GIMP with its bridge
The bridge runs inside GIMP’s Python and never starts by itself. This run used the command of the guide for a GIMP started without its interface. The machine had no screen, and GIMP stops without an X display even with --no-interface, so xvfb-run -a gave it a virtual one.
LIBREMODULAR_BRIDGE_PORT=9301 \
LIBREMODULAR_TOKEN_PATH=$HOME/.config/libremodular-mcp/token \
xvfb-run -a gimp-3.0 --new-instance --no-data --no-fonts --no-interface --batch-interpreter python-fu-eval \
-b "exec(open('/usr/share/libremodular-mcp/bridges/gimp_bridge.py', encoding='utf-8').read()); serve_main_thread()"
The run itself used port 9311, a new home directory and a new token,. The port listened 5.5 s after the start. get_state on hub then listed GIMP as connected, with the version GIMP reported:
{
"app": "hub",
"app_version": "0.1.0",
"bridge_version": null,
"captured_at": "2026-10-21T12:47:26.453Z",
"connected": true,
"content_origin": "hub",
"screenshot": null,
"state": {
"modules": [
{
"app_version": "3.0.4",
"bridge_version": "1",
"connected": true,
"mode": "tcp",
"name": "gimp"
}
]
},
"warnings": []
}Text given to the model
Before writing, the model called get_state on gimp, as the guide for the model asks. GIMP held no image: "images": [] and "active_image": null.
Script written
A model, Claude Sonnet 5.5, wrote this script from the guide and from that empty state. It is not a script taken from the project’s tests.
import os
image = Gimp.Image.new(800, 450, Gimp.ImageBaseType.RGB)
sky = Gimp.Layer.new(image, "Sky", 800, 450, Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(sky, None, 0)
Gimp.context_set_foreground(Gimp.color_parse_hex("#1c385c"))
Gimp.context_set_background(Gimp.color_parse_hex("#29a196"))
sky.edit_gradient_fill(Gimp.GradientType.LINEAR, 0.0, False, 1, 0.0, True, 0.0, 0.0, 0.0, 450.0)
sun = Gimp.Layer.new(image, "Sun", 800, 450, Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(sun, None, 0)
sun.fill(Gimp.FillType.TRANSPARENT)
image.select_ellipse(Gimp.ChannelOps.REPLACE, 533 - 60, 150 - 60, 120, 120)
Gimp.context_set_foreground(Gimp.color_parse_hex("#f2b344"))
sun.fill(Gimp.FillType.FOREGROUND)
Gimp.Selection.none(image)
path = os.path.join(LMM_WORK_DIR, "banner.png")
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, Gio.File.new_for_path(path), None)
result = {"layers": [layer.get_name() for layer in image.get_layers()], "png_bytes": os.path.getsize(path)}The Hub’s answer
{
"app": "gimp",
"duration_ms": 432,
"error": null,
"result": {
"layers": [
"Sun",
"Sky"
],
"png_bytes": 3594
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}status is ok, error is null and result is what the script set. Nothing in the answer says the picture is wrong. The model read the PNG the script had exported:

What the first image showed
The picture is the colour of the circle everywhere. A second script, sent to the open image, read two things that the answer did not give: the selection after select_ellipse, and the colours of the Sky layer at its top and at its bottom.
image = images()[0]
sun = [layer for layer in image.get_layers() if layer.get_name() == "Sun"][0]
sky = [layer for layer in image.get_layers() if layer.get_name() == "Sky"][0]
ok = image.select_ellipse(Gimp.ChannelOps.REPLACE, 473, 90, 120, 120)
print("select_ellipse returned", ok)
print("bounds", Gimp.Selection.bounds(image))
print("is_empty", Gimp.Selection.is_empty(image))
sky_top = sky.get_pixel(5, 5)
sky_bottom = sky.get_pixel(5, 444)
print("sky pixels", sky_top.get_rgba(), sky_bottom.get_rgba()){
"app": "gimp",
"duration_ms": 141,
"error": null,
"result": null,
"status": "ok",
"stderr": "",
"stdout": "select_ellipse returned True\nbounds (True, non_empty=True, x1=473, y1=90, x2=593, y2=210)\nis_empty False\nsky pixels (red=0.011612247675657272, green=0.04091521352529526, blue=0.10702310502529144, alpha=1.0) (red=0.022173885256052017, green=0.35153278708457947, blue=0.3005438446998596, alpha=1.0)\n",
"truncated": false
}The selection was right (x1=473, y1=90, x2=593, y2=210, 120 by 120 pixels) and so was the gradient: the top pixel of Sky is dark blue and the bottom one teal, in linear values. So the Sun layer had been filled whole.
A third script measured the cause on a new 100 by 100 image, with the selection set. It filled one transparent layer with layer.fill and another with layer.edit_fill, then read the alpha of a pixel at the centre and of a pixel in the corner:
image = Gimp.Image.new(100, 100, Gimp.ImageBaseType.RGB)
Gimp.context_set_foreground(Gimp.color_parse_hex("#f2b344"))
image.select_ellipse(Gimp.ChannelOps.REPLACE, 30, 30, 40, 40)
result = {}
for name, method in (("fill", "fill"), ("edit_fill", "edit_fill")):
layer = Gimp.Layer.new(image, name, 100, 100, Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(layer, None, 0)
layer.fill(Gimp.FillType.TRANSPARENT)
getattr(layer, method)(Gimp.FillType.FOREGROUND)
result[name] = {"alpha_centre": layer.get_pixel(50, 50).get_rgba().alpha, "alpha_corner": layer.get_pixel(2, 2).get_rgba().alpha}
image.delete(){
"app": "gimp",
"duration_ms": 27,
"error": null,
"result": {
"edit_fill": {
"alpha_centre": 1.0,
"alpha_corner": 0.0
},
"fill": {
"alpha_centre": 1.0,
"alpha_corner": 1.0
}
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}layer.fill ignores the selection: its corner pixel is opaque. layer.edit_fill keeps to it: its corner pixel is transparent.
The corrected script and its answer
Only what failed changed. The script empties the Sun layer, fills it again with edit_fill while the selection is still set, and exports the image again over banner.png.
import os
image = images()[0]
sun = [layer for layer in image.get_layers() if layer.get_name() == "Sun"][0]
print("is_empty before", Gimp.Selection.is_empty(image))
sun.fill(Gimp.FillType.TRANSPARENT)
Gimp.context_set_foreground(Gimp.color_parse_hex("#f2b344"))
sun.edit_fill(Gimp.FillType.FOREGROUND)
Gimp.Selection.none(image)
inside = sun.get_pixel(533, 150).get_rgba()
outside = sun.get_pixel(5, 5).get_rgba()
path = os.path.join(LMM_WORK_DIR, "banner.png")
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, Gio.File.new_for_path(path), None)
result = {"sun_inside_alpha": inside.alpha, "sun_outside_alpha": outside.alpha, "png_bytes": os.path.getsize(path)}{
"app": "gimp",
"duration_ms": 2246,
"error": null,
"result": {
"png_bytes": 232912,
"sun_inside_alpha": 1.0,
"sun_outside_alpha": 0.0
},
"status": "ok",
"stderr": "",
"stdout": "is_empty before False\n",
"truncated": false
}The exported file is 232 912 bytes, against 3 594 for the flat image. The Sun layer is opaque at the centre of the circle (sun_inside_alpha) and transparent at the pixel (5, 5) (sun_outside_alpha).
The image produced
This is the exported banner.png, 800 by 450 pixels. The pixels at the top and at the bottom read srgba(28,57,92,1) and srgba(41,160,149,1), one level from #1c385c and #29a196: the gradient is dithered, so a replay differs by one level on some pixels.
