Technisch artikel

PDF Predictor en LZWDecode Decodering in Delphi met HotPDF

Een stream gemarkeerd met /Predictor 12 betekent niet dat elke rij PNG-filter 2 gebruikt. HotPDF, het native VCL PDF-component voor Delphi en C++Builder, behandelt predictor-waarden 10 tot en met 15 als één familie: de echte filter-tag, 0 tot 4, is de eerste byte van elke gecodeerde rij, en HPDFDecodePredictor leest en valideert die tag rij voor rij. Dat onderscheid is de vorm van bijna elke bug in deze hoek van PDF, want niets slaat alarm wanneer je het fout doet. De filterketen draait, de raster heeft de grootte die je verwachtte, en de afbeelding komt tevoorschijn als diagonale ruis of een gradiënt die met elke scanlijn verder afdrijft. De vijf getallen in /DecodeParms (ISO 32000-1 §7.4.4) veranderen meestal de betekenis van de bytes in plaats van hun lengte, dus produceert een verkeerde waarde plausibele rommel in plaats van een fout

Waarom betekent /Predictor 12 niet PNG-filter 2 op elke rij?

Omdat het predictor-nummer alleen zegt "PNG-predictie is in gebruik", niet welk filter. PNG-encoders kiezen een filter per scanlijn en het PDF-filter erft dat, dus decoderen predictor-waarden 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) en 15 (Optimum) allemaal identiek: de leidende tag-byte van elke rij is wat de decoder moet respecteren. Het layoutgevolg telt net zo zwaar als de semantiek. Elke gecodeerde rij is 1 + RowBytes bytes lang, de input overschrijdt de output dus met precies het aantal rijen, en een stream waarvan de lengte geen geheel veelvoud is van RowBytes + 1 is per definitie afgekapt. HotPDF controleert die grens voordat er een byte aangeraakt wordt, wijst elke tag boven 4 af met Invalid PNG predictor row tag, en leest de vorige rij rechtstreeks uit de enkele outputbuffer in plaats van een tweedimensionale rij-array te materialiseren. Filters 1 en 3 reiken BytesPerPixel terug binnen de huidige rij, filter 2 leest recht omhoog, filter 4 draait de Paeth-keuze over links, boven en linksboven — en alle vier werken op reeds gereconstrueerde output, wat is waarom de boven-rij de gedecodeerde rij moet zijn en nooit de gefilterde input

uses
  HPDFPredictor;

var
  Filtered, Raster: AnsiString;
  ErrorText: string;
begin
  // /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
  if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
       Int64(1024) * 3 * 8192, Raster, ErrorText) then
    ConsumeRaster(Raster)
  else
    LogStreamDefect('predictor', ErrorText);   // no exception, no partial raster
end;

Het argument MaxOutputBytes is geen versiering. Een predictor-stap is een vermomde decompressiestap, en een vijandige of gewoon kapotte /Columns-waarde verandert een paar kilobytes input in een aanvraag voor een allocatie van meerdere gigabytes. HotPDF berekent bits per rij, bytes per rij en totale rastergrootte eerst in Int64, weigert geometrie die overloopt, en respecteert het door de aanroeper opgegeven plafond. Geef een echte grens door afgeleid van het image-dictionary, en de faalmodus wordt een gelogd bericht in plaats van een out-of-memory-dialoog op een klantmachine

Waarom corrumpeert TIFF Predictor 2 afbeeldingen van 4 bits?

Omdat Predictor 2 horizontale differencing is per sample, niet per byte, en bij 1, 2 of 4 bits per component delen meerdere samples een byte. De gangbare implementatie telt byte N-Colors op bij byte N, wat toevallig correct is bij 8 bits per component en overal elders stilzwijgend fout is. Een 8-bits RGB-scan decodeert perfect, en dan vernietigt dezelfde code een 4-bits geïndexeerde afbeelding de eerste keer dat er eentje verschijnt in productie

De correcte rekenkunde werkt binnen het bitveld. HotPDF loopt door samples van index Colors tot Colors * Columns - 1, extraheert de sample en zijn linkerbuur van dezelfde component met een masker van (1 shl BitsPerComponent) - 1 op de juiste verschuiving, telt ze modulo dat masker op, en schrijft het resultaat terug zonder de andere samples verpakt in dezelfde byte te verstoren. De staart telt ook mee: een rij wordt opgevuld tot een bytegrens, dus moeten de opvulbits na de laatste sample onaangeroerd overleven in plaats van in de rekenkunde gevouwen te worden. Bij 16 bits per component is elke sample een big-endian paar bytes en wikkelt de optelling om bij $FFFF over het paar in plaats van onafhankelijk over te dragen tussen bytes; bij 8 bits is de eenvoudige byte-recurrentie correct, met stappen van Colors zodat rood accumuleert tegen rood en alpha tegen alpha. In elke variant is het eerste pixel van een rij een letterlijke waarde, nooit een verschil, en de recurrentie herstart bij elke rijgrens — TIFF-predictie leest nooit de rij erboven, wat het hele verschil is tussen TIFF en de PNG-familie

Wat regelt EarlyChange nu echt in LZWDecode?

Het regelt wanneer de lezer zijn codegrootte met één bit verbreedt, en één code uit de pas zijn corrumpeert alles wat volgt. HotPDF drukt de regel uit als één enkele invariant: na het toevoegen van een dictionary-entry verbreedt de volgende read wanneer NextCode (1 shl CodeSize) - Ord(EarlyChange) bereikt. Met /EarlyChange 1, de standaard van ISO 32000-1 §7.4.4, gebeurt de omschakeling één code te vroeg; met /EarlyChange 0 gebeurt het precies op de grens. Beide komen voor in echte bestanden en niets in de bitstream vertelt je welke de encoder gebruikte. De rest van de state machine moet in lockstep meebewegen: een clear-code herstelt codegrootte, bitmasker, volgende vrije code en de phrase-opslag samen, en de end-of-information-code wordt gelezen op welke breedte dan ook actueel is op dat moment, niet op de initiële 9 bits. HotPDF begint op InitialCodeSize 9, begrenst de codegrootte op 12 en het dictionary op 4096 entries, en standaardiseert FillOrder naar foTop omdat PDF codes eerst met het hoogste-orde-bit verpakt — foBottom bestaat voor TIFF-achtige streams die dat niet doen

uses
  HPDFLZW;

var
  Decoder: TPDFLZWDecompressor;
  Parms: TPDFLZWParms;
  Plain: AnsiString;
begin
  Decoder := TPDFLZWDecompressor.Create;
  try
    Decoder.EarlyChange := True;      // /EarlyChange 1 is the PDF default
    Decoder.FillOrder := foTop;       // high-order bit first
    Decoder.MaxOutputBytes := 256 * 1024 * 1024;
    Decoder.RequireInitialClear := False;
    Decoder.RequireEndOfInformation := False;

    Parms.Predictor := 12;
    Parms.Colors := 3;
    Parms.BitsPerComponent := 8;
    Parms.Columns := 1024;
    Parms.ExpandedTo8Bit := False;
    Parms.ColorSpace := 'DeviceRGB';

    if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
      LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
        Decoder.KwKwKExpansions, Decoder.OutputBytes)
    else
      LogStreamDefect('lzw', Decoder.LastError);
  finally
    Decoder.Free;
  end;
end;

De statistieken bestaan voor triage, niet voor de show. Wanneer een bestand naar de juiste lengte decodeert maar de verkeerde pixels, vertellen PeakCodeSize en DictionaryAdds je onmiddellijk of de lezer ooit verbreed heeft waar de writer dat deed. Zet EarlyChange om, decodeer opnieuw, vergelijk de twee: als de getallen verschuiven, heb je je antwoord in één run in plaats van door een bit-lezer te stappen

De KwKwK-tak, en wanneer een stream gewoon moet falen

Het ene legale geval dat er illegaal uitziet is Code = NextCode, en HotPDF handelt het af door de entry te construeren voordat hij hem uitzendt. Een encoder mag de code uitzenden voor een phrase die hij in dezelfde stap definieert, wat gebeurt wanneer de invoer een patroon bevat van de vorm K w K w K; de decoder kan die code niet opzoeken omdat hij nog niet bestaat, dus moet hij Previous + First(Previous) bouwen, het als nieuwe entry toevoegen, en de entry uitzenden die hij zojuist gemaakt heeft. HotPDF telt die in KwKwKExpansions en controleert kruiselings dat de code die hij toevoegde de code is waar om gevraagd werd. Alles boven NextCode is corruptie, en daar moet een decoder stoppen in plaats van improviseren: HotPDF slaat alarm bij een toekomstige code, bij een dictionary-prefix die buiten de phrase-arena wijst, bij een vol dictionary, en bij een eerste code die geen letterlijke waarde is. Twee striktheidsschakelaars staan bewust standaard uit, RequireInitialClear en RequireEndOfInformation, omdat veel productie-PDF's de leidende clear-code weglaten of zonder terminator door de data heen raken. Zet ze aan wanneer je je eigen output valideert, laat ze uit wanneer je bestanden uit het wild consumeert

Waar /DecodeParms daadwerkelijk gelezen wordt aan de kant van het geladen document

HotPDF lost /DecodeParms of zijn afkorting /DP op in het image-stream-dictionary, accepteert zowel een dictionary als een array en neemt het laatste element wanneer het een array is, en draagt vervolgens Predictor, Colors, BitsPerComponent, Columns en EarlyChange door naar het rasterpad. Het array-geval is degene die mensen vergeten: een stream gefilterd door [/ASCII85Decode /FlateDecode] draagt een parallelle parameterarray, en de predictor-instellingen behoren bij het laatste filter, niet het eerste

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
      for I := 0 to Pdf.GetLoadedImageCount - 1 do
        if Pdf.GetLoadedImageInfo(I, Info) then
        begin
          Bmp := Pdf.ExtractLoadedImage(I);   // nil when the raster is unusable
          if Bmp <> nil then
          try
            Bmp.SaveToFile(Format('image-%d.bmp', [I]));
          finally
            Bmp.Free;
          end;
        end;
  finally
    Pdf.Free;
  end;
end;

Eén historisch defect op dat pad is het waard te noemen, omdat de klasse van bug zich herhaalt. De oude Flate-met-parameters-routine creëerde een decompressiestream en kopieerde vervolgens vanuit de originele gecomprimeerde invoer, dus ontving de predictor-stap gecomprimeerde bytes en de-predicte die plichtsgetrouw: altijd fout, nooit een alarm. De huidige code leest alleen van de decoder voordat het resultaat aan de gedeelde predictor doorgegeven wordt, en wijst een raster af dat korter is dan de berekende grootte in plaats van terug te vallen op de nog-steeds-gecomprimeerde bytes — een fallback die vroeger een decodeerfout in een corrupte bitmap veranderde. Diezelfde predictor-implementatie bedient nu ook cross-reference streams, wat een nuttige consistentie is als je ook werkt met object streams en incrementele updates, en de omringende extractiemachinerie wordt behandeld in het begeleidende artikel over het extraheren van geladen afbeeldingen en hun decode-filters. Afbeeldingen die binnenkomen als DCTDecode of JPXDecode bereiken de predictor helemaal nooit; ze dragen hun eigen gecomprimeerde pixelmodel

Doorvoer: een aaneengesloten phrase-arena versus per-entry strings

Het vervangen van het per-entry string-dictionary door een aaneengesloten phrase-arena mat ongeveer 1,61 keer sneller op een pathologische invoer: 1558 MiB/s tegen 969 MiB/s op een benchmark waarvan de enkele langste phrase 7.370.880 bytes bereikt. De vorm van die invoer verklaart het verschil, omdat de klassieke implementaties een van twee slechte afwegingen kiezen. Een dictionary van AnsiString-waarden alloceert en kopieert een verse string voor elk van tot 4096 entries, waarbij elke nieuwe entry zijn ouder in zijn geheel kopieert; een prefix/suffix-stack vermijdt dat geheugengebruik volledig maar reconstrueert elke phrase door de keten achterwaarts byte voor byte af te lopen en om te draaien, wat prima werkt voor gewone tekst en pijnlijk is wanneer één phrase tot megabytes oploopt. HotPDF voegt elke phrase aaneengesloten toe aan een geometrisch groeiende arena, indexeert entries op offset en lengte, en zendt een phrase uit met één enkele Move naar de outputbuffer. De eerlijke kost is geheugen: een arena die elke phrase in zijn geheel bevat, wordt begrensd door de som van alle phrase-lengtes in plaats van door het aantal entries, wat precies waarom MaxOutputBytes bestaat op zowel de decompressor als de predictor. Leid die limiet af van wat het image-dictionary beweert dat de raster zou moeten zijn, en een liegende stream faalt snel

De LZW-decompressor, de gedeelde predictor en het extractiepad voor geladen afbeeldingen die hier getoond worden, worden geleverd als onderdeel van het standaard HotPDF Component voor Delphi en C++Builder, met de volledige filter- en DecodeParms-referentie op de productpagina