RFC 0005 — Native, format-übergreifende Bild-Metadaten¶
| Feld | Wert |
|---|---|
| Status | Accepted — implementiert (PNG/JPEG/JP2/GIF/WebP/TIFF) |
| Datum | 2026-06-29 |
| Autor | lepy lepy@tuta.io |
| Komponente | sdata/imagemeta.py, sdata/sclass/image.py |
| Betrifft | Einbettung von sdata-Metadaten direkt in Bilddateien |
| Validierung | imagemeta.py 100 %; Pillow-Round-Trips für 6 Formate |
Umsetzungsstand. Implementiert.
sdata/imagemeta.pybettet sdata-Metadaten nativ und Pillow-frei in PNG, JPEG, JP2, GIF und WebP ein;Imagenutzt es für eine einheitlichesave/from_file-API über alle Formate.
1. Zusammenfassung¶
sdata bekommt eigenen Code, um Metadaten (das sdata-Metadaten-JSON) direkt in
Bilddateien zu schreiben und zu lesen — mit einer API über alle Formate. Bisher
konnte Image Metadaten nur in PNG einbetten (über Pillows PngInfo); JPEG,
JPEG 2000 und andere Formate blieben außen vor.
Die neue Schicht sdata.imagemeta ist reiner Python-Code (Standardbibliothek,
keine Pillow-Abhängigkeit). Sie erkennt das Format an den Magic-Bytes und schreibt
die Nutzlast in den nativen Metadaten-Träger des jeweiligen Containers:
| Format | Träger der sdata-Nutzlast | Kennung |
|---|---|---|
| PNG | iTXt-Chunk (UTF-8) vor IEND |
Keyword sdata |
| JPEG | APP1-Segment direkt hinter SOI |
sdata\0-Präfix |
| JP2 | uuid-Box (ISO BMFF) vor der jp2c-Codestream-Box |
feste sdata-UUID |
| GIF | Comment-Extension hinter dem Header | sdata\0-Präfix |
| WebP | eigener RIFF-Chunk sdAT |
FourCC sdAT |
| TIFF | privates IFD-Tag (65000), Original-Bytes unverändert | Tag 65000 |
2. Motivation¶
- Format-Unabhängigkeit: eine Tabelle, ein Bild, ein PDF — Metadaten gehören in das Asset, nicht nur in eine Sidecar-Datei. Für Bilder soll das format-agnostisch und mit identischer API funktionieren.
- Kein Tool-Zwang: keine Abhängigkeit von
exiftoolo. ä.; das Schreiben/Lesen ist Teil von sdata. - Pillow-frei lesen: das Auslesen eingebetteter Metadaten darf nicht von einem
optionalen Bild-Backend abhängen —
imagemeta.extractarbeitet auf den rohen Bytes.
3. Entwurf¶
3.1 Fassade (sdata.imagemeta)¶
detect_format(data) -> "png"|"jpeg"|"jp2"|"gif"|"webp"|None
embed(data, payload, fmt=None) -> bytes # replace-Semantik
extract(data, fmt=None) -> str | None # lenient: unbekannt -> None
supported_formats() -> tuple[str, ...]
- Replace-Semantik: eine vorhandene sdata-Nutzlast wird ersetzt, nicht dupliziert (idempotentes erneutes Einbetten).
- Lenient lesen:
extractliefert für unbekannte Formate bzw. Bilder ohne eingebettete NutzlastNone(kein Fehler).embedwirftUnsupportedImageFormatErrorfür nicht unterstützte Formate. - Registry:
fmt -> (embed_fn, extract_fn)— weitere Formate (TIFF, …) sind als zwei kleine Funktionen + ein Registry-Eintrag nachrüstbar.
3.2 Pro Format (Byte-Ebene)¶
- PNG — Chunk-Struktur (
len|type|data|crc). Ein unkomprimierteriTXt-Chunk mit Keywordsdatawird vorIENDeingefügt; CRC-32 übertype+data. - JPEG — Marker-Segmente. Ein
APP1-Segment (sdata\0+ UTF-8) direkt hinter SOI; der Marker-Walk stoppt bei SOS. Limit: 16-bit-Längenfeld → Nutzlast ≤ 65527 Byte (PayloadTooLargeError). - JP2 — ISO-BMFF-Boxen (
LBox|TBox|DBox, optional 64-bitXLBox). Eineuuid-Box mit fester sdata-UUID wird vor derjp2c-Box eingefügt. - GIF — Sub-Block-Streams. Eine Comment-Extension (
0x21 0xFE) mit Präfixsdata\0hinter Header + Logical Screen Descriptor (+ Global Color Table); Nutzlast in 255-Byte-Sub-Blöcken. Der Block-Walker überspringt Bild- und sonstige Extension-Daten korrekt. - WebP — RIFF-Container. Ein eigener Chunk
sdATwird angehängt und die RIFF-Größe aktualisiert. Begründung der Wahl s. u. - TIFF — offset-basierte IFDs. Statt fehleranfälliger Offset-Chirurgie bleiben die
Original-Bytes unverändert (alle bestehenden Offsets, inkl.
StripOffsets, gültig): eine Kopie der ersten IFD — ergänzt um ein privates Tag (65000) mit der Nutzlast — wird ans Dateiende angehängt und der Header auf diese neue IFD umgelenkt. Little- und Big-Endian (II/MM, klassisches TIFF/Magic 42); BigTIFF (Magic 43) ist nicht abgedeckt. Erneutes Einbetten ersetzt logisch die Nutzlast; verwaiste Vorgänger-Bytes bleiben ungenutzt im File (kein Re-Pack).
3.3 Image-Integration¶
Image.save(path)wählt den Container an der Datei-Endung. Liegt der Inhalt bereits in diesem Container vor, wird die Nutzlast ohne Re-Encoding eingebettet (verlustfrei, Pillow-frei); sonst transkodiert Pillow zuerst. Formate ohne nativen Handler werden via Pillow geschrieben (Warnung, keine Einbettung).Image.from_file/from_byteslesen eingebettete Metadaten überimagemeta.extractzurück (Pillow-frei) und mergen sie (update_from_usermetadata).Image.embedded_metadata()liefert die eingebetteteMetadata(oderNone).- Sidecar-Fallback (gleiche API): Formate ohne nativen Träger (z. B. BMP)
werden via Pillow geschrieben und die Metadaten in einem verlustfreien
<filepath>.meta.json-Sidecar abgelegt (gleiche Nutzlast wie eingebettet);from_filemerged einen vorhandenen Sidecar.save(sidecar=True|False|None)steuert die Policy (immer / nie / nur ohne nativen Träger).
4. Designentscheidungen¶
- WebP: eigener
sdAT-Chunk statt VP8X+XMP. Empirisch behält ein zusätzlicher, unbekannter RIFF-Chunk die Dekodier-Integrität (libwebp/Pillow ignorieren unbekannte Chunks; Bildgröße/Pixel bleiben unverändert). Das ist robuster und einfacher als eine VP8X-Promotion mit XMP-Verpackung. Trade-off: ein pedantischer Validator könnte einen „simple"-WebP mit Zusatz-Chunk bemängeln; funktional (Dekodierung + sdata- Round-Trip) ist es einwandfrei. VP8X+XMP bleibt als spätere Verfeinerung möglich. - Hash/Identität. Das Einbetten verändert die Datei-Bytes (und damit deren Hash).
Wer einen stabilen Inhalts-Hash braucht, hasht vor dem Einbetten oder die reinen
Pixel — analog zum Daten-vs-Metadaten-Hash bei
DataFrame(RFC 0004). - Sidecar als Fallback und Komplement. Formate ohne nativen Träger werden über
einen automatischen, verlustfreien
<filepath>.meta.json-Sidecar einheitlich abgedeckt (gleiche Nutzlast wie eingebettet). Für maschinenlesbare Linked Data bleibt zusätzlich der JSON-LD-Sidecar (semantic.write_sidecar) verfügbar; beide teilen dasselbe Metadaten-Modell.
5. Tests / Coverage¶
tests/test_imagemeta.py: synthetische Container-Bytes (Pillow-frei) deckenimagemeta.pyzu 100 % ab — inkl. Replace-Semantik, fehlender Nutzlast, JPEG- Standalone-/Non-FF-Marker, JP2-XLBox/LBox==0/malformed-Guard, GIF mit/ohne (Local) Color Table und Nicht-Comment-Extensions, WebP-Padding. Zusätzlich Pillow-Round-Trips über PNG/JPEG/JP2/GIF/WebP/TIFF (Decodier-Integrität).tests/test_image.py: einheitlicheImage-API über alle sechs Formate + Transkodierung.
6. Kompatibilität / Migration¶
- Strikt additiv:
imagemetaist neu;Image.from_file/from_bytes/savebehalten ihre Signaturen. PNG-Round-Trips bleiben kompatibel (jetzt überiTXtstattPngInfo, identischessdata-Keyword). imagemeta.pyist gemessen (100 %);image.pybleibt wegen des optionalen Pillow-Transkodier-Pfads in der Coverage-omit.
7. Offene Punkte / Zukunft¶
- Weitere native Träger über die Registry, wo ein Container sie bietet, z. B. BigTIFF (Magic 43, 8-byte-Offsets). Formate ohne nativen Träger (BMP, …) sind bereits über den Sidecar-Fallback abgedeckt.
- Optional: WebP VP8X+XMP für strikte Interop; PNG
zTXt(komprimiert) für sehr große Nutzlasten; JPEG Multi-Segment-APP1 jenseits 64 KiB.