# 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.

*The background of this image was removed in GIMP by a script of the Hub; the checkerboard shows where there is no background left.*

## Facts

- Version tried: 3.0.4
- Mode: `tcp`

## 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](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/gimp.md) 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.

```sh
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**

```json
{
  "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
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](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/GUIDE_LLM.md) 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.

**Script written**

```python
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**

```json
{
  "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.](https://libremodular.davalan.fr/images/applications/gimp-first-attempt.png)

*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.

**Script written**

```python
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**

```json
{
  "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:

**Script written**

```python
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**

```json
{
  "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`.

**Script written**

```python
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**

```json
{
  "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.](https://libremodular.davalan.fr/images/applications/gimp-banner.png)

*banner.png after the corrected script: the gradient from #1c385c to #29a196 and the amber circle.*


> **Limit**
>
> Not tried: starting the bridge from GIMP's interactive Python console, several images open at once, and text layers (GIMP was started with `--no-fonts`). The [guide](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/gimp.md) lists the calls of GIMP 3.0 that fail without raising an error, `layer.fill` with a selection among them.

[Reference guide in the repository](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/gimp.md)

