Technisch artikel

Eigen JBIG2 Huffman-tabellen in een pure Pascal PDF-decoder

Dump een JBIG2 Tables-segment (type 53) byte voor byte en de eerste verrassing is wat het achterwege laat: de Huffman-codes zelf. Je krijgt een flags-byte, twee grenzen en een reeks kleine getallen, en die getallen zijn codelengtes. Encoder en decoder bouwen allebei de echte bitpatronen op uit de lengtes met de procedure uit ITU-T T.88 Annex B.3. Bouw ze in een net iets andere volgorde op en je krijgt nog steeds een geldige prefixcode, elk patroon decodeert nog steeds, en elke waarde komt uit aan het verkeerde patroon vastgeplakt

Op een pagina die met Huffman-symbool-ID's en door de encoder meegeleverde tabellen is gecodeerd, ziet die mismatch eruit als een slechte scan: glyphs een paar pixels verschoven, tekens opgebouwd uit de verkeerde symbolen, of plausibele getallen tot een width enkele segmenten later uit bereik raakt. Het meeste JBIG2 in PDF-bestanden is aritmetisch gecodeerd, en het meeste Huffman-gecodeerde JBIG2 houdt zich aan de standaardtabellen B.1 tot B.15. Bestanden die hun eigen tabellen meesturen zijn zeldzaam, en ze tenderen ernaar de tabelparser, Huffman-gecodeerde refinement en ongecomprimeerde collective bitmaps allemaal tegelijk uit te proberen

Wat een Tables-segment werkelijk bevat

Een Tables-segment beschrijft exact één Huffman-tabel. T.88 §7.4.13 en Annex B.2 leggen hem uit als één flags-byte, een signed 32-bit HTLOW, een signed 32-bit HTHIGH, en daarna (prefixlengte, rangelengte)-paren die het interval van HTLOW tot HTHIGH tegelen. Bit 0 van de flags is HTOOB, die zegt of de tabel een out-of-band-code heeft. Bits 1 tot 3 plus één geven HTPS, de breedte van elk prefixlengteveld. Bits 4 tot 6 plus één geven HTRS, de breedte van elk rangelengteveld. Bit 7 is gereserveerd

De grenzen zijn waar een handgeschreven parser meestal als eerste struikelt. Lees HTLOW als unsigned en een tabel die op -2 begint, wat voor delta-gecodeerde symboolbreedtes en -hoogtes gebruikelijk is, beweert opeens net onder de 4,3 miljard te beginnen. PDF Library for Delphi leest beide grenzen als signed waarden, weigert een tabel waarvan HTLOW niet onder HTHIGH ligt of waarvan de rangelengte boven de 32 uitkomt, en controleert elke veldlezing op de bit waar de gedeclareerde data van het segment eindigt. Die laatste controle is belangrijker dan hij klinkt: een tabel die voorbij zijn segment leest, consumeert de volgende segmentheader als prefixlengtes en parst tot complete onzin. Een gezette bit 7 faalt met reserved custom Huffman table flag. Die weigering verkiezen we boven een gok, want een gegokte tabel produceert verkeerde pixels die niemand naar zijn oorzaak kan traceren

Na de gewone regels komen de escaperegels van Annex B.2. De low-range-regel begint bij HTLOW min één en telt af met een 32-bit offset, de high-range-regel begint bij HTHIGH met een 32-bit range, en de out-of-band-regel bestaat alleen als HTOOB gezet is. Deze regels dragen dezelfde markeringen die de ingebouwde standaardtabellen gebruiken, dus een eigen tabel en Tabel B.6 gaan door één decodeerloop. Een eigen tabel kan tot 65.536 regels bevatten

Lay-out van het Tables-segment achter JBIG2-decodering met eigen Huffman-tabellen in PDFlibPas: één flags-byte met HTOOB, HTPS en HTRS plus een gereserveerd bit dat wordt geweigerd, signed HTLOW- en HTHIGH-grenzen, een reeks paren van prefix- en bereiklengtes, en de escape-regels jbig2HuffmanLOW, een vaste 32-bits hoge regel en optioneel jbig2HuffmanOOB
Elk veld van het segment wordt via een bounds-checked helper gelezen, want een tabel die voorbij zijn gedeclareerde einde leest zou de volgende segmentheader als prefixlengtes consumeren, en de sentinel-escape-regels komen overeen met de ingebouwde standaardtabellen

Waarom moeten prefixcodes in de volgorde van de tabelregels worden toegekend?

Want de declaratievolgorde is het enige dat een bitpatroon aan een waardebereik koppelt. Annex B.3 telt hoeveel regels elke prefixlengte gebruiken, deelt eerst codes van lengte 1 uit, schuift naar links om bij lengte 2 te komen, enzovoort; binnen één lengte gaan codes naar regels in de volgorde waarin ze in het segment staan. Een klein tabel maakt dit concreet. Zes regels, A tot F gelabeld in bestandsvolgorde, met deze prefixlengtes:

RegelPrefixlengteCode (bestandsvolgorde aangehouden)Code als B en D wisselen
A20000
B3100101
C20101
D3101100
E3110110
F0ongebruiktongebruikt

Beide kolommen zijn geldige prefixcodes. Alleen de linker is wat de encoder bedoelde: met B en D gewisseld levert het patroon 100 het waardebereik van D op en gaat de decoder verder met een getal uit het verkeerde interval. Regel F heeft lengte 0, wat B.3 als ongebruikt definieert; geef hem een code van één bit en elke latere toewijzing schuift op

De makkelijke manier om de rechterkolom te krijgen is een algemene sortering op prefixlengte. De TArray.Sort en TList<T>.Sort van Delphi zijn niet stabiel, dus gelijke lengtes kunnen in willekeurige volgorde terugkomen. Voor canonieke codes in je eigen gereedschap loop je de regels liever één keer per lengte in bestandsvolgorde na:

uses
  System.SysUtils;

type
  TCodeLine = record
    PrefixLen: Integer;   // zoals gelezen uit het Tables-segment
    Code: Cardinal;       // hieronder toegekend
  end;

// Canonieke toewijzing volgens Annex B.3, geschreven voor je eigen gereedschap.
// Gelijke lengtes houden hun bestandsvolgorde; lengte 0 krijgt geen 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-check: meer codes dan deze lengte kan bevatten
    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       // bestandsvolgorde is de tie-break
      if Lines[I].PrefixLen = CurLen then
      begin
        Lines[I].Code := Cardinal(NextCode);
        Inc(NextCode);
      end;
  end;
end;

Voer hem de zes regels hierboven en A krijgt 00, C krijgt 01, B krijgt 100, D krijgt 101 en E krijgt 110; F houdt geen code over. PDF Library for Delphi doet hetzelfde werk met een telpassage, die uit constructie stabiel is en voorkomt dat de tabel 32 keer opnieuw wordt doorgescand, en past dezelfde twee vangrails toe. Oversubscribed lengtes falen met oversubscribed Huffman prefix codes; laat ze door en één patroon kan op twee regels passen. Prefixes langer dan 32 bits falen ook bij naam, al staat T.88 ze op papier toe. Dat plafond is naar onze mening de juiste afweging: elke code past in één machine-woord, en een bestand dat meer nodig heeft is veel waarschijnlijker beschadigd dan slim

Waarden vragen dezelfde zorg. De low-range-regel decodeert als HTLOW min één min een 32-bit unsigned offset, en met een tussenstap in 32-bit Integer kan die subtractie omslaan in een onschuldig ogende positieve breedte. De decoder doet het in 64 bits en gooit Huffman value exceeds signed integer range als het echte resultaat niet past

Toekenning van canonieke prefixcodes in de JBIG2-decoder van PDFlibPas: alleen prefixlengtes komen in het segment aan, een stabiele counting sort over Counts, Starts en Positions houdt de declaratievolgorde binnen elke lengte aan, codes van lengte één worden eerst uitgedeeld en de code schuift per lengte naar links, terwijl oversubscription door de Kraft-check wordt geweigerd
De encoder schrijft de bitpatronen nooit weg, dus elke afwijking van de tabelregelvolgorde bouwt stilzwijgend een andere maar geldige prefixcode en ziet de output er aannemelijk uit; regels met lengte nul vallen af als ongebruikt en prefixes langer dan 32 bits worden geweigerd

Selectorvolgorde, en twee valkuilen die elkaar dekken

Een tekstregio of symbol dictionary noemt zijn tabellen niet. Hij draagt twee-bit-selectors, en elke selector die op custom staat pakt het volgende Tables-segment uit de referred-to-lijst, in referral-volgorde. T.88 §7.4.3.1.2 ordent tekstregio-selectors als FS, DS, DT, RDW, RDH, RDX, RDY en RSIZE; §7.4.2.1.1 ordent symbol dictionary-selectors als DH, DW, BMSIZE en AGGINST. Waarden 0 en 1 pakken standaardtabellen, 3 pakt een eigen tabel, en 2 is gereserveerd op velden met slechts twee standaardtabellen. PDF Library for Delphi weigert gereserveerde selectorwaarden, en faalt met missing custom Huffman table reference als de selectors meer eigen tabellen willen dan de regio aanwijst

Schrijf je zelf een decoder, dan zijn hier twee fouten makkelijk te maken, en elk verbergt de ander. De eerste bijt in Delphi, of FPC in Delphi-modus (objfpc-modus weigert de code als dubbele identifier): een flags-setter waarvan de parameter dezelfde naam draagt als het veld. De parameter schaduwt het veld, Flags := Flags wijst het aan zichzelf toe, en het veld blijft nul. Elke selector leest dan nul, een tekstregio gebruikt standaardtabellen B.6, B.8 en B.11 voor FS, DS en DT wat het bestand ook vroeg, en het eigen pad draait nooit

De tweede zit in de symbol dictionary. Een Huffman-dictionary kan zijn symbolen opslaan als ongecomprimeerde collective bitmap (BMSIZE nul), waarbij elke rij eindigt in een partiele byte. De padding-waarde is het aantal geldige bits in die byte, en het is verleidelijk om hem als de laagste geldige bitpositie te lezen:

// Laatste byte van één collective-bitmap-rij, bits gelezen MSB eerst.
// Padding := TotalWidth mod 8  -> aantal GELDIGE bits in deze byte

// Fout: behandelt Padding als de laagste bitindex om te kopiëren
for BitPos := 7 downto Padding do
  CopyPixel((LastByte shr BitPos) and 1);

// Goed: de laagste geldige bitindex is 8 - Padding (0 als Padding = 0)
for BitPos := 7 downto ((8 - Padding) and 7) do
  CopyPixel((LastByte shr BitPos) and 1);

// CopyPixel staat voor wat de pixel ook schrijft in je eigen decoder

Controleer de grensbreedtes. Een rij van 63 pixels heeft een padding van 7: de verkeerde loop kopieert één bit van de laatste byte, de goede zeven. Een rij van 9 pixels heeft een padding van 1: de verkeerde loop kopieert er zeven, de goede één. Op 64 pixels kopieert allebei alle acht, dus veelvouden van 8 leggen hem nooit bloot. Synthetische collective bitmaps van 7 en 9 pixels breed spelden beide kanten van de bytegrens vast. En de schade reist mee, want refinement gebruikt deze symbolen als referenties

Heeft je eigen decoder beide valkuilen, dan wordt debuggen een treurige aangelegenheid. Corrigeer alleen de setter en het eigen pad draait eindelijk, recht de gebroken collective bitmaps in, dus de pagina ziet er nog steeds verkeerd uit. Corrigeer alleen de padding en er verandert niets zichtbaars, want het eigen pad blijft onbereikbaar. Test allebei met synthetische invoer voordat je een van de twee wijzigingen vertrouwt. Eén verwant detail: een Huffman-symbool dictionary waarvan input en nieuwe symbolen samen één opleveren krijgt uit de log2-formule een symbol code length van 0, terwijl de Huffman-variant elk symbool-ID met ten minste één bit schrijft. PDF Library for Delphi verhoogt die lengte in Huffman-modus naar 1

Waar leest Huffman-refinement zijn bitmapgrootte?

Uit een BMSIZE-veld dat alleen in Huffman-modus bestaat, gedecodeerd vóór de aritmetische refinementdata. Als een tekstregio-instantie refinement heeft (RI niet nul) en SBHUFF gezet is, laat T.88 §6.4.11 de decoder RDW, RDH, RDX en RDY met hun geselecteerde tabellen lezen, dan BMSIZE met de RSIZE-tabel, dan naar een bytegrens springen, en pas daarna generieke refinement-decodering draaien over precies BMSIZE bytes. Tekstregio's in aritmetische modus hebben zo'n veld niet, dus een decoder die één codepad tussen de modussen deelt slaat het over, start de aritmetische decoder vroeg, en verfijnt elk symbool tegen ruis. Het pad van de symbol dictionary met REFAGG en één refinement-instantie (§6.5.8.2.2) heeft hetzelfde veld

PDF Library for Delphi begrenst BMSIZE aan beide kanten. Het minimum is 2, het eerste bytepaar dat de aritmetische decoder altijd verbruikt. Het maximum is wat van het huidige segment resteert, bewust niet van de hele stream: een BMSIZE dat alleen past door bytes uit het volgende segment te lenen is misvormd, en een stream-lengtecontrole zou de aritmetische decoder in de volgende header laten lezen. Out-of-range-groottes falen met invalid refinement bitmap size. Daarna hervat de Huffman-reader exact BMSIZE bytes voorbij het veld, wat de aritmetische decoder ook alvast had gelezen

Grenzen van refinement in Huffman-modus in de JBIG2-decoder van PDFlibPas: RDW, RDH, RDX en RDY decoderen uit hun tabellen, BMSIZE decodeert uit de RSIZE-tabel en wordt op een bytegrens uitgelijnd, waarna de aritmetische decoder precies BMSIZE bytes verfijnt tussen RefinementEnd en SegmentEnd, waarbij groottes onder twee of voorbij de segmentgrens worden geweigerd
De ondergrens van twee bytes weerspiegelt het eerste paar dat de aritmetische decoder altijd verbruikt, de bovengrens is het huidige segment en niet de hele stream, en na de refinement springt de reader naar RefinementEnd ongeacht het vooruit lezen

De gedeclarede segmentlengte is de grens

Elke segmentheader zegt hoeveel databytes volgen, en de decoder houdt het segment in beide richtingen aan dat getal: voorbij het einde lezen is een fout, vroeg klaar zijn is normaal, en de volgende header wordt altijd gelezen op de plek waar het bestand zegt dat hij begint. Kleinere regels volgen. De unknown data length van T.88 §7.2.7 wordt vooraf geweigerd in plaats van naar een terminator te zoeken. Referred-to-segmenten moeten kleinere nummers dragen dan het huidige en moeten al bestaan. END_OF_PAGE en END_OF_FILE moeten nul databytes declareren. Een Profiles-segment (type 52) moet exact 4 plus 4 keer zijn identifier-aantal groot zijn; hij wordt dan overgeslagen maar bewaard, zodat latere segmenten hem nog kunnen aanwijzen, en een onbekende profile-identifier stopt de decode niet

Het klassieke geval van vroeg klaar zijn is MMR. Een MMR-decoder is klaar zodra hij de laatste pixel van de laatste rij produceert, wat vóór de EOFB-terminator kan gebeuren die T.88 §6.2.5.7 aan het einde van de data zet. Een reader die aanneemt dat hij nu op de volgende header zit, parst de overgebleven EOFB-bytes als segmentnummer en faalt een paar bytes later, ver van de echte oorzaak. Naar het gedeclarede einde springen omzeilt de vraag. Dezelfde gewoonte loopt door in hoe PDF Library for Delphi in het algemeen niet-vertrouwde PDF-structuren parset

Een JBIG2-beeldstream vanuit Delphi decoderen

De decoder woont in de unit PDFlibJBIG2 als TPLJBIG2Decoder. LoadFromByteArray neemt de ingebedde JBIG2-data van een /JBIG2Decode-afbeelding en optioneel de data van zijn /JBIG2Globals-stream, wat doorgaans de plek is waar eigen tabellen en gedeelde symbol dictionaries wonen. Hij geeft False terug bij elke weigering en laat de reden in LastError achter; uitvoer van een eerdere aanroep wordt bij elke aanroep eerst gewist, dus een mislukte decode laat nooit oude pixels achter die de aanroeper voor een resultaat kan aanzien

uses
  System.SysUtils, PDFlibJBIG2;

// ImageBytes: de data van de beeldstream, met eventuele filters vóór
//   JBIG2Decode al verwijderd
// GlobalBytes: de data van de /JBIG2Globals-stream, of een lege 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
      // Vorm: '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);   // één byte per pixel, 0 of 1
      for Col := 0 to Decoder.Width - 1 do
        Inc(BlackPixels, Line[Col]);    // 1 = zwart in JBIG2
    end;
  finally
    Decoder.Free;
  end;
end;

De telling van zwarte pixels is je eigen controle, een goedkope vingerprint om tegen een tweede renderer te vergelijken. Falingen op segmentniveau komen binnen met segmentnummer, type en byte-offset als prefix, dus (type 53) vóór oversubscribed Huffman prefix codes wijst rechtstreeks naar een Tables-segment. Houd de twee foutkanalen uit elkaar: TPLJBIG2Decoder.LastError is de diagnostische tekst van deze decoder, terwijl TPDFlib.LastErrorCode de numerieke code van de API op bibliothecasniveau is. Paginaselectie volgt de stream: de decoder geeft de bitmap van het eerste page-information-segment dat hij tegenkomt terug en zoekt paginakoppeling 0 niet op, want JBIG2 in PDF draagt één pagina en encoders nummeren hem routinematig 1

Renderers op deze bestanden vergelijken vraagt enige bescheidenheid, want onafhankelijke JBIG2-decoders zijn het op bestanden met eigen tabellen en Huffman-refinement niet altijd met elkaar eens. Twee decoders die het oneens zijn bewijzen niets. De formaatvelden wel: welke tabellen de selectors kozen, wat BMSIZE declareerde, waar elk segment eindigt

Encoding komt aan bod in het artikel over JBIG2-encoder-backends en hoe ze worden gelinkt. Het leespad moet nemen wat de encoder van een ander ook uitspuugt, en deelt zijn regels met de ingebouwde TIFF-decoder en zijn BigTIFF- en tiled-layout-controles: weiger bij naam, leen nooit bytes voorbij een gedeclareerde grens, houd tussenrekenen breed. Als een map vol gescande PDF's andere toolkits telkens laat struikelen, controleer dan of de afbeeldingen Huffman-gecodeerd JBIG2 met eigen tabellen zijn en probeer ze tegen het native Delphi- en C++Builder-leespad dat op de productpagina van PDF Library for Delphi wordt beschreven