Technischer Artikel

TIFF mit 12-Bit-, Lossless- und Arithmetic-JPEG nach PDF

PDFlibPas, die PDF Library for Delphi, importiert TIFF-Bilder, deren JPEG-Kompression über den 8-Bit-Baseline hinausgeht: 12-Bit-Extended-Sequential (SOF1), Huffman-lossless (SOF3), arithmetisch kodiert (SOF9 und SOF10) und Old-Style-TIFF-6.0-JPEG mit Tabellen-Offsets. Weil PDF DCTDecode diese Prozesse nicht tragen kann, dekodiert PDFlibPas sie in nativem Pascal und bettet die Samples verlustfrei mit 8 oder 16 Bit pro Komponente ein

Der volle Umfang braucht v3.724.0 oder neuer und zielt geradewegs auf TIFFs von Scannern, medizinischer und wissenschaftlicher Erfassungssoftware und auf Archivsysteme, die in den 1990ern eingefroren sind. Unten steht, warum sich diese Varianten nicht unverändert ins PDF kopieren lassen, wie PDFlibPas sie unterbringt und welche Pascal-Fallen sich unterwegs fanden

Warum kann ein 12-Bit-JPEG nicht direkt in ein PDF?

Ein 12-Bit-JPEG lässt sich nicht durchreichen, weil PDF keinen Filter hat, der seine Dekodierung verlangt. ISO 32000-1 §7.4.8 definiert DCTDecode rund um das JPEG-Baseline-Format, mit der Progressive-Erweiterung ab PDF 1.3. Nichts in jener Klausel deckt 12-Bit-Sample-Präzision ab, den lossless-Prozess aus ITU-T T.81 Anhang H oder den arithmetischen Entropiecodierer aus Anhang D. Ein Reader, der in einem DCTDecode-Stream auf einen SOF1-12-Bit-Frame oder einen SOF9-Marker trifft, darf nichts zeichnen, und die meisten Viewer machen genau das

Für die 8-Bit-Huffman-Baseline- und Progressive-Strips, die den Großteil der JPEG-in-TIFF-Dateien ausmachen, validiert PDFlibPas weiterhin jeden Scan und reicht die komprimierten Bytes als DCTDecode durch, was das PDF klein hält. Alles andere wird dekodiert. PDFlibPas liest den Frame-Marker und die Sample-Präzision des ersten JPEG-Blocks, und wenn es SOF3, SOF9, SOF10 oder eine andere Präzision als 8 findet, läuft die Seite durch den Sample-Dekodierer statt durch den Durchreich-Pfad. Arithmetische Kodierung an ihrem Frame-Marker zu erkennen, ist wichtiger, als es klingt: Eine Prüfung, die nur auf die Sample-Präzision schaut, lässt einen klassischen TIFF einen 8-Bit-SOF9-Strip in den DCTDecode-Pfad rutschen und erzeugt ein PDF, das Reader nicht darstellen können

Entscheidungsfluss für TIFF-JPEG-Blöcke in PDFlibPas für Delphi: der Frame-Marker lots 8-Bit-Scans zu DCTDecode und andere Prozesse zur nativen Dekodierung
der Frame-Marker, nicht die Sample-Präzision allein, entscheidet, ob ein TIFF-JPEG-Strip als DCTDecode durchgereicht oder nativ dekodiert wird

Was PDFlibPas stattdessen ins PDF schreibt

PDFlibPas macht aus dekodierten JPEG-Blöcken ein gewöhnliches Image-XObject mit BitsPerComponent 8 oder 16. Die Samples werden standardmäßig unkomprimiert gespeichert; rufe CompressImages(1) auf, bevor du das Bild hinzufügst, und PDFlibPas schreibt sie mit FlateDecode. ISO 32000-1 §8.9.5.1 erlaubt 16 Bit pro Komponente ab PDF 1.5, deshalb hebt PDFlibPas die Dokumentversion auf mindestens 1.5 an, sobald ein Bild mit 16-Bit-Samples hinzukommt. Das umfasst Multi-Strip- und gekachelte Bilder, deren Wurzelobjekt ein Form-XObject ohne eigenes BitsPerComponent zum Nachsehen ist

Die Zuordnung ist je JPEG-Prozess fest:

  • 12-Bit-Huffman-JPEG (sequential SOF1, progressive SOF2) wird dequantisiert, rücktransformiert und auf 16-Bit-Samples erweitert
  • Arithmetisch kodiertes JPEG (sequential SOF9, progressive SOF10) wird zu 8-Bit-Samples bei 8-Bit-Präzision und zu 16-Bit-Samples bei 12-Bit-Präzision
  • Huffman-lossless-JPEG (SOF3) wird zu 8-Bit- oder 16-Bit-Samples, exakt passend zu seiner deklarierten Präzision

Die Erweiterung von 12 auf 16 Bit nutzt Bit-Replikation, (V shl 4) or (V shr 8), also bildet 4095 auf 65535 ab und Weiß bleibt Weiß, statt bei 65520 abzuschneiden. PDFlibPas senkt diese Bilder nie hinter deinem Rücken auf 8 Bit. Wenn die Bibliothek so eine Seite für ihre eigene Anzeigeoberfläche rastert, nimmt sie das High-Byte jedes Samples, aber das eingebettete PDF-Bild behält alle sechzehn

Zuordnung dekodierter JPEG-Prozesse zu Sample-Breiten in PDFlibPas für Delphi: 12-Bit-Huffman weitet auf 16 Bit, arithmetic und lossless mappen je deklarierter Präzision
jeder JPEG-Prozess hat eine feste Sample-Breite im PDF, und 12-Bit-Daten werden per Bit-Replikation geweitet, damit Weiß Weiß bleibt

Der Preis ist die Dateigröße: Selbst mit CompressImages(1) sind Flate-komprimierte 16-Bit-Samples größer als die DCT-Daten, aus denen sie stammen, und ohne noch größer. Für Diagnose- und Archiv-Workflows ist dieser Trade meist richtig. Wenn die Größe bei einem bestimmten Job mehr wiegt, zeigt der Leitfaden zum Reduzieren der PDF-Dateigröße in Delphi, wohin die Bytes tatsächlich gehen

Diese TIFFs mit TPDFlib importieren

Der aufrufende Code ändert sich überhaupt nicht. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer nimmt die 1-basierte TIFF-Seitennummer in Options entgegen und liefert eine Bild-ID, oder 0, wenn sich die Seite nicht importieren lässt. Nicht unterstützte JPEG-Varianten werden abgewiesen, bevor das Bild eingefügt wird, eine 0 bedeutet also, dass nichts Halbdekodiertes das Dokument erreicht hat

uses
  PDFlibrary;

function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
  Lib: TPDFlib;
  Page, PageCount, ImageID: Integer;
  DPI, W, H: Double;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    Lib.SetMeasurementUnits(0);   // Standard-User-Space, 1/72 Zoll
    Lib.SetOrigin(1);             // Ursprung oben links für DrawImage
    Lib.CompressImages(1);        // Flate für dekodierte Samples
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = TIFF-Seite
      if ImageID = 0 then
        Exit;                     // nicht unterstützte oder beschädigte Bilddaten
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // Punkte pro Zoll
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // Punkte pro cm
      else
        DPI := 0;
      end;
      if DPI <= 0 then
        DPI := 300;
      W := Lib.ImageWidth * 72 / DPI;
      H := Lib.ImageHeight * 72 / DPI;
      if Page > 1 then
        Lib.NewPage;
      Lib.SetPageDimensions(W, H);
      Lib.DrawImage(0, 0, W, H);
    end;
    Result := Lib.SaveToFile(PdfName) = 1;
  finally
    Lib.Free;
  end;
end;

Keine Verzweigung je Format nötig, und AddImageFromStream und AddImageFromString folgen für TIFF-Daten im Speicher denselben Regeln

Old-Style-JPEG: Tabellen an Offsets, Strips ohne Header

Seit v3.713.0 importiert PDFlibPas Old-Style-JPEG-TIFFs (Compression = 6), deren Tabellen aus den Offset-Tags JPEGQTables, JPEGDCTables und JPEGACTables oder aus einem reinen Tabellen-JPEGInterchangeFormat-Stream stammen. TIFF 6.0 Section 22 definierte jenes Layout, und TIFF Technical Note 2 ersetzte es später durch Compression = 7 und ein gemeinsames JPEGTables-Tag, aber Scanner haben die alte Form noch jahrelang geschrieben

Die Container-Seite, BigTIFF und gekachelte Layouts, behandelt der Beitrag zum Härten des TIFF-Dekodierers; hier geht es um die JPEG-Nutzlast. Jedes Tabellen-Offset-Tag zeigt auf eine rohe 64-Byte-Quantisierungstabelle oder auf eine Huffman-Tabelle aus 16 Zählern gefolgt von ihren Werten, ein Eintrag je Komponente. PDFlibPas macht daraus DQT- und DHT-Segmente in einem synthetisierten JPEGTables-Stream. Jeder Strip aus reinen entropy-codierten Daten bekommt dann einen eigenen Header: SOI, ein Frame-Marker mit der Zeilenzahl jenes Strips, DRI, wenn JPEGRestartInterval gesetzt ist, SOS, die Entropie-Daten und EOI. Die Strips lassen sich nicht zu einem Scan verkleben, denn Old-Style-JPEG setzt die DC-Vorhersage an jeder Strip-Grenze zurück

Zwei Details geraten leicht schief. Die drei Komponenten eines YCbCr-Bildes zeigen ihre Tabellen-Offsets oft auf dieselben Bytes, also dedupliziert PDFlibPas Tabellen nach Inhalt, bevor es Tabellennummern vergibt. Bleiben mehr als zwei Huffman-Tabellen einer Klasse übrig, verwendet der synthetisierte Frame SOF1 statt SOF0, denn ein Baseline-Frame darf nur zwei DC- und zwei AC-Tabellen referenzieren. Zeigt JPEGInterchangeFormat stattdessen auf einen vollständigen JPEG-Stream mit Scans, behandelt PDFlibPas jenen Stream als das ganze Bild und importiert ihn als einzelnen New-Style-Strip. Lossless-, gekacheltes und Separate-Plane-Old-Style-JPEG werden mit explizitem Fehler abgewiesen statt erraten, konvertiere sie also upstream zu New-Style-JPEG

Warum kommt ein CMYK-JPEG in einem TIFF invertiert heraus?

Ein CMYK-JPEG in einem TIFF kommt invertiert heraus, wenn der Importer die Adobe-APP14-Konvention anwendet, die zu eigenständigen JPEG-Dateien gehört. Photoshop schreibt invertiertes CMYK in Standalone-JPEGs, daher fügen PDF-Erzeuger traditionell /Decode [1 0 1 0 1 0 1 0] zum Ausgleich hinzu. TIFF PhotometricInterpretation 5 definiert Samples als Tintenmengen, wobei 0 keine Tinte bedeutet, und gängige TIFF-Reader nehmen die dekodierten JPEG-Samples, wie sie sind, ohne auf APP14 zu schauen

PDFlibPas bettet JPEG-komprimierte CMYK-TIFF-Strips ohne das invertierte Decode-Array ein, ob ein APP14-Marker da ist oder nicht. Das APP14-Transform-Flag entscheidet weiterhin, ob YCCK nach CMYK konvertiert wird, und DCTDecode erledigt diesen Schritt selbst. Standalone-JPEG-Dateien mit vier Komponenten behalten die invertierte Adobe-Konvention, also werden identische JPEG-Bytes je nach Container verschieden behandelt, wie es die beiden Formate verlangen

Warum scheitert die Lossless-JPEG-Dekodierung an einer Pascal-Shift?

Die Lossless-JPEG-Dekodierung scheitert in Pascal, weil die Prädiktoren 5 und 6 eine arithmetische Rechtsverschiebung brauchen und Pascal-shr eine logische ist. SOF3 sagt jedes Sample aus seinem linken (Ra), oberen (Rb) und oberen linken (Rc) Nachbarn voraus und kodiert nur die Differenz. Von den sieben Prädiktoren in T.81 Table H.1 ist Prädiktor 5 Ra + ((Rb - Rc) >> 1) und Prädiktor 6 Rb + ((Ra - Rc) >> 1), und der eingeklammerte Unterschied wird negativ, sobald der obere linke Nachbar der hellere ist. Gib einen negativen Integer in shr, und das Vorzeichenbit wird zum Betragsbit, das Bild löst sich in Rauschen auf

Shift-Verhalten der Lossless-JPEG-Prädiktoren in Delphi und PDFlibPas: logisches shr korrumpiert negative Werte, div 2 schneidet gegen Null ab, HalfDown rundet ab wie die Referenz
die Prädiktoren 5 und 6 brauchen Floor-Semantik, und ein kleiner Helfer passt auf Delphi und Free Pascal zum Referenzdekodierer

Die Shift durch div 2 zu ersetzen behebt es auch nicht. div schneidet gegen Null ab, während eine arithmetische Shift gegen negative Unendlichkeit rundet, und beide weichen bei jedem ungeraden negativen Wert ab. Free Pascal hat SarLongint in seiner System-Unit; Delphi hat kein Äquivalent, also verwendet PDFlibPas einen Helfer, der auf beiden identisch kompiliert:

// Arithmetische Rechtsverschiebung um eins: floor(Value / 2)
// shr ist eine logische Shift, und div 2 schneidet gegen Null ab
function HalfDown(Value: Integer): Integer;
begin
  if Value >= 0 then
    Result := Value shr 1
  else
    Result := -((1 - Value) shr 1);
end;

// Value = -3:  -3 shr 1 = 2147483646,  -3 div 2 = -1,  HalfDown(-3) = -2

Für die Typbreiten-Seite derselben Problemklasse siehe Longint- und Zeiger-Fallen auf Delphi Linux64

Der Rest des Lossless-Dekodierers hat eigene Randregeln. Die erste Zeile, und die erste Zeile nach jedem Restart-Marker, sagt nur aus dem linken Nachbarn voraus und startet bei 2^(P−Pt−1) für Präzision P und Point Transform Pt; jede spätere Zeile startet vom Sample darüber. Die Rekonstruktion läuft modulo 2^16, und der Point Transform skaliert nur die Ausgabe, nie die Prädiktionskette. Huffman-DC-Kategorie 16, eine Differenz von 32768 ohne zusätzliche Bits, ist nur im Lossless-Modus legal, deshalb muss der Tabellen-Parser sie akzeptieren, bevor er weiß, welchen Prozess der Frame deklarieren wird

PDFlibPas unterstützt alle sieben Prädiktoren, Point Transforms und Restart-Intervalle, die ganze Zeilen abdecken. Die JPEG-Präzision muss gleich BitsPerSample sein und 8 oder 16 betragen, und jede Komponente muss mit 1x1 gesampelt sein. YCbCr-Lossless-Daten und 12-Bit-Lossless-JPEG werden mit explizitem Fehler abgewiesen

12-Bit-DCT und arithmetische Kodierung in nativem Pascal

Die 12-Bit- und Arithmetic-Dekodierer in PDFlibPas teilen sich eine inverse DCT und unterscheiden sich nur in der Entropie-Dekodierung. Im 12-Bit-Huffman-JPEG erreichen DC-Kategorien 15 und AC-Kategorien 14, Quantisierungstabellen dürfen 16-Bit-Einträge tragen, und die DC-Vorhersage setzt sich bei jedem Scan und jedem Restart zurück. Koeffizienten bleiben in Zigzag-Ordnung bis zur Dequantisierung, die den Quantisierungswert am selben Zigzag-Index verwenden muss, und wandern erst danach in die natürliche Ordnung für die separierbare Two-Pass-IDCT. Dekodierte Samples landen innerhalb ±1 eines unabhängigen Fließkomma-Referenzdekodierers

Arithmetische Kodierung ersetzt Huffman durch den QM-Coder aus T.81 Anhang D: eine Wahrscheinlichkeits-Zustandstabelle mit 114 Einträgen, einen DC-Statistikbereich mit 64 Fächern und einen AC-Bereich mit 256 Fächern, gesteuert von Conditioning-Parametern aus dem DAC-Segment (Defaults L = 0, U = 1, Kx = 5). Statistik, DC-Kontext und Prädiktion setzen sich bei jedem Restart-Intervall und am Anfang jedes Scans zurück. Deshalb behält PDFlibPas DAC-Segmente, wenn es gemeinsame JPEGTables für Arithmetic-Bilder normalisiert; wirft man sie weg, dekodiert jeder Strip mit dem falschen Statistikmodell

v3.723.0 brachte subsamplete Komponenten und die YCbCr-zu-RGB-Konvertierung mit den JFIF-Gleichungen in diese dekodierten Pfade, und v3.724.0 brachte Progressive-Scans für sowohl 12-Bit-Huffman- (SOF2) als auch Arithmetic-Daten (SOF10). Subsamplete Chroma wird auf volle Auflösung repliziert statt geglättet, was ein Referenzdekodierer bei ausgeschalteter Chroma-Glättung erzeugt. Hierarchisches JPEG und arithmetisch kodiertes Lossless-JPEG (SOF11) werden mit explizitem Fehler abgewiesen

Binäre JPEG-Fixtures in Delphi testen

Handgebaute JPEG-Fixtures in Delphi-Unit-Tests können auf eine Weise scheitern, die exakt wie ein Decoder-Bug aussieht. Eine String-Konstante aus #$xx-Zeichen, die über 255 Zeichen hinausläuft, ist als UnicodeString typisiert, und ihre Zuweisung an einen AnsiString konvertiert sie über die aktive Codepage. Bytes $80 bis $9F, die in entropy-codierten Daten Routine sind, werden auf dem Weg hinein umgeschrieben. Marker-Header zur Laufzeit zu bauen hat dieselbe Falle: In Delphi ergibt AnsiChar + AnsiChar einen UnicodeString, hänge also einzelne Bytes an einen AnsiString an oder schreibe sie mit SetLength und indizierter Zuweisung. Für Fixtures: Hex-Text speichern und zur Laufzeit dekodieren:

uses
  SysUtils, PDFlibrary;

function HexToBytes(const Hex: AnsiString): AnsiString;
var
  I: Integer;
begin
  SetLength(Result, Length(Hex) div 2);
  for I := 1 to Length(Result) do
    Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;

// Das Fixture bleibt reines ASCII, keine Codepage kann es anfassen
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Beim Erzeugen von Testbildern gilt: jpegtran -arithmetic schreibt sequential SOF9; SOF10 braucht zusätzlich -progressive. Und eine Bild-ID ungleich null beweist nur, dass der Import akzeptiert wurde. Rendere die Seite und vergleiche Pixel, denn ein Durchreich-Fehler erzeugt ein wohlgeformtes PDF, das kein Viewer zeichnen kann

Alte Scan-Archive ins PDF bringen

Diese JPEG-Varianten tauchen bei einer Archivmigration gern alle auf einmal auf. PDFlibPas liest 12-Bit-, Lossless-, Arithmetic- und Old-Style-JPEG-TIFFs über denselben AddImageFromFile-Aufruf wie alles andere, hält die volle Präzision im PDF und weist die seltenen Prozesse außerhalb dieser Menge mit explizitem Fehler ab. Für Lizenzierung, die vollständige Feature-Liste und Trial-Downloads siehe die Produktseite der PDFlibPas PDF Library for Delphi