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.

A spark of light whose rays are lines of text, on a checkerboard: GIMP removed the background of the image, and the checkerboard shows the transparency.
The background of this image was removed in GIMP by a script of the Hub; the checkerboard shows where there is no background left.

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:

Hub response
{
  "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

Text given to the model
In GIMP, make a banner of 800 by 450 pixels: a vertical gradient from dark blue (#1c385c) at the top to teal (#29a196) at the bottom, on a layer named “Sky”, and an amber (#f2b344) filled circle 120 pixels across on a separate layer named “Sun”, about two thirds of the way across and a third of the way down. Export it as banner.png and leave the image open.

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.

python · Script written
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

Hub response
{
  "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:

An 800 by 450 image filled with a single amber colour, without gradient or circle.
banner.png as the first script exported it: every pixel read, at three points, is srgba(242,179,68,1).

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.

python · Script written
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())
Hub response
{
  "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:

python · Script written
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()
Hub response
{
  "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.

python · Script written
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)}
Hub response
{
  "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.

A vertical gradient from dark blue at the top to teal at the bottom, with an amber disc in the upper right.
banner.png after the corrected script: the gradient from #1c385c to #29a196 and the amber circle.

Reference guide in the repository →

NextKrita →