Teknisk artikel

PDF-prediktor och LZWDecode-avkodning i Delphi med HotPDF

En ström märkt /Predictor 12 betyder inte att varje rad använder PNG-filter 2. HotPDF, den native VCL-PDF-komponenten för Delphi och C++Builder, behandlar prediktorvärdena 10 till 15 som en enda familj: den riktiga filtertaggen, 0 till 4, är det första byten i varje kodad rad, och HPDFDecodePredictor läser och validerar den taggen rad för rad. Den distinktionen är formen på nästan varje bugg i det här hörnet av PDF, eftersom ingenting slår larm när man gör fel. Filterkedjan körs, rastret har den storlek du förväntade dig, och bilden kommer ut som diagonalt brus eller en gradient som glider allt längre bort för varje skanlinje. De fem talen i /DecodeParms (ISO 32000-1 §7.4.4) ändrar mestadels betydelsen av byten snarare än deras längd, så ett fel värde ger troligt skräp i stället för ett fel

Varför betyder inte /Predictor 12 PNG-filter 2 på varje rad?

Därför att prediktornumret bara säger "PNG-prediktion används", inte vilket filter. PNG-kodare väljer ett filter per skanlinje och PDF-filtret ärver det, så prediktorvärdena 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) och 15 (Optimum) avkodas alla identiskt: den inledande taggbyten i varje rad är det avkodaren måste följa. Layoutkonsekvensen spelar lika stor roll som semantiken. Varje kodad rad är 1 + RowBytes byte lång, indata överskrider därför utdata med exakt radantalet, och en ström vars längd inte är en heltalsmultipel av RowBytes + 1 är per definition trunkerad. HotPDF kontrollerar den gränsen innan den rör vid ett enda byte, avvisar varje tagg över 4 med Invalid PNG predictor row tag, och läser föregående rad direkt ur den enda utdatabufferten i stället för att materialisera en tvådimensionell radarray. Filter 1 och 3 sträcker sig BytesPerPixel bakåt inom den aktuella raden, filter 2 läser rakt upp, filter 4 kör Paeth-valet över vänster, upp och upp-vänster — och alla fyra opererar på redan rekonstruerad utdata, vilket är varför uppraden måste vara den avkodade raden och aldrig den filtrerade indatan

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;

Argumentet MaxOutputBytes är ingen dekoration. Ett prediktorsteg är ett dekompressionssteg i förklädnad, och ett fientligt eller bara trasigt /Columns-värde förvandlar några kilobyte indata till en flergigabyte-allokeringsförfrågan. HotPDF beräknar bitar per rad, byte per rad och total rasterstorlek i Int64 först, vägrar geometri som svämmar över, och respekterar det anropar-angivna taket. Skicka en riktig gräns härledd från bilddictionaryn, och felläget blir ett loggat meddelande i stället för en minnesbristdialog på en kundmaskin

Varför korrumperar TIFF Predictor 2 4-bitars bilder?

Därför att Predictor 2 är horisontell differentiering per sampel, inte per byte, och vid 1, 2 eller 4 bitar per komponent delar flera samplar samma byte. Den vanliga implementationen adderar byte N-Colors till byte N, vilket råkar vara korrekt vid 8 bitar per komponent och är tyst fel överallt annars. En 8-bitars RGB-skanning avkodas perfekt, och samma kod förstör sedan en 4-bitars indexerad bild första gången en sådan dyker upp i produktion

Den korrekta aritmetiken verkar inuti bitfältet. HotPDF vandrar genom samplar från index Colors till Colors * Columns - 1, extraherar sampeln och dess vänstra granne i samma komponent med en mask på (1 shl BitsPerComponent) - 1 vid lämplig skiftning, adderar dem modulo den masken, och skriver resultatet tillbaka utan att störa de andra samplarna packade i samma byte. Svansen spelar också roll: en rad fylls ut till en bytegräns, så utfyllnadsbitarna efter den sista sampeln måste överleva orörda i stället för att vikas in i aritmetiken. Vid 16 bitar per komponent är varje sampel ett big-endian-par av byte och additionen wrap-ar vid $FFFF över paret snarare än att bära mellan byten oberoende av varandra; vid 8 bitar är den enkla byterekursionen rätt, med steg om Colors så att rött ackumuleras mot rött och alfa mot alfa. I varje variant är den första pixeln på en rad en literal, aldrig en differens, och rekursionen startar om vid varje radgräns — TIFF-prediktion läser aldrig raden ovanför, vilket är hela skillnaden mellan den och PNG-familjen

Vad styr EarlyChange egentligen i LZWDecode?

Den styr när läsaren breddar sin kodstorlek med en bit, och att vara en kod ur fas korrumperar allt som följer. HotPDF uttrycker regeln som en enda invariant: efter att en dictionary-post lagts till breddas nästa läsning när NextCode når (1 shl CodeSize) - Ord(EarlyChange). Med /EarlyChange 1, ISO 32000-1 §7.4.4-standardvärdet, sker bytet en kod tidigt; med /EarlyChange 0 sker det exakt vid gränsen. Båda förekommer i riktiga filer och ingenting i bitströmmen talar om för dig vilken kodaren använde. Resten av tillståndsmaskinen måste röra sig i lås steg: en clear-kod återställer kodstorlek, bitmask, nästa fria kod och frasförvaringen tillsammans, och slut-på-information-koden läses vid vilken bredd som än är aktuell i det ögonblicket, inte vid de ursprungliga 9 bitarna. HotPDF startar vid InitialCodeSize 9, täcker kodstorleken vid 12 och dictionaryn vid 4096 poster, och sätter FillOrder som standard till foTop eftersom PDF packar koder med högsta biten först — foBottom finns för TIFF-liknande strömmar som inte gör det

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;

Statistiken finns för triage, inte för fåfänga. När en fil avkodas till rätt längd men fel pixlar berättar PeakCodeSize och DictionaryAdds omedelbart om läsaren någonsin breddade där skrivaren gjorde det. Väx EarlyChange, avkoda igen, jämför de två: om siffrorna flyttar sig har du svaret på en körning i stället för att stega genom en bitläsare

KwKwK-grenen, och när en ström helt enkelt bör misslyckas

Det enda lagliga fallet som ser olagligt ut är Code = NextCode, och HotPDF hanterar det genom att konstruera posten innan den sänds ut. En kodare kan sända ut koden för en fras den definierar i samma steg, vilket händer närhelst indata innehåller ett mönster av formen K w K w K; avkodaren kan inte slå upp den koden eftersom den inte finns ännu, så den måste bygga Previous + First(Previous), lägga till den som den nya posten, och sända ut posten den just skapade. HotPDF räknar dessa i KwKwKExpansions och korskontrollerar att koden den lade till är koden den ombads om. Allt över NextCode är korruption, och där bör en avkodare stanna snarare än improvisera: HotPDF slår larm vid en framtida kod, vid ett dictionary-prefix som pekar utanför frasarenan, vid en full dictionary, och vid en första kod som inte är en literal. Två strikthetsomkopplare är medvetet avstängda som standard, RequireInitialClear och RequireEndOfInformation, eftersom gott om produktions-PDF:er utelämnar den inledande clear-koden eller tar slut på data utan en terminator. Slå på dem när du validerar din egen utdata, låt dem vara avstängda när du konsumerar filer från vildmarken

Var /DecodeParms faktiskt läses på den inlästa dokumentsidan

HotPDF löser upp /DecodeParms eller dess förkortning /DP på bildströmmens ordbok, accepterar antingen en ordbok eller en array och tar det sista elementet när det är en array, och för sedan in Predictor, Colors, BitsPerComponent, Columns och EarlyChange i rastervägen. Array-fallet är det folk glömmer: en ström filtrerad av [/ASCII85Decode /FlateDecode] bär en parallell parameterarray, och prediktorinställningarna tillhör det sista filtret, inte det första

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;

En historisk defekt på den vägen är värd att nämna, eftersom klassen av bugg upprepar sig. Den gamla Flate-med-parametrar-rutinen skapade en dekompressionsström och kopierade sedan från den ursprungliga komprimerade indatan, så prediktorsteget mottog komprimerade byte och avprediktade dem plikttroget: alltid fel, aldrig ett larm. Den nuvarande koden läser bara från avkodaren innan resultatet lämnas till den delade prediktorn, och den avvisar ett raster kortare än den beräknade storleken i stället för att falla tillbaka till de fortfarande komprimerade byten — en reservlösning som brukade förvandla ett avkodningsfel till en korrupt bitmapp. Samma prediktorimplementation betjänar nu även korsreferensströmmar, vilket är en användbar konsekvens om du också arbetar med objektströmmar och inkrementella uppdateringar, och den omgivande extraktionsmekaniken täcks i följdartikeln om att extrahera inlästa bilder och deras avkodningsfilter. Bilder som kommer som DCTDecode eller JPXDecode når aldrig prediktorn alls; de bär sin egen komprimerade pixelmodell

Genomströmning: en sammanhängande frasarena kontra strängar per post

Att ersätta strängdictionaryn per post med en sammanhängande frasarena mätte cirka 1,61 gånger snabbare på en patologisk indata: 1558 MiB/s mot 969 MiB/s på ett benchmark vars enskilt längsta fras når 7 370 880 byte. Formen på den indatan förklarar gapet, eftersom de klassiska implementationerna väljer en av två dåliga avvägningar. En dictionary med AnsiString-värden allokerar och kopierar en ny sträng för var och en av upp till 4096 poster, där varje ny post kopierar hela sin förälder; en prefix/suffix-stack undviker det minnet helt men rekonstruerar varje fras genom att vandra kedjan baklänges ett byte i taget och vända den, vilket är helt i sin ordning för vanlig text och plågsamt när en fras sträcker sig till megabyte. HotPDF lägger till varje fras sammanhängande i en geometriskt växande arena, indexerar poster efter offset och längd, och sänder ut en fras med en enda Move in i utdatabufferten. Den ärliga kostnaden är minne: en arena som håller varje fras i sin helhet begränsas av summan av alla fraslängder snarare än av postantalet, vilket är precis varför MaxOutputBytes finns på både dekompressorn och prediktorn. Härled den gränsen från vad bilddictionaryn påstår att rastret borde vara, och en ljugande ström misslyckas snabbt

LZW-dekompressorn, den delade prediktorn och vägen för att extrahera inlästa bilder som visas här levereras som en del av standard-HotPDF Component för Delphi och C++Builder, med den fullständiga filter- och DecodeParms-referensen på produktsidan