# GIMP

Un modèle pilote GIMP 3.0.4 par le Hub pour dessiner une bannière sur deux calques et l'exporter en PNG. Cette page donne le texte qu'il a reçu, son script, les réponses du Hub et l'image, avec l'erreur que son premier script a cachée : il s'est exécuté sans échec et a exporté une seule couleur unie.

*Le fond de cette image a été retiré dans GIMP par un script du Hub ; le damier se voit là où il n'y a plus de fond.*

## Faits

- Version essayée : 3.0.4
- Mode : `tcp`

## Démarrer GIMP avec son pont

Le pont tourne dans le Python de GIMP et ne démarre jamais de lui-même. Cette exécution a utilisé la commande du [guide](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/gimp.md) pour un GIMP lancé sans son interface. La machine n'avait pas d'écran, et GIMP s'arrête sans affichage X même avec `--no-interface` : `xvfb-run -a` lui en a donc donné un virtuel.

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

L'exécution elle-même a utilisé le port 9311, un répertoire personnel neuf et un jeton neuf. Le port s'est mis à l'écoute 5,5 s après le démarrage de GIMP. `get_state` sur `hub` a alors listé GIMP comme connecté, avec la version que GIMP déclarait :

**Réponse du Hub**

```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": []
}
```


## Texte donné au modèle

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


Avant d'écrire, le modèle a appelé `get_state` sur `gimp`, comme le demande le [guide du modèle](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/GUIDE_LLM.md). GIMP ne contenait aucune image : `"images": []` et `"active_image": null`.

## Script écrit

Un modèle, Claude Sonnet 5.5, a écrit ce script à partir du guide et de cet état vide. Ce n'est pas un script tiré des tests du projet.

**Script écrit**

```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)}
```


## La réponse du Hub

**Réponse du Hub**

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


`status` vaut `ok`, `error` vaut `null` et `result` est ce que le script a défini. Rien dans la réponse ne dit que l'image est fausse. Le modèle a lu le PNG que le script avait exporté :

![Une image de 800 par 450 remplie d'une seule couleur ambre, sans dégradé ni cercle.](https://libremodular.davalan.fr/images/applications/gimp-first-attempt.png)

*banner.png tel que le premier script l'a exporté : chaque pixel lu, en trois points, vaut srgba(242,179,68,1).*


## Ce que la première image a montré

L'image a partout la couleur du cercle. Un deuxième script, envoyé à l'image ouverte, a lu deux choses que la réponse ne donnait pas : la sélection après `select_ellipse`, et les couleurs du calque `Sky` en haut et en bas.

**Script écrit**

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


**Réponse du Hub**

```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
}
```


La sélection était juste (`x1=473, y1=90, x2=593, y2=210`, 120 par 120 pixels) et le dégradé aussi : le pixel du haut de `Sky` est bleu foncé et celui du bas turquoise, en valeurs linéaires. Le calque `Sun` avait donc été rempli en entier.

Un troisième script a mesuré la cause sur une image neuve de 100 par 100, la sélection étant définie. Il a rempli un calque transparent avec `layer.fill` et un autre avec `layer.edit_fill`, puis a lu l'alpha d'un pixel au centre et d'un pixel dans le coin :

**Script écrit**

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


**Réponse du Hub**

```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` ignore la sélection : son pixel de coin est opaque. `layer.edit_fill` la respecte : son pixel de coin est transparent.

## Le script corrigé et sa réponse

Seul ce qui a échoué a changé. Le script vide le calque `Sun`, le remplit de nouveau avec `edit_fill` alors que la sélection est toujours définie, et exporte l'image de nouveau par-dessus `banner.png`.

**Script écrit**

```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)}
```


**Réponse du Hub**

```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
}
```


Le fichier exporté fait 232 912 octets, contre 3 594 pour l'image unie. Le calque `Sun` est opaque au centre du cercle (`sun_inside_alpha`) et transparent au pixel (5, 5) (`sun_outside_alpha`).

## L'image produite

Voici le `banner.png` exporté, de 800 par 450 pixels. Les pixels du haut et du bas valent `srgba(28,57,92,1)` et `srgba(41,160,149,1)`, à un niveau près de `#1c385c` et de `#29a196` : le dégradé est tramé, si bien qu'un rejeu diffère d'un niveau sur certains pixels.

![Un dégradé vertical du bleu foncé en haut au turquoise en bas, avec un disque ambre en haut à droite.](https://libremodular.davalan.fr/images/applications/gimp-banner.png)

*banner.png après le script corrigé : le dégradé de #1c385c à #29a196 et le cercle ambre.*


> **Limite**
>
> Non essayé : démarrer le pont depuis la console Python interactive de GIMP, plusieurs images ouvertes en même temps, et les calques de texte (GIMP a été lancé avec `--no-fonts`). Le [guide](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/gimp.md) liste les appels de GIMP 3.0 qui échouent sans lever d'erreur, dont `layer.fill` avec une sélection.

[Guide de référence dans le dépôt](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/gimp.md)

