Technischer Artikel

JBIG2 Custom Huffman Tables im Pure-Pascal-PDF-Decoder

Werfen Sie ein JBIG2-Tables-Segment (Typ 53) Byte für Byte auseinander, ist die erste Überraschung, was es weglässt: die Huffman-Codes selbst. Sie bekommen ein Flags-Byte, zwei Grenzen und eine Folge kleiner Zahlen, und diese Zahlen sind Code-Längen. Encoder und Decoder bauen die tatsächlichen Bitmuster beide aus den Längen mit dem Verfahren aus ITU-T T.88 Annex B.3 wieder auf. Bauen Sie sie in leicht anderer Reihenfolge auf, erhalten Sie trotzdem einen gültigen Prefix Code, jedes Muster dekodiert weiter, und jeder Wert landet am falschen Muster

Auf einer Seite, die mit Huffman-Symbol-IDs und selbst mitgeschickten Encoder-Tables kodiert ist, sieht diese Diskrepanz aus wie ein schlechter Scan: Glyphen um einige Pixel verschoben, Zeichen aus den falschen Symbolen gebaut, oder plausibel aussehende Zahlen, bis mehrere Segmente später eine Breite aus dem Bereich läuft. Das meiste JBIG2 in PDF-Dateien ist arithmetisch kodiert, und das meiste Huffman-kodierte JBIG2 hält sich an die Standard-Tables B.1 bis B.15. Dateien mit eigenen Tables sind selten, und sie trainieren Table-Parser, Huffman-kodiertes Refinement und unkomprimierte Collective Bitmaps meist alle auf einmal

Was ein Tables-Segment tatsächlich trägt

Ein Tables-Segment beschreibt genau eine Huffman-Tabelle. T.88 §7.4.13 und Annex B.2 legen es aus als ein Flags-Byte, ein vorzeichenbehaftetes 32-Bit-HTLOW, ein vorzeichenbehaftetes 32-Bit-HTHIGH und dann (Prefix-Länge, Range-Länge)-Paare, die das Intervall von HTLOW bis HTHIGH kacheln. Bit 0 des Flags ist HTOOB, was sagt, ob die Table einen Out-of-Band-Code hat. Bits 1 bis 3 plus eins ergeben HTPS, die Breite jedes Prefix-Längen-Felds. Bits 4 bis 6 plus eins ergeben HTRS, die Breite jedes Range-Length-Felds. Bit 7 ist reserviert

An den Grenzen stolpert ein handgeschriebener Parser meist zuerst. Liest man HTLOW als vorzeichenlos, behauptet eine Table, die bei -2 beginnt — für delta-kodierte Symbol-Breiten und -Höhen völlig üblich — plötzlich, knapp unter 4,3 Milliarden zu beginnen. PDF Library for Delphi liest beide Grenzen als vorzeichenbehaftete Werte, weist eine Table zurück, deren HTLOW nicht unter HTHIGH liegt oder deren Range-Länge 32 übersteigt, und prüft jedes Feldlesen gegen das Bit, an dem die deklarierten Segmentdaten enden. Diese letzte Prüfung wiegt schwerer, als sie klingt: Eine Table, die über ihr Segment hinausliest, konsumiert den nächsten Segment-Header als Prefix-Längen und parst sich in kompletten Unsinn. Ein gesetztes Bit 7 scheitert mit reserved custom Huffman table flag. Diese Zurückweisung ziehen wir einem Ratsspiel vor, denn eine geratene Table produziert falsche Pixel, die niemand bis zur Ursache zurückverfolgen kann

Auf die regulären Lines folgen die Escape-Lines aus Annex B.2. Die untere Range-Line startet bei HTLOW minus eins und zählt mit einem 32-Bit-Offset abwärts, die obere Range-Line startet bei HTHIGH mit einer 32-Bit-Range, und die Out-of-Band-Line existiert nur, wenn HTOOB gesetzt ist. Diese Lines tragen dieselben Marker wie die eingebauten Standard-Tables, also laufen eine Custom-Table und Table B.6 durch eine Dekodier-Schleife. Eine Custom-Table kann bis zu 65.536 Lines aufnehmen

Tables-Segment-Layout hinter der JBIG2-Custom-Huffman-Dekodierung in PDFlibPas: ein Flags-Byte mit HTOOB, HTPS und HTRS plus einem zurückgewiesenen reservierten Bit, vorzeichenbehaftete HTLOW- und HTHIGH-Grenzen, eine Folge von Prefix- und Range-Length-Paaren und die Escape-Lines jbig2HuffmanLOW, eine feste 32-Bit-High-Line und optional jbig2HuffmanOOB
Jedes Feld des Segments wird durch einen bounds-geprüften Helfer gelesen, denn eine Table, die über ihr deklariertes Ende hinausliest, würde den nächsten Segment-Header als Prefix-Längen konsumieren, und die Sentinel-Escape-Lines entsprechen den eingebauten Standard-Tables

Warum müssen Prefix Codes in Table-Line-Reihenfolge zugewiesen werden?

Weil die Deklarationsreihenfolge das Einzige ist, das ein Bitmuster an einen Wertebereich bindet. Annex B.3 zählt, wie viele Lines jede Prefix-Länge benutzen, vergibt zuerst Codes der Länge 1, schiebt nach links, um zu Länge 2 zu gelangen, und so weiter; innerhalb einer Länge gehen die Codes an die Lines in der Reihenfolge ihres Auftretens im Segment. Eine kleine Table macht das konkret. Sechs Lines, in Dateireihenfolge von A bis F benannt, mit diesen Prefix-Längen:

LinePrefix-LängeCode (Dateireihenfolge gehalten)Code, wenn B und D tauschen
A20000
B3100101
C20101
D3101100
E3110110
F0ungenutztungenutzt

Beide Spalten sind gültige Prefix Codes. Nur die linke ist es, was der Encoder meinte: Mit getauschten B und D liefert das Muster 100 Ds Wertebereich, und der Decoder macht weiter mit einer Zahl aus dem falschen Intervall. Line F hat die Länge 0, was B.3 als ungenutzt definiert; geben Sie ihr einen Ein-Bit-Code, verschiebt sich jede spätere Zuweisung

Der bequeme Weg zur rechten Spalte ist ein Allzweck-Sortierlauf nach Prefix-Länge. Delphis TArray.Sort und TList<T>.Sort sind nicht stabil, gleiche Längen können also in beliebiger Reihenfolge zurückkommen. Für kanonische Codes im eigenen Werkzeug geht man die Lines stattdessen einmal pro Länge in Dateireihenfolge durch:

uses
  System.SysUtils;

type
  TCodeLine = record
    PrefixLen: Integer;   // wie aus dem Tables-Segment gelesen
    Code: Cardinal;       // unten zugewiesen
  end;

// Kanonische Zuweisung nach Annex B.3, geschrieben für Ihr eigenes Werkzeug.
// Gleiche Längen behalten ihre Dateireihenfolge; Länge 0 bekommt keinen Code.
procedure AssignCanonicalCodes(var Lines: array of TCodeLine);
var
  LenCount: array[0..32] of UInt64;
  FirstCode: array[0..32] of UInt64;
  NextCode: UInt64;
  CurLen, I: Integer;
begin
  FillChar(LenCount, SizeOf(LenCount), 0);
  for I := 0 to High(Lines) do
  begin
    if (Lines[I].PrefixLen < 0) or (Lines[I].PrefixLen > 32) then
      raise Exception.Create('prefix length out of range');
    Inc(LenCount[Lines[I].PrefixLen]);
  end;
  LenCount[0] := 0;
  FirstCode[0] := 0;
  for CurLen := 1 to 32 do
  begin
    FirstCode[CurLen] := (FirstCode[CurLen - 1] + LenCount[CurLen - 1]) shl 1;
    // Kraft-Prüfung: mehr Codes, als diese Länge fassen kann
    if FirstCode[CurLen] + LenCount[CurLen] > (UInt64(1) shl CurLen) then
      raise Exception.Create('oversubscribed prefix lengths');
    NextCode := FirstCode[CurLen];
    for I := 0 to High(Lines) do       // Dateireihenfolge ist der Tie-Break
      if Lines[I].PrefixLen = CurLen then
      begin
        Lines[I].Code := Cardinal(NextCode);
        Inc(NextCode);
      end;
  end;
end;

Füttern Sie ihn mit den sechs Lines oben und A bekommt 00, C bekommt 01, B bekommt 100, D bekommt 101 und E bekommt 110; F behält keinen Code. PDF Library for Delphi erledigt dieselbe Aufgabe mit einem Zähl-Durchlauf, der konstruktionsbedingt stabil ist und das 32-malige Neudurchlaufen der Table vermeidet, und zieht dieselben beiden Schranken an. Übersubskribierte Längen scheitern mit oversubscribed Huffman prefix codes; lässt man sie durch, kann ein Muster auf zwei Lines passen. Prefixes länger als 32 Bits scheitern ebenfalls namentlich, obwohl T.88 sie auf dem Papier erlaubt. Wir halten diese Deckelung für den richtigen Trade: Jeder Code passt in ein Maschinenwort, und eine Datei, die mehr braucht, ist weit eher beschädigt als schlau

Werte brauchen dieselbe Sorgfalt. Die untere Range-Line dekodiert als HTLOW minus eins minus eines 32-Bit-Offsets ohne Vorzeichen, und mit einem 32-Bit-Integer als Zwischenergebnis kann diese Subtraktion in eine gewöhnlich aussehende positive Breite umlaufen. Der Decoder rechnet in 64 Bits und wirft Huffman value exceeds signed integer range, wenn das wahre Ergebnis nicht hineinpasst

Kanonische Prefix-Code-Zuweisung im PDFlibPas-JBIG2-Decoder: Im Segment kommen allein Prefix-Längen an, ein stabiles Counting Sort über Counts, Starts und Positions hält die Deklarationsreihenfolge innerhalb jeder Länge, Codes der Länge eins werden zuerst vergeben und der Code schiebt pro Länge nach links, während die Kraft-Prüfung eine Übersubskribierung zurückweist
Der Encoder schreibt die Bitmuster nie, also baut jede Abweichung von der Table-Line-Reihenfolge stillschweigend einen anderen, aber gültigen Prefix Code, und die Ausgabe sieht plausibel aus; Lines der Länge null fallen als ungenutzt heraus, und Prefixes länger als 32 Bits werden zurückgewiesen

Selektor-Reihenfolge und zwei Fallen, die einander decken

Eine Text Region oder ein Symbol-Dictionary benennt seine Tables nicht. Es trägt Zwei-Bit-Selektoren, und jeder auf Custom gesetzte Selektor nimmt das nächste Tables-Segment aus der Referral-Liste, in Referral-Reihenfolge. T.88 §7.4.3.1.2 ordnet die Text-Region-Selektoren als FS, DS, DT, RDW, RDH, RDX, RDY und RSIZE; §7.4.2.1.1 ordnet die Symbol-Dictionary-Selektoren als DH, DW, BMSIZE und AGGINST. Die Werte 0 und 1 wählen Standard-Tables, 3 wählt eine Custom-Table, und 2 ist auf Feldern mit nur zwei Standard-Tables reserviert. PDF Library for Delphi weist reservierte Selektorwerte zurück und scheitert mit missing custom Huffman table reference, wenn die Selektoren mehr Custom-Tables wollen, als die Region referenziert

Wer seinen eigenen Decoder schreibt, macht hier leicht zwei Fehler, und jeder versteckt den anderen. Der erste beißt in Delphi, oder FPC im Delphi-Modus (der Objfpc-Modus weist den Code als doppelten Bezeichner zurück): ein Flags-Setter, dessen Parameter denselben Namen trägt wie das Feld. Der Parameter verschattet das Feld, Flags := Flags weist es sich selbst zu, und das Feld bleibt null. Jeder Selektor liest dann null, eine Text Region benutzt die Standard-Tables B.6, B.8 und B.11 für FS, DS und DT, was auch immer die Datei verlangte, und der Custom-Pfad läuft nie

Der zweite sitzt im Symbol-Dictionary. Ein Huffman-Dictionary kann seine Symbole als unkomprimierte Collective Bitmap ablegen (BMSIZE null), wobei jede Zeile in einem Teilbyte endet. Der Padding-Wert ist die Anzahl gültiger Bits in diesem Byte, und es verführt, ihn als die niedrigste gültige Bit-Position zu lesen:

// Letztes Byte einer Collective-Bitmap-Zeile, Bits MSB-zuerst gelesen.
// Padding := TotalWidth mod 8  -> Anzahl der GÜLTIGEN Bits in diesem Byte

// Falsch: behandelt Padding als den niedrigsten zu kopierenden Bit-Index
for BitPos := 7 downto Padding do
  CopyPixel((LastByte shr BitPos) and 1);

// Richtig: der niedrigste gültige Bit-Index ist 8 - Padding (0 bei Padding = 0)
for BitPos := 7 downto ((8 - Padding) and 7) do
  CopyPixel((LastByte shr BitPos) and 1);

// CopyPixel steht für das, was in Ihrem eigenen Decoder das Pixel schreibt

Prüfen Sie die Grenzbreiten. Eine 63-Pixel-Zeile hat ein Padding von 7: Die falsche Schleife kopiert ein Bit des letzten Bytes, die richtige sieben. Eine 9-Pixel-Zeile hat ein Padding von 1: Die falsche Schleife kopiert sieben, die richtige eins. Bei 64 Pixeln kopieren beide alle acht, Vielfache von 8 legen den Fehler also nie offen. Synthetische Collective Bitmaps mit 7 und 9 Pixeln Breite nageln beide Seiten der Byte-Grenze fest. Und der Schaden wandert weiter, denn das Refinement benutzt diese Symbole als Referenzen

Hat Ihr eigener Decoder beide Fallen, wird das Debuggen elend. Korrigiert man nur den Setter, läuft der Custom-Pfad endlich — direkt in kaputte Collective Bitmaps, die Seite sieht also weiterhin falsch aus. Korrigiert man nur das Padding, ändert sich sichtbar nichts, weil der Custom-Pfad weiter unerreichbar ist. Testen Sie beides mit synthetischen Eingaben, bevor Sie einer der beiden Änderungen vertrauen. Ein verwandtes Detail: Ein Huffman-Symbol-Dictionary, dessen Eingabe- und neue Symbole sich zu eins aufaddieren, bekommt aus der log2-Formel eine Symbol-Code-Länge von 0, doch die Huffman-Variante schreibt jede Symbol-ID mit mindestens einem Bit. PDF Library for Delphi hebt diese Länge im Huffman-Modus auf 1 an

Wo liest das Huffman-Refinement seine Bitmap-Größe?

Aus einem BMSIZE-Feld, das nur im Huffman-Modus existiert und vor den arithmetischen Refinement-Daten dekodiert wird. Hat eine Text-Region-Instanz Refinement (RI ungleich null) und SBHUFF ist gesetzt, liest der Decoder nach T.88 §6.4.11 RDW, RDH, RDX und RDY mit ihren gewählten Tables, dann BMSIZE mit der RSIZE-Table, dann springt er zu einer Byte-Grenze, und erst danach läuft die generische Refinement-Dekodierung über genau BMSIZE Bytes. Text Regions im arithmetischen Modus haben kein solches Feld, also überspringt ein Decoder, der einen Codepfad zwischen beiden Modi teilt, es, startet den arithmetischen Decoder zu früh und verfeinert jedes Symbol gegen Rauschen. Der Symbol-Dictionary-Pfad mit REFAGG und einer einzelnen Refinement-Instanz (§6.5.8.2.2) hat dasselbe Feld

PDF Library for Delphi begrenzt BMSIZE auf beiden Seiten. Das Minimum ist 2, das initiale Byte-Paar, das der arithmetische Decoder immer konsumiert. Das Maximum ist, was vom aktuellen Segment übrig bleibt, bewusst nicht vom ganzen Stream: Ein BMSIZE, das nur passt, wenn es Bytes vom nächsten Segment borgt, ist missgestaltet, und eine Stream-Längenprüfung würde den arithmetischen Decoder in den folgenden Header hineinlesen lassen. Außerhalb des Bereichs liegende Größen scheitern mit invalid refinement bitmap size. Danach setzt der Huffman-Reader exakt BMSIZE Bytes hinter dem Feld wieder an, was auch immer der arithmetische Decoder vorausgelesen hat

Refinement-Grenzen im Huffman-Modus im PDFlibPas-JBIG2-Decoder: RDW, RDH, RDX und RDY dekodieren aus ihren Tables, BMSIZE dekodiert aus der RSIZE-Table und ist byte-ausgerichtet, dann verfeinert der arithmetische Decoder exakt die BMSIZE Bytes zwischen RefinementEnd und SegmentEnd und weist Größen unter zwei oder über die Segmentgrenze hinaus zurück
Die Untergrenze von zwei Bytes spiegelt das initiale Paar wider, das der arithmetische Decoder immer konsumiert, die Obergrenze ist das aktuelle Segment statt des ganzen Streams, und nach dem Refinement springt der Reader auf RefinementEnd, egal wie weit vorausgelesen wurde

Die deklarierte Segmentlänge ist die Grenze

Jeder Segment-Header gibt an, wie viele Datenbytes folgen, und der Decoder hält das Segment in beide Richtungen an diese Zahl: Über das Ende hinaus zu lesen ist ein Fehler, früh fertig zu werden ist normal, und der nächste Header wird immer von der Stelle gelesen, an der die Datei sagt, dass er beginnt. Kleinere Regeln folgen. Die unbekannte Datenlänge aus T.88 §7.2.7 wird vorne abgewiesen, statt nach einem Terminator zu scannen. Referenzierte Segmente müssen kleinere Nummern tragen als das aktuelle und müssen bereits existieren. END_OF_PAGE und END_OF_FILE müssen null Datenbytes deklarieren. Ein Profiles-Segment (Typ 52) muss exakt 4 plus 4 mal seine Bezeichneranzahl sein; es wird dann übersprungen, aber behalten, spätere Segmente können es also weiterhin referenzieren, und ein unbekannter Profil-Bezeichner stoppt die Dekodierung nicht

Der klassische Fall für frühes Fertigsein ist MMR. Ein MMR-Decoder ist fertig, sobald er das letzte Pixel der letzten Zeile produziert, was vor dem EOFB-Terminator passieren kann, den T.88 §6.2.5.7 ans Datenende setzt. Ein Reader, der annimmt, jetzt am nächsten Header zu sitzen, parst die verbliebenen EOFB-Bytes als Segmentnummer und scheitert einige Bytes später, weit weg von der wahren Ursache. Der Sprung zum deklarierten Ende vermeidet die Frage. Dieselbe Gewohnheit zieht sich durch das Parsen nicht vertrauenswürdiger PDF-Strukturen bei PDF Library for Delphi im Allgemeinen

Einen JBIG2-Bild-Stream aus Delphi dekodieren

Der Decoder wohnt in der Unit PDFlibJBIG2 als TPLJBIG2Decoder. LoadFromByteArray nimmt die eingebetteten JBIG2-Daten eines /JBIG2Decode-Bilds und optional die Daten seines /JBIG2Globals-Streams, in dem Custom-Tables und geteilte Symbol-Dictionarys üblicherweise wohnen. Er liefert False bei jeder Zurückweisung und hinterlässt den Grund in LastError; die Ausgabe eines früheren Aufrufs wird zu Beginn jedes Aufrufs geleert, ein gescheitertes Dekodieren hinterlässt also nie alte Pixel, die der Aufrufer für ein Ergebnis halten könnte

uses
  System.SysUtils, PDFlibJBIG2;

// ImageBytes: die Bild-Stream-Daten, von denen alle Filter vor
//   JBIG2Decode bereits entfernt sind
// GlobalBytes: die /JBIG2Globals-Stream-Daten oder ein leeres Array
function DecodeAndCountBlack(const ImageBytes, GlobalBytes: TJBIG2ByteArray;
  out BlackPixels: Int64): Boolean;
var
  Decoder: TPLJBIG2Decoder;
  Line: TJBIG2ByteArray;
  Row, Col: Integer;
begin
  BlackPixels := 0;
  Decoder := TPLJBIG2Decoder.Create;
  try
    Result := Decoder.LoadFromByteArray(ImageBytes, GlobalBytes);
    if not Result then
    begin
      // Format: 'JBIG2 segment <n> (type <t>) at byte <offset>: <reason>'
      Writeln('JBIG2 refused: ', Decoder.LastError);
      Exit;
    end;
    for Row := 0 to Decoder.Height - 1 do
    begin
      Decoder.GetScanline(Row, Line);   // ein Byte pro Pixel, 0 oder 1
      for Col := 0 to Decoder.Width - 1 do
        Inc(BlackPixels, Line[Col]);    // 1 = schwarz in JBIG2
    end;
  finally
    Decoder.Free;
  end;
end;

Die Schwarzpixel-Zählung ist Ihre eigene Prüfung, ein billiger Fingerabdruck zum Vergleichen gegen einen zweiten Renderer. Fehler auf Segment-Ebene kommen mit Segmentnummer, Typ und Byte-Offset vorangestellt an, ein (type 53) vor oversubscribed Huffman prefix codes zeigt also direkt auf ein Tables-Segment. Halten Sie die beiden Fehlerkanäle auseinander: TPLJBIG2Decoder.LastError ist der Diagnosetext dieses Decoders, während TPDFlib.LastErrorCode der numerische Code der API auf Bibliotheksebene ist. Die Seitenwahl folgt dem Stream: Der Decoder liefert die Bitmap des ersten Page-Information-Segments, auf das er trifft, und schlägt Seiten-Assoziation 0 nicht nach, denn in PDF eingebettetes JBIG2 trägt eine Seite, und Encoder nummerieren sie routinemäßig 1

Renderer an diesen Dateien zu vergleichen verlangt etwas Demut, denn unabhängige JBIG2-Decoder sind sich bei Custom-Table-Dateien mit Huffman-Refinement nicht immer einig. Zwei sich widersprechende Decoder entscheiden nichts. Die Formatfelder schon: welche Tables die Selektoren wählten, was BMSIZE deklarierte, wo jedes Segment endet

Das Kodieren deckt der Artikel zu JBIG2-Encoder-Backends und ihrer Anbindung ab. Der Lesepfad muss nehmen, was der Encoder eines anderen ausgeworfen hat, und teilt seine Regeln mit dem eingebauten TIFF-Decoder und seinen BigTIFF- und Tiled-Layout-Prüfungen: namentlich zurückweisen, nie Bytes über eine deklarierte Grenze hinweg borgen, arithmetische Zwischenwerte breit halten. Wenn ein Ordner gescannter PDFs andere Toolkits ständig stolpern lässt, prüfen Sie, ob die Bilder Huffman-kodiertes JBIG2 mit Custom-Tables sind, und legen Sie sie dann gegen den nativen Delphi- und C++Builder-Lesepfad, beschrieben auf der PDF Library for Delphi-Produktseite