GIMP

Ein Modell steuert GIMP 3.0.4 über den Hub, um auf zwei Ebenen ein Banner zu zeichnen und als PNG zu exportieren. Die Seite nennt den Text, den es erhielt, sein Skript, die Antworten des Hubs und das Bild, samt dem Fehler, den sein erstes Skript verbarg: Es lief ohne Fehlschlag und exportierte eine einzige einfarbige Fläche.

Ein Lichtfunke, dessen Strahlen Textzeilen sind, auf einem Schachbrettmuster: GIMP hat den Hintergrund des Bildes entfernt, und das Muster zeigt die Transparenz.
Der Hintergrund dieses Bildes wurde in GIMP von einem Skript des Hubs entfernt; das Schachbrettmuster ist dort zu sehen, wo kein Hintergrund mehr ist.

GIMP mit seiner Brücke starten

Die Brücke läuft im Python von GIMP und startet nie von selbst. Dieser Lauf nutzte den Befehl des Leitfadens für ein GIMP, das ohne Benutzeroberfläche gestartet wird. Der Rechner hatte keinen Bildschirm, und GIMP bricht ohne X-Display selbst mit --no-interface ab; deshalb gab xvfb-run -a ihm ein virtuelles.

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()"

Der Lauf selbst nutzte Port 9311, ein neues Home-Verzeichnis und ein neues Token. Der Port lauschte 5,5 s nach dem Start. get_state für hub listete GIMP danach als verbunden auf, mit der Version, die GIMP meldete:

Antwort des Hubs
{
  "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": []
}

Dem Modell gegebener Text

Dem Modell gegebener 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.

Vor dem Schreiben rief das Modell get_state für gimp auf, wie es der Leitfaden für das Modell verlangt. GIMP hielt kein Bild: "images": [] und "active_image": null.

Geschriebenes Skript

Ein Modell, Claude Sonnet 5.5, schrieb dieses Skript nach dem Leitfaden und nach diesem leeren Zustand. Es ist kein Skript aus den Tests des Projekts.

python · Geschriebenes Skript
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)}

Antwort des Hubs

Antwort des Hubs
{
  "app": "gimp",
  "duration_ms": 432,
  "error": null,
  "result": {
    "layers": [
      "Sun",
      "Sky"
    ],
    "png_bytes": 3594
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}

status ist ok, error ist null, und result ist, was das Skript gesetzt hat. Nichts in der Antwort sagt, dass das Bild falsch ist. Das Modell las das PNG, das das Skript exportiert hatte:

Ein Bild von 800 mal 450 Pixeln, gefüllt mit einer einzigen bernsteinfarbenen Farbe, ohne Farbverlauf und ohne Kreis.
banner.png, wie das erste Skript es exportierte: Jedes an drei Stellen gelesene Pixel ist srgba(242,179,68,1).

Was das erste Bild zeigte

Das Bild hat überall die Farbe des Kreises. Ein zweites Skript, an das geöffnete Bild gesendet, las zwei Dinge, die die Antwort nicht lieferte: die Auswahl nach select_ellipse und die Farben der Ebene Sky an ihrem oberen und an ihrem unteren Rand.

python · Geschriebenes Skript
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())
Antwort des Hubs
{
  "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
}

Die Auswahl war richtig (x1=473, y1=90, x2=593, y2=210, 120 × 120 Pixel), und der Farbverlauf ebenso: Das oberste Pixel von Sky ist dunkelblau und das unterste türkis, in linearen Werten. Also war die Ebene Sun ganz gefüllt worden.

Ein drittes Skript maß die Ursache an einem neuen Bild von 100 × 100 Pixeln, bei gesetzter Auswahl. Es füllte eine transparente Ebene mit layer.fill und eine andere mit layer.edit_fill und las dann das Alpha eines Pixels in der Mitte und eines Pixels in der Ecke:

python · Geschriebenes Skript
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()
Antwort des Hubs
{
  "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 ignoriert die Auswahl: Sein Eckpixel ist deckend. layer.edit_fill hält sich an sie: Sein Eckpixel ist transparent.

Das korrigierte Skript und seine Antwort

Nur das, was fehlschlug, änderte sich. Das Skript leert die Ebene Sun, füllt sie bei weiterhin gesetzter Auswahl erneut mit edit_fill und exportiert das Bild noch einmal über banner.png.

python · Geschriebenes Skript
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)}
Antwort des Hubs
{
  "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
}

Die exportierte Datei hat 232 912 Bytes, gegenüber 3 594 beim einfarbigen Bild. Die Ebene Sun ist in der Mitte des Kreises deckend (sun_inside_alpha) und am Pixel (5, 5) transparent (sun_outside_alpha).

Das erzeugte Bild

Das ist das exportierte banner.png, 800 × 450 Pixel. Die Pixel oben und unten lesen sich srgba(28,57,92,1) und srgba(41,160,149,1), eine Stufe neben #1c385c und #29a196: Der Farbverlauf ist mit Dithering gerastert, daher unterscheidet sich eine Wiederholung bei einigen Pixeln um eine Stufe.

Ein senkrechter Farbverlauf von Dunkelblau oben nach Türkis unten, mit einer bernsteinfarbenen Scheibe oben rechts.
banner.png nach dem korrigierten Skript: der Farbverlauf von #1c385c nach #29a196 und der bernsteinfarbene Kreis.

Referenz-Leitfaden im Repository →

WeiterKrita →