# LibreOffice

Ein Modell steuert LibreOffice 25.2.3.2 über den Hub, um in Calc ohne geöffnetes Dokument eine Verkaufstabelle mit Formeln und einer Kopfzeile zu bauen und als .ods-Datei zu speichern. Das erste Skript funktionierte. Die Seite nennt den Text, das Skript, die Antworten des Hubs und die Aufnahme.

*Ein Brief des Hubs an die fünf Anwendungen, von einem Skript in Writer gesetzt, mit dem Logo über dem Titel.*

## Fakten

- Erprobte Version: 25.2.3.2
- Modus: `uno`

## LibreOffice und seine Brücke starten

LibreOffice und seine Brücke starten nie auf Wunsch des Modells: Der Bediener startet beide. Ein Starter erledigt das, nach dem [Leitfaden](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/libreoffice.md), und kehrt sofort zurück:

```sh
libremodular-launch libreoffice --bridge-file /usr/share/libremodular-mcp/bridges/uno_bridge.py
```

Der Lauf hatte keinen Bildschirm, ein virtuelles X-Display ersetzte ihn, und er setzte in `config.toml` die Ports 2102 und 9314, nicht die Standardwerte 2002 und 9304. Der Starter gab aus:

```text
LibreOffice started (process 612876), listening for UNO on 127.0.0.1:2102
bridge started (process 612877), listening on 127.0.0.1:9314
a script runs on the current document of its target's family, or with doc set to None when none is open
stop the bridge with 'libremodular-launch libreoffice --stop'; LibreOffice is yours to close
```

LibreOffice lauschte 1,3 s nach dem Start auf UNO, und der Hub wurde danach gestartet. `get_state` für `hub` listete die drei Ziele, die sich die Brücke teilen, `libreoffice-writer`, `libreoffice-calc` und `libreoffice-impress`, als verbunden auf, mit der Version, die LibreOffice meldete:

**Antwort des Hubs**

```json
{
  "app": "hub",
  "app_version": "0.1.0",
  "bridge_version": null,
  "captured_at": "2026-10-21T12:48:38.198Z",
  "connected": true,
  "content_origin": "hub",
  "screenshot": null,
  "state": {
    "modules": [
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-writer"
      },
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-calc"
      },
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-impress"
      }
    ]
  },
  "warnings": []
}
```


## Dem Modell gegebener Text

```text
In LibreOffice Calc, build a quarterly sales sheet named "Sales". Rows: Tent (120, 150, 310, 90), Stove (80, 60, 140, 200), Lantern (45, 70, 65, 120), for the columns Q1 to Q4. Add a Total column and a Total row, computed with formulas. Header row in bold white on dark blue (#1c385c). Save it as sales.ods in your working directory.
```


Vor dem Schreiben rief das Modell `get_state` für `libreoffice-calc` auf. `active` war `null` und `documents` leer, mit einer Warnung, dass LibreOffice kein aktuelles Calc-Dokument hatte. Das ist kein Fehler: Ein Skript läuft dann mit `doc` auf `None` und kann das erste Dokument anlegen.

## Geschriebenes Skript

Ein Modell, Claude Sonnet 5.5, schrieb dieses Skript nach dem Leitfaden und nach diesem Zustand. Es ist kein Skript aus den Tests des Projekts. Es legt das Dokument an, wenn `doc` gleich `None` ist, und speichert mit `storeToURL` innerhalb des Arbeitsverzeichnisses.

**Geschriebenes Skript**

```python
import os

import uno

if doc is None:
    doc = desktop.loadComponentFromURL("private:factory/scalc", "_blank", 0, ())
sheet = doc.Sheets.getByIndex(0)
sheet.Name = "Sales"

header = ["Product", "Q1", "Q2", "Q3", "Q4", "Total"]
data = [("Tent", 120, 150, 310, 90), ("Stove", 80, 60, 140, 200), ("Lantern", 45, 70, 65, 120)]
for column, title in enumerate(header):
    cell = sheet.getCellByPosition(column, 0)
    cell.setString(title)
    cell.CharWeight = 150.0
    cell.CharColor = 0xFFFFFF
    cell.CellBackColor = 0x1C385C
    if column > 0:
        cell.HoriJustify = uno.Enum("com.sun.star.table.CellHoriJustify", "RIGHT")
for row, (name, *values) in enumerate(data, start=1):
    sheet.getCellByPosition(0, row).setString(name)
    for column, value in enumerate(values, start=1):
        sheet.getCellByPosition(column, row).setValue(value)
    sheet.getCellByPosition(5, row).setFormula(f"=SUM(B{row + 1}:E{row + 1})")
last = len(data) + 1
sheet.getCellByPosition(0, last).setString("Total")
for column in range(1, 6):
    letter = "ABCDEF"[column]
    sheet.getCellByPosition(column, last).setFormula(f"=SUM({letter}2:{letter}{last})")
for column in range(0, 6):
    sheet.getCellByPosition(column, last).CharWeight = 150.0
    sheet.Columns.getByIndex(column).Width = 2600

path = os.path.join(LMM_WORK_DIR, "sales.ods")
properties = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
properties[0].Name = "FilterName"
properties[0].Value = "calc8"
doc.storeToURL(uno.systemPathToFileUrl(path), properties)
result = {"sheet": sheet.Name, "grand_total": sheet.getCellByPosition(5, last).getValue(), "ods_bytes": os.path.getsize(path)}
```


## Antwort des Hubs

**Antwort des Hubs**

```json
{
  "app": "libreoffice-calc",
  "duration_ms": 555,
  "error": null,
  "result": {
    "grand_total": 1450.0,
    "ods_bytes": 13472,
    "sheet": "Sales"
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}
```


Die Gesamtsumme, von der Formel berechnet, ist 1 450: 670 für das Zelt, 480 für den Kocher und 300 für die Laterne. Die vom Skript gespeicherte `.ods`-Datei hat 13 472 Bytes. `get_state` beschrieb danach die Tabelle und gab eine Aufnahme zurück:

**Antwort des Hubs**

```json
{
  "app": "libreoffice-calc",
  "app_version": "25.2.3.2",
  "bridge_version": "1",
  "captured_at": "2026-10-21T12:48:39.480Z",
  "connected": true,
  "content_origin": "document",
  "screenshot": "$XDG_RUNTIME_DIR/libremodular-mcp/captures/libreoffice-calc-20261021T124838664Z.png",
  "state": {
    "active": {
      "sheets": [
        {
          "last_column": 5,
          "last_row": 4,
          "name": "Sales"
        }
      ]
    },
    "documents": [
      {
        "family": "calc",
        "title": "Untitled 1"
      }
    ]
  },
  "warnings": []
}
```


## Erzeugtes Bild

Die Aufnahme ist eine vom PNG-Filter von LibreOffice exportierte Seite, 828 × 1 170 Pixel, mit der Tabelle oben und dem Rest weiß. Dieses Bild behält seine oberen 230 Pixel. Die Kopfzeile ist fett und weiß auf `#1c385c`, und die letzte Zeile und die letzte Spalte enthalten die Summen.

![Eine Tabelle mit dem Titel Sales, mit der Kopfzeile Product, Q1 bis Q4 und Total auf einem dunkelblauen Band, drei Produktzeilen und einer fetten Zeile Total.](https://libremodular.davalan.fr/images/applications/libreoffice-sales.png)

*Der obere Teil der Aufnahme der Tabelle Sales, wie der Hub sie exportiert hat.*


*Die Verkaufstabelle des Rezepts und ihr Balkendiagramm, vom selben Skript in Calc erzeugt.*


> **Grenze**
>
> Nur das Ziel Calc wurde gesteuert. Nicht ausprobiert: eine echte Desktop-Sitzung, die Aufnahme eines Dokuments mit mehreren Tabellenblättern oder Seiten und ein LibreOffice, das langsamer startet, als die Brücke wartet. Verbindet sich der Hub, bevor LibreOffice lauscht, zeigt er die Version als `unknown`, wie die Seite [Kompatibilität](https://libremodular.davalan.fr/de/compatibility/index.md#libreoffice) festhält. Ein bereits geöffnetes LibreOffice von Ihnen hält den Starter an, der sich nie an eine Instanz hängt, die ungespeicherte Arbeit enthalten kann.

[Referenz-Leitfaden im Repository](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/libreoffice.md)

