Formatmigration

In der digitalen Langzeitarchivierung gibt es drei wesentliche Erhaltungskonzepte, um digitale Objekte benutzbar zu erhalten:

  • Hardwaremuseum
  • Emulation
  • Formatmigration

Bei der Formatmigration fragt man sich “Welche Eigenschaften sind signifikant, um die Benutzbarkeit zu gewährleisten?”. Ausgehend davon sucht man Dateiformate aus, die diese Eigenschaften abbilden können.

Wenn ein Dateiformat im Laufe der Zeit altert, würde man ein neues Dateiformat auswählen, welches die gleichen Eigenschaften abbilden kann.

Warum TIFF vielleicht gealtert ist

Das TIFF 6.0 Format wird gerne als Speicherformat für Scans von Büchern und Monographien verwendet und war auch mehrfach Gegenstand dieses Blogs. Das Format selbst hat mehr als 30 Jahre auf den Buckel. Das alleine ist kein K.O.-Kriterium. Doch in der LZA-Community wird mit zunehmender Anzahl an digitalen Bildern, insbesondere wenn diese auch in immer größeren Auflösungen vorliegen, der zunehmende Speicherbedarf ein relevanter Kostenfaktor. Eine Möglichkeit dieser Entwicklung zu begegnen, ist Datenkompression zu nutzen. Diese sollte aber die besonderen Belange der Langzeitarchivierung berücksichtigen, dazu später mehr.

Die in TIFF nutzbaren Kompressionsverfahren sind oft veraltet oder so speziell, dass nicht jede Software diese versteht. Auch dies hat seine Ursache darin, dass eine offizielle Registry für TIFF-Erweiterungen nicht mehr existiert.

Datenkompression und Langzeitarchivierung

In der Langzeitarchivierung muss man archivfähige Dateiformate für die Sicherung der digitalen Objekte so auswählen, dass

  • diese selbstbeschreibend,
  • fehlertolerant bei Implementierungsfehlern,
  • deren Spezifikation gut dokumentiert, möglichst einfach und frei zugänglich,
  • gut etabliert,
  • und robust gegen Bitfehler

sind.

Datenkompression scheint auf den ersten Blick gegen den letzten Punkt zu verstoßen. Denn bei einem Bitfehler in einer kompromittierten Datei kann ein Großteil, wenn nicht sogar der gesamte Inhalt unbrauchbar sein, weil die Dekompression nicht mehr möglich ist.

Die Datenkompression besteht aus zwei Klassen: Irrelevanzreduktion und Redundanzentfernung. Im Falle der Irrelevanzreduktion lässt man die Teile der Daten weg, die für den Empfänger nicht relevant sind. Klassische Beispiele sind MP3, bei dem für das menschliche Ohr maskierte Frequenzen erst gar nicht übertragen werden. Kompressionsverfahren, die irrelevante Bestandteile entfernen, nennt man auch lossy. Lossless sind Verfahren, bei dem nur eine bestimmte Verteilung von Symbolen in den Daten genutzt wird, um die Menge der zu übertragenden Bytes zu reduzieren, sprich: eine andere Kodierung gewählt wird.

Beispiele hierfür sind Huffmann-Kodierung und Lauflängenkodierung (statt “aaaaab” wird “5ab” übertragen).

Um eine automatisierte Formatmigration zu gewährleisten, ist es erforderlich, lossless Codecs einzusetzen. Ein netter Nebeneffekt ist, dass eine Degradation der digitalen Objekte nicht erfolgt und eine Rekonstruktion des Originals möglich ist.

In der obigen Auflistung, was bei der Auswahl von archivfähigen Dateiformaten beachtet werden muss, spielt der Punkt “robust gegen Bitfehler” eine wichtige Rolle. Wie kann man also Datenkompression und Robustheit verbinden? Dies erfolgt durch die gezielte Hinzufügung von Redundanzen, z.B. durch CRC. In der LZA-Community ist dieses Vorgehen durch die Verwendung von Matroska-FFV1 als Dateiformat für die verlustfrei komprimierte Speicherung von Videos etabliert.

Bilder und der Einspareffekt durch lossless Codecs

In Tests mit typischen Bildern aus der Retrodigitalisierung von Monographien kommt man im Durchschnitt bei der Verwendung von lossless-Dateiformaten bzw. -Varianten auf Kompressionsraten von 1:2 bis 1:2,5.

Aus den Tests sticht die lossless-Variante von JPEG XL (https://jpeg.org/jpegxl/) mit Kompressionsraten von 1:3 heraus. TIFF mit LZW erreicht Raten von 1:1,5.

Ein Grund genug, über eine Formatmigration TIFF -> JPEG XL nachzudenken.

Was sind die Anforderungen an die Migration?

Für die Migration benötigen wir ein Ziel-Dateiformat, welches die gleichen signifikanten Eigenschaften der digitalen Objekte abbilden kann, wie das Quell-Dateiformat. Da bei großen Datenmengen eine manuelle Prüfung der migrierten Objekte unmöglich ist, aber das Risiko besteht, dass die verwendete Software nicht für jede Variante der Dateiformate fehlerfrei funktioniert, muss sichergestellt sein, dass keine lossy-Kompression verwendet wird.

Da wir davon ausgehen müssen, dass ein Kompressionsalgorithmus als Software-Bibliothek implementiert wird, die gleichermaßen von Kompressor und Dekompressor benutzt wird, wäre es sinnvoll zwei unabhängige Implementierungen zu nutzen. In dem Fall können wir zumindest feststellen, wenn sich beide Implementierungen unterschiedlich verhalten.

Ein Beispielscript für TIFF->JPEG XL

Im folgenden ein Shell-Script, welches diesen Ansatz aufgreift. Es verwendet die voneinander unabhängigen Implementierungen jxl-oxide und cjxl. Das Script erstellt im ersten Schritt aus einem TIFF eine JPEG XL Datei. Danach wird mit der anderen Implementierung das JPEG XL in eine PPM Datei umgewandelt. Das ursprüngliche TIFF wird auch in eine PPM Datei umgewandelt. Danach werden beide PPM Dateien verglichen. Sind sie identisch, so wissen wir, a) das JPEG XL wurde lossless komprimiert und b) in den Implementierungen gibt es keine unterschiedlichen Auffassungen, wie die JPEG XL Spezifikation umzusetzen ist.

PPM ist ein einfaches, unkomprimiertes Bilddatenformat und dadurch gut geeignet einen byteweisen Vergleich durchzuführen.

Hier das Script:

#!/bin/bash
#
# check if JPEG XL encoding is really lossless and different encoder / decoder can handle this.
# script by Andreas Romeyke, (c) 2026
# licensed under GPL v3.0


red=`tput setaf 1`
green=`tput setaf 2`
reset=`tput sgr0`

cecho() {
	color="$1"
	if [[ $(tput -T$TERM colors) -ge 8 ]]; then
		printf "%s" "$color"
	        echo $@
	        printf "%s" "$reset"
	else
		echo $@
	fi
}

if [ -z "$JPEGXL_ENCODER" ]; then
JPEGXL_ENCODER=/usr/bin/cjxl
#JPEGXL_ENCODER="${HOME}/git/libjxl/build/tools/cjxl"
fi
JPEGXL_DECODER="${HOME}/.cargo/bin/jxl-oxide"
IMAGE_MAGICK=/usr/bin/convert
SHA512=/usr/bin/sha512sum
TEMP1_PPM=$( mktemp "/tmp/PPM1_XXXXXXXX.ppm" )
TEMP1_PNG=$( mktemp "/tmp/PPM1_XXXXXXXX.png" )
TEMP2_PPM=$( mktemp "/tmp/PPM2_XXXXXXXX.ppm" )
TEMP2_PNG=$( mktemp "/tmp/PPM2_XXXXXXXX.png" )

info() {
  echo $"Using ff. tools:"
  echo "JPEGXL encoder: $JPEGXL_ENCODER"
  echo "JPEGXL decoder: $JPEGXL_DECODER"
}

help() {
  echo $"
this script reads image file IMG.tiff and converts it to IMG.jxl
It proves the conversion is lossless by using different JPEG-XL en-/decoders

call it: $0 [-h|-i IMG.tiff -o IMG.jxl]

      -h ................ to print this help
      -V ................ print used JPEG XL Tools
      -i IMG.tiff ....... reads IMG
      -o IMG.jxl ........ writes IMG as JPEG-XL

the script is licensed under terms of GPL 3.0, see file LICENSE

You can use the environment variables JPEGXL_ENCODER and JPEGXL_DECODER to define own binaries
"
}

check_prerequisites() {
  ANYMISSED=""
  if [[ ! -e "${JPEGXL_ENCODER}" ]]; then cecho $red $"missed JPEG XL encoder" " '${JPEGXL_ENCODER}'"; ANYMISSED=1; fi
  if [[ ! -e "${JPEGXL_DECODER}" ]]; then cecho $red $"missed JPEG XL decoder" " '${JPEGXL_DECODER}'"; ANYMISSED=1; fi
  if [[ ! -e "${IMAGE_MAGICK}" ]]; then cecho $ref $"missed image magick" " '${IMAGE_MAGICK}'"; ANYMISSED=1; fi
  if [[ ! -e "${SHA512}" ]]; then cecho $red $"missed sha512sum" " '${SHA512}'"; ANYMISSED=1; fi
  if [[ -n "${ANYMISSED}" ]]; then exit 1; fi
}

#################### MAIN 

check_prerequisites

# check options
while getopts ':hVo:i:' OPTION; do
  case "${OPTION}" in
    h) help
      exit;;
    i) INPUT_FILE="${OPTARG}";;
    o) OUTPUT_FILE="${OPTARG}";;
    V) info
      exit;;
    \?) cecho $red "Unknown option: -$OPTARG" >&2; exit 1;;
    :) cecho $red "Missing option argument for -$OPTARG" >&2; exit 1;;
    *) cecho $red "Unimplemented option: -$option" >&2; exit 1;;
  esac
done
if [[ ! -e "${INPUT_FILE}" ]]; then
  cecho $red "No inputfile '${INPUT_FILE}' exists or readable";
  exit 1;
fi
if [[ -z "${OUTPUT_FILE}" ]]; then
  cecho $red "Missed outputfile option";
  exit 1;
fi

# end check options

echo "prepare"
${IMAGE_MAGICK} ${INPUT_FILE} -strip ppm:${TEMP1_PPM}
${IMAGE_MAGICK} ${TEMP1_PPM} ${TEMP1_PNG}
SHA_1=$(cat ${TEMP1_PPM} | ${SHA512} -b)
echo "encode"
time ${JPEGXL_ENCODER} ${TEMP1_PNG} ${OUTPUT_FILE} --quality 100 --keep_invisible=1 --responsive=0 --brotli_effort=11 --effort=9 -v
echo "decode"
time ${JPEGXL_DECODER} -o ${TEMP2_PNG} --output-format png -j 6 ${OUTPUT_FILE}
${IMAGE_MAGICK} ${TEMP2_PNG} -strip ppm:${TEMP2_PPM}
SHA_2=$(cat ${TEMP2_PPM} | ${SHA512} -b)
if [[ $SHA_1 = $SHA_2 ]]; then cecho $green "conversion was lossless"
else
	cecho $red "conversion was not lossless, compare PPMs bytewise"
	cmp ${TEMP1_PPM} ${TEMP2_PPM}
	compare -verbose -metric rmse ${TEMP1_PPM} ${TEMP2_PPM} difference.png
fi
rm -f ${TEMP1_PPM} ${TEMP2_PPM} ${TEMP1_PNG} ${TEMP2_PNG} ${ICC_PROFILE}

Falls nicht nur andere Varianten von jxl-oxide und cjxl durch das Setzen der Umgebungsvariablen $JPEGXL_ENCODER und $JPEGXL_DECODER verwendet werden, müssten die Aufrufparameter im Script noch entsprechend angepasst werden.

Im folgenden eine Beispielausgabe des Scripts:

$> ./convert.sh -i tiffs/00000001.tif -o out.jxl
prepare
encode
JPEG XL encoder v0.11.2 [AVX2,SSE4,SSE2]
Read 1995x2868 image, 8499567 bytes, 254.3 MP/s
Encoding [Modular, lossless, effort: 9]
Compressed to 5295.0 kB (7.403 bpp).
1995 x 2868,  2.024 MP/s [2.02, 2.02], , 1 reps, 16 threads.

real	0m2,933s
user	0m39,625s
sys	0m1,160s
decode
2026-08-13T18:34:52.209071Z  INFO jxl_oxide_cli::decode: Image dimension: 1995x2868
2026-08-13T18:34:52.376657Z  INFO jxl_oxide_cli::decode: Took 167.55 ms (34.15 MP/s)

real	0m1,409s
user	0m2,145s
sys	0m0,025s
 conversion was lossless
$> ls -lha out.jxl tiffs/00000001.tif out.png
-rw-rw-r-- 1 art1 art1 5,1M 13. Aug 20:34 out.jxl
-rw-r--r-- 1 art1 art1  17M 13. Aug 18:30 tiffs/00000001.tif
-rw-rw-r-- 1 art1 art1 8,6M 13. Aug 20:37 out.png

Wie man in der Ausgabe sehen kann, komprimiert das PNG ungefähr auf die Hälfte, JPEG XL auf ein Drittel.

Sollte man JPEG XL nehmen?

Dieser Blogbeitrag hat nur anhand von JPEG XL beleuchtet, wie man bei der Formatmigration vorgehen könnte.

Ob JPEG XL allen anderen Anforderungen an ein archivfähiges Dateiformat erfüllt, muss individuell bewertet werden.

Der vorliegende Ansatz erlaubt den Verzicht auf eine Validierung, da diese durch die 2-Wege-Prüfung substituiert wurde.

Was gegen JPEG XL sprechen könnte, wären folgende Punkte:

  • noch keine stabilisierte Referenzimplementierung
  • relativ komplexer Standard, siehe https://jpeg.org/jpegxl/

Dafür spreche:

  • ein Bilddatenformat, sowohl für Lossless, als auch für Lossy Images
  • JPEG XL kann JPEGs rekonstruieren, damit zur Normalisierung von JPEGs aus born digitals geeignet
  • mittlerweile breit unterstützt, unter anderen in allen gängigen Browsern
  • offen standardisiert
  • existierende Referenzimplementierung

Installation von jxl-oxide und cjxl

Unter Debian Trixie kann cjxl mit dem ff. Aufruf von Apt installiert werden:

apt install libjxl-tools

Die libjxl-tools sind die Referenzimplementierung von JPEG XL.

Das jxl-oxide lässt sich über cargo installieren:

cargo install jxl-oxide-cli