Zurück

Vom Originalbild ins Web: Sharp, libvips und die Grenzen von XMP

Warum der richtige Fix vor Sharp lag, nicht dahinter.

Damit waren die Metadaten im Originalbild dank aiprov sauber.

Creator, DigitalSourceType und Credit waren gesetzt, der Prompt lag im eigenen Namespace und ExifTool hatte nichts zu meckern. Ich übrigens auch nicht.

Kurz – denn leider veröffentliche ich keine Originalbilder.

Ein Static Site Generator nimmt ein Bild nicht entgegen, um es anschließend ehrfürchtig „as is“ ins Web zu verfrachten. Er verkleinert es, ändert das Format, erzeugt Vorschaubilder – und zusätzlich Material für den Open-Graph. Aus einer Bilddatei wird eine kleine Familie.

Und damit taucht eine neue Frage auf:

Welche Metadaten dürfen die Kinder erben?

Das ist ein Build-Problem.

Das Bild im Ordner ist nicht das Bild im Browser

In meinem Artikel-Bundle liegt ein PNG (meistens) oder JPEG. Im Browser landet später WebP, vielleicht AVIF, in mehreren Breiten und zusätzlich als Social-Media-Vorschau. Diese Bildverwandtschaft erzeugt hadley , mein eigener Static Site Generator. Für die eigentliche Arbeit nutzt er Sharp, das wiederum auf libvips aufsetzt. 1

Als Ergebnis dieses Verarbeitungsprozesses entsteht eine gute Kombination aus schnell, speicherschonend und erprobt. Bis Metadaten dazukommen.

Die Frage aus Teil 2 lautete noch:

Wie bekomme ich saubere Metadaten ins Originalbild?

Jetzt hieß sie:

Welche Metadaten landen in den Dateien, die ich tatsächlich ausliefere?

Ähnlich formuliert, andere Baustelle.

Sharp ist nicht kaputt

Wenn nach der Bildverarbeitung etwas fehlt, schaut man zuerst auf das Tool. Das ist nicht unbedingt fair, aber praktisch. Irgendwer muss ja Schuld haben.

Sharp entfernt Metadaten standardmäßig. 2 Für Bilder im Web ist das erst einmal kein Problem. Viele Metadaten sind Ballast, manche möchte man trotzdem behalten, andere auf gar keinen Fall.

Dafür gibt es withMetadata() beziehungsweise keepMetadata():

sharp(input)
  .resize(1200)
  .webp()
  .withMetadata()
  .toFile(output)

Fertig?

Nicht ganz.

withMetadata() beantwortet die Frage: Wie behalte ich Metadaten?

Ich hatte aber eine andere:

Welche Metadaten sollen überhaupt behalten werden?

Das Arbeitsbild enthält nämlich mehr als die später veröffentlichte Datei wissen muss.

Das Arbeitsbild darf mehr wissen

aiprov schreibt neben öffentlichen Feldern auch interne Angaben in einen eigenen XMP-Namespace:

XMP-<namespace>:aiTool
XMP-<namespace>:destination
XMP-<namespace>:aiPrompt

Das ist Absicht. Im Arbeitsbild möchte ich nachvollziehen können, womit ein Bild entstanden ist, wofür es gedacht war und welcher Prompt dahinterlag.

Der Prompt ist dort eine Notiz. Im Webbild wäre er plötzlich eine Veröffentlichung. Gleiche Zeichenfolge, anderer Kontext. Und schon wird aus Technik Redaktion.

DigitalSourceType , Credit, Creator und gegebenenfalls Beschreibung oder Schlagwörter sollen in den Webbildern bleiben. Der vollständige Prompt, interne Zielangaben oder sonstige Werkstattspäne nicht.

Metadaten zu entfernen ist hier keine Optimierung. Es ist Auswahl. Ein bisschen Hausputz, nur mit XMP.

Mein erster Versuch klappte

Ich setzte beim Output an.

Sharp erzeugte ein WebP. Anschließend öffnete ich den Buffer, suchte den XMP -Chunk und ersetzte ihn durch eine bereinigte Fassung. WebP ist ein RIFF-Container, die Chunks sind gut erkennbar, und das Bild muss dafür nicht neu encodiert werden.

Es funktionierte.

Für WebP.

Dann kam AVIF.


AVIF ist kein RIFF-Container. Die WebP-Funktion erkannte den fehlenden Header, tat nichts und gab den Buffer unverändert zurück. Kein Fehler, kein Absturz, kein rotes Licht. Nur ein falsches Ergebnis.

Der Code blieb höflich, während er Mist baute.

AVIF war allerdings nicht das eigentliche Problem. AVIF war nur der Bote, und der Bote hatte recht: Ich hatte die falsche Abstraktion gewählt.

Für jedes Ausgabeformat eine eigene Reparaturfunktion zu schreiben: das bedeutet WebP hier, AVIF dort, JPEG anders und PNG sowieso. Kann man so machen.

Man kann auch mit einem Regenschirm duschen.

Technisch möglich. Architektonisch verdächtig.

Der Fix lag vor Sharp

Ich fragte mich:

Warum bekommt Sharp überhaupt ein Bild, das noch interne Metadaten enthält?

Nicht jedes Derivat braucht eine eigene Metadatenbehandlung. Die Pipeline braucht eine saubere Eingabe.

Also nicht:

Arbeitsbild

Sharp erzeugt Derivate

jedes Derivat wird separat gepatcht

Sondern:

Arbeitsbild

öffentliche Metadaten auswählen

bereinigten Quell-Buffer erzeugen

Sharp erzeugt Derivate

Ich nenne diesen Zwischenzustand Publikationsbild . Kein neues Motiv, keine zusätzliche Datei im Repository, sondern das Arbeitsbild nach der redaktionellen Gepäckkontrolle:

Folgendes bleibt:
- Creator
- Credit
- DigitalSourceType
- Description
- Subject

Das fliegt raus:
- vollständiger Prompt
- interne Zielangaben
- Arbeitsnotizen
- Generatorreste

Erst veröffentlichbar machen. Dann verarbeiten.

Das ist die ganze Pointe.

Sharp war nicht der Ort, an dem ich die Metadaten reparieren musste. Sharp war der Grund, sie vorher zu bereinigen.

Danach wird withMetadata() wieder nützlich. Nicht als magischer Reparaturmodus, sondern als Transportband: Ist die Eingabe sauber, darf Sharp die Metadaten ruhig weiterreichen.

Source-Level-Patching

Technisch lese ich den XMP-Block aus dem Quellbild, entferne die internen Felder und übergebe Sharp statt des ursprünglichen Dateipfads einen bereinigten Buffer.

Stark vereinfacht:

const meta = await sharp(sourceFile).metadata()
const cleanXmp = stripInternalFields(meta.xmp)

const sourceBuffer = await readFile(sourceFile)
const cleanSource = replaceXmpInSource(sourceBuffer, cleanXmp)

await sharp(cleanSource)
  .resize(width)
  .toFormat(format)
  .withMetadata()
  .toFile(dest)

Der wichtige Teil ist nicht replaceXmpInSource(), sondern dessen Position: vor Sharp.

Damit muss die Pipeline nicht wissen, wie man jedes mögliche Zielformat nachträglich flickt. Sie bereinigt ihre Quelle einmal; danach darf Bildverarbeitung wieder Bildverarbeitung sein. Wenn ein späterer Schritt plötzlich weniger wissen muss, ist das meistens ein gutes Zeichen. Natürlich nicht generell – Softwarearchitektur verteilt ihre Gemeinheiten recht demokratisch – aber oft genug.

PNG möchte sein XMP an einer bestimmten Stelle

Ganz ohne Sonderfälle ging es trotzdem nicht.

Bei PNG steckt XMP in einem iTXt-Chunk mit dem Schlüssel XML:com.adobe.xmp. 3 Meine erste Testfunktion setzte einen neuen Chunk direkt vor IEND. Das erschien logisch: vor dem Dateiende ist schließlich noch in der Datei.

libvips sah das XMP trotzdem nicht.

Der Chunk lag hinter den IDAT -Bilddaten. Formal mag das zulässig sein; praktisch liest libvips ihn dort nicht mehr. Die brauchbare Reihenfolge lautet:

IHDR → iTXt/XMP → IDAT → IEND

und nicht:

IHDR → IDAT → iTXt/XMP → IEND

Ein kleiner Unterschied zwischen „Die Spezifikation erlaubt es“ und „Das Tool respektiert das“. Im Alltag gewinnt fast immer die zweite Variante.

ExifTool schreibt den XMP-Chunk vor IDAT . Die Pipeline ersetzt deshalb den vorhandenen Chunk an Ort und Stelle und berechnet dessen CRC neu. Weniger kreativ. Besser.

Open-Graph-Bilder sind Patchwork

Dann kam das Open-Graph-Bild.

Meine OG-Bilder sind JPEGs im Format 1200×630. Sie entstehen nicht nur durch Resize, sondern mit composite(): Hintergrundbild, Titel, Logo und KI-Hinweis werden zu einer neuen Grafik zusammengesetzt.

Und „neu“ meint hier tatsächlich neu.

Nach composite() waren die Metadaten weg. Kein XMP, kein EXIF, nichts. Ein frisch gebackenes JPEG ohne Erinnerung an seine Herkunft.

Hier reicht die bereinigte Eingabe nicht aus. Das OG-Bild muss nach der Komposition wieder öffentliche XMP-Metadaten erhalten. Bei JPEG bedeutet das, ein APP1-Segment nach dem SOI-Marker und gegebenenfalls hinter dem APP0/JFIF-Block einzufügen.

Output-Patching ist an dieser Stelle richtig — nicht als allgemeines Prinzip, sondern weil composite() ein neues Veröffentlichungsbild erzeugt.

Normale Derivate erben von der bereinigten Quelle. Das OG-Bild bekommt seine Metadaten neu.

Familie bleibt Familie. Patchworkfamilie zählt auch.

Ein Signal, drei Ausgaben

Damit war die Dateiseite geklärt. XMP allein reicht trotzdem nicht.

Ein Browser zeigt XMP nicht an. Leser sehen es nicht. Plattformen können es entfernen. Suchmaschinen wiederum betrachten nicht nur die Datei, sondern auch den Kontext der Seite.

Die Pipeline verwendet deshalb dasselbe Ursprungssignal mehrfach:

DigitalSourceType = trainedAlgorithmicMedia

hadley erkennt es per Regex im XMP-Buffer des Quellbilds — einmalig, vor Sharp, unabhängig vom Ausgabeformat. IPTC führt trainedAlgorithmicMedia als „Created using Generative AI”. 4 Daraus leitet hadley drei Dinge ab:

1. XMP in den ausgelieferten Bilddateien
2. JSON-LD im HTML
3. einen sichtbaren Hinweis für Leser

Facebook und Instagram lesen das XMP-Feld automatisch aus und setzen ein „Made with AI”-Label — ohne eigene Konfiguration, allein durch das gesetzte Feld.

Für die strukturierte Web-Ebene wird aus dem Bild zum Beispiel ein ImageObject: 5

{
  "@type": "ImageObject",
  "creditText": "Michael Kortstiege / Ideogram",
  "digitalSourceType": "https://schema.org/TrainedAlgorithmicMediaDigitalSource",
  "description": "KI-generiertes Titelbild für einen Blogartikel"
}

Das JSON-LD pflege ich effizienterweise nicht von Hand. Es wird aus demselben XMP-Signal erzeugt, das auch die Bildpipeline steuert. Eine Quelle, mehrere Ausgaben und damit weniger Stellen, an denen ich mir selbst widersprechen kann.

In Build-Pipelines ist das kein Luxus. Eher Schadensbegrenzung.

Menschen lesen kein JSON-LD

Fehlt nur noch der Mensch.

Niemand lädt beim Lesen eines Blogartikels erst das Titelbild herunter und prüft mit ExifTool, ob darin trainedAlgorithmicMedia steht.

Jedenfalls hoffe ich das.

Falls doch: willkommen. Wir sind vermutlich mindestens beruflich verwandt.

Für alle anderen braucht es einen sichtbaren Hinweis. Bei Inline-Bildern genügt ein Attribut am <figure> -Element — CSS erledigt den Rest, und der Alt-Text bekommt einen Suffix für Screenreader. Beim Cover muss die Zuordnung eindeutig bleiben. Ein einsames „KI-generiert“ im Artikelkopf könnte sich auch auf den Text beziehen. Oder den Autor. Oder den Kaffee.


Also lieber:

Hintergrundbild: KI-generiert

Bei Open-Graph-Bildern hilft CSS nicht, weil Mastodon, LinkedIn und andere Plattformen nur das Bild bekommen. Dort muss der Hinweis in die Pixel.

Daraus ergibt sich eine einfache Regel:

Wer den Rendering-Kontext kontrolliert, kann im Kontext kennzeichnen. Wer nur eine Bilddatei ausliefert, muss die Bilddatei kennzeichnen.

Keine große Theorie. Nur Web.

Na ja, eigentlich doch eine große Theorie – aber mit schlechterer Dokumentation.

Was meine Pipeline jetzt tut

Der fertige Ablauf sieht so aus:

1. aiprov schreibt XMP ins Arbeitsbild
2. hadley erkennt DigitalSourceType
3. interne Felder werden vor Sharp entfernt
4. Sharp erzeugt normale Derivate aus dem bereinigten Buffer
5. diese Derivate behalten die öffentlichen Metadaten
6. OG-Bilder erhalten nach composite() neues XMP
7. JSON-LD wird aus demselben Signal erzeugt
8. sichtbare Hinweise erscheinen im passenden Rendering-Kontext

Das ist mehr Aufwand, als die Ausgangsfrage vermuten ließ. Was auch sonst!

Kleine Tooling-Probleme im Web sehen gern erst harmlos aus und bringen dann eine Spezifikation, mehrere Containerformate und ein unerwartetes Verhalten der eingesetzten Tools mit sich.

Aber der Ablauf ist jetzt ge- und erklärt.

Und das ist mir bei Metadaten wichtiger als Eleganz.

Was offen bleibt

Der Workflow erzeugt keine fälschungssichere Herkunftskette. XMP lässt sich entfernen oder verändern. C2PA wäre dafür die robustere Schicht, benötigt aber eine andere Infrastruktur und überlebt längst nicht jeden Plattformweg.

Auch die rechtliche Auslegung des EU AI Act ist damit nicht abschließend erledigt. hadley ist keine Compliance-Maschine. Die Pipeline bildet eine nachvollziehbare Publikationspraxis für meinen eigenen Blog ab.

Am Horizont: ein IETF-Entwurf für einen AI-Disclosure-HTTP-Response-Header und ein WHATWG-Proposal für <meta name="ai-generated"> – beides ohne Browser-Implementierung, Stand Juli 2026. IPTC und Schema.org sind die heute stabilen Schichten.

Für diesen Zweck reicht sie. Jedenfalls bis mir die nächste Spezifikation freundlich mitteilt, dass ich wieder etwas übersehen habe.

Die eigentliche Lektion

Teil 1 dieser Serie fragte, warum KI-Bilder gekennzeichnet werden müssen (manchmal) und sollten (immer).

Teil 2 führte zu aiprov , weil ein einzelner ExifTool-Aufruf zwar Metadaten schreiben kann, aber noch keine vernünftige redaktionelle Auswahl trifft.

Teil 3 offenbarte, dass ein sauber ausgezeichnetes Originalbild noch keine sauber ausgezeichnete Veröffentlichung bedeutet.

Der entscheidende Fix lag nicht am Ende der Pipeline, sondern an ihrem Beginn. Erst das Bild veröffentlichbar machen, dann die Familie gründen — und bei den Kindern gelegentlich nachsehen, was sie geerbt haben.

Nur zur Sicherheit.


Kommentar schreiben