Teknisk artikkel

PDF Predictor og LZWDecode-dekoding i Delphi med HotPDF

En strøm merket /Predictor 12 betyr ikke at hver rad bruker PNG-filter 2. HotPDF, den native VCL PDF-komponenten for Delphi og C++Builder, behandler predictor-verdiene 10 til 15 som én familie: den ekte filtertaggen, 0 til 4, er den første byten i hver kodede rad, og HPDFDecodePredictor leser og validerer den taggen rad for rad. Det skillet er formen på nesten hver eneste feil i dette hjørnet av PDF, fordi ingenting varsler når du får det galt. Filterkjeden kjører, rasteret har størrelsen du forventet, og bildet kommer ut som diagonal støy eller en gradient som driver stadig lenger av med hver skannelinje. De fem tallene i /DecodeParms (ISO 32000-1 §7.4.4) endrer stort sett meningen til bytene fremfor lengden deres, så en gal verdi produserer plausibelt søppel i stedet for en feil

Hvorfor betyr ikke /Predictor 12 PNG-filter 2 på hver rad?

Fordi predictor-nummeret bare sier «PNG-prediksjon er i bruk», ikke hvilket filter. PNG-kodere velger et filter per skannelinje og PDF-filteret arver det, så predictor-verdiene 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) og 15 (Optimum) dekodes alle identisk: den ledende tag-byten i hver rad er det dekoderen må adlyde. Layout-konsekvensen betyr like mye som semantikken. Hver kodede rad er 1 + RowBytes byte lang, inputen overstiger derfor utdataen med nøyaktig radantallet, og en strøm hvis lengde ikke er et helt multiplum av RowBytes + 1 er avkuttet per definisjon. HotPDF sjekker den grensen før den rører en byte, avviser enhver tag over 4 med Invalid PNG predictor row tag, og leser den forrige raden direkte ut av den enkelte utdatabufferen fremfor å materialisere en todimensjonal radarray. Filter 1 og 3 rekker tilbake BytesPerPixel innenfor gjeldende rad, filter 2 leser rett opp, filter 4 kjører Paeth-valget over venstre, opp og opp-venstre — og alle fire opererer på allerede rekonstruert utdata, og det er derfor opp-raden må være den dekodede raden og aldri den filtrerte inputen

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;

MaxOutputBytes-argumentet er ikke pynt. Et predictor-trinn er et dekomprimeringstrinn i forkledning, og en fiendtlig eller bare ødelagt /Columns-verdi gjør noen få kilobyte input om til en flergigabyte allokeringsforespørsel. HotPDF beregner bit per rad, byte per rad og total rastergrøtterelse i Int64 først, avviser geometri som overflyter, og respekterer den kallerangitte taket. Send inn en reell grense utledet fra bildeordboken, og feilmodusen blir en logget melding fremfor en tomt-for-minne-dialog på en kundemaskin

Hvorfor korrumperer TIFF Predictor 2 4-bits bilder?

Fordi Predictor 2 er horisontal differensiering per sample, ikke per byte, og ved 1, 2 eller 4 bit per komponent deler flere samples samme byte. Den vanlige implementasjonen legger byte N-Colors til byte N, noe som tilfeldigvis er riktig ved 8 bit per komponent og stille feil overalt ellers. En 8-bits RGB-skanning dekodes perfekt, så ødelegger samme kode et 4-bits indeksert bilde første gang et slikt dukker opp i produksjon

Den korrekte aritmetikken virker inne i bitfeltet. HotPDF går gjennom samples fra indeks Colors til Colors * Columns - 1, trekker ut sampleen og dens samme-komponent venstre-nabo med en maske på (1 shl BitsPerComponent) - 1 ved passende skift, legger dem sammen modulo den masken, og skriver resultatet tilbake uten å forstyrre de andre samplene pakket inn i samme byte. Halen betyr også noe: en rad polstres til en bytegrense, så polstringsbitene etter den siste sampleen må overleve urørt fremfor å foldes inn i aritmetikken. Ved 16 bit per komponent er hver sample et big-endian bytepar og addisjonen ruller rundt ved $FFFF på tvers av paret fremfor å føre over mellom byte uavhengig; ved 8 bit er den enkle byte-rekurrensen riktig, med steg på Colors slik at rødt akkumuleres mot rødt og alfa mot alfa. I hver variant er den første pikselen i en rad en literal, aldri en differanse, og rekurrensen starter på nytt ved hver radgrense — TIFF-prediksjon leser aldri raden over, som er hele forskjellen mellom den og PNG-familien

Hva styrer EarlyChange egentlig i LZWDecode?

Den styrer når leseren utvider kodestørrelsen med én bit, og å være ute av trinn med én kode korrumperer alt som følger. HotPDF uttrykker regelen som én enkelt invariant: etter å ha lagt til en ordbokoppføring, utvider neste lesing når NextCode når (1 shl CodeSize) - Ord(EarlyChange). Med /EarlyChange 1, standarden i ISO 32000-1 §7.4.4, skjer byttet én kode tidlig; med /EarlyChange 0 skjer det nøyaktig ved grensen. Begge forekommer i ekte filer og ingenting i bitstrømmen forteller deg hvilken koderen brukte. Resten av tilstandsmaskinen må bevege seg i samme takt: en clear-kode nullstiller kodestørrelse, bitmaske, neste ledige kode og frasedataene sammen, og end-of-information-koden leses ved hvilken bredde som enn er gjeldende på det tidspunktet, ikke ved den initielle 9 bit. HotPDF starter ved InitialCodeSize 9, setter tak på kodestørrelsen ved 12 og ordboken ved 4096 oppføringer, og setter FillOrder som standard til foTop fordi PDF pakker koder med høyeste-orden-bit først — foBottom finnes for TIFF-stil-strømmer som ikke gjø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;

Statistikken finnes for triage, ikke forfengelighet. Når en fil dekodes til riktig lengde men feil piksler, forteller PeakCodeSize og DictionaryAdds deg umiddelbart om leseren noensinne utvidet der skriveren gjorde det. Vend om EarlyChange, dekod igjen, sammenlign de to: hvis tallene beveger seg, har du svaret ditt i én kjøring i stedet for å steppe gjennom en bitleser

KwKwK-grenen, og når en strøm rett og slett bør feile

Det ene lovlige tilfellet som ser ulovlig ut er Code = NextCode, og HotPDF håndterer det ved å konstruere oppføringen før den utstedes. En koder kan utstede koden for en frase den definerer i samme steg, noe som skjer når som helst inputen inneholder et mønster av formen K w K w K; dekoderen kan ikke slå opp den koden fordi den ikke finnes ennå, så den må bygge Previous + First(Previous), legge den til som den nye oppføringen, og utstede oppføringen den nettopp opprettet. HotPDF teller disse i KwKwKExpansions og krysssjekker at koden den la til er koden den ble bedt om. Alt over NextCode er korrupsjon, og der bør en dekoder stoppe fremfor å improvisere: HotPDF varsler på en fremtidig kode, på et ordbok-prefiks som peker utenfor fraseareaet, på en full ordbok, og på en første kode som ikke er en literal. To strenghetsbrytere er bevisst avslått som standard, RequireInitialClear og RequireEndOfInformation, fordi mange produksjons-PDF-er utelater den innledende clear-koden eller går tom for data uten en terminator. Slå dem på når du validerer din egen utdata, la dem stå av når du konsumerer filer fra det ville

Hvor /DecodeParms faktisk leses på det lastede-dokument-siden

HotPDF løser opp /DecodeParms eller forkortelsen /DP på bildets strømordbok, aksepterer enten en ordbok eller en array og tar det siste elementet når det er en array, og fører deretter Predictor, Colors, BitsPerComponent, Columns og EarlyChange inn i rasterveien. Array-tilfellet er det folk glemmer: en strøm filtrert av [/ASCII85Decode /FlateDecode] bærer en parallell parameterarray, og predictor-innstillingene tilhører det siste filteret, ikke det første

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;

Én historisk defekt på den veien er verdt å nevne, fordi klassen av feil gjentar seg. Den gamle Flate-med-parametere-rutinen opprettet en dekomprimeringsstrøm og kopierte deretter fra den originale komprimerte inputen, så predictor-trinnet mottok komprimerte byte og av-predikterte dem lydig: alltid feil, aldri varslet. Den nåværende koden leser bare fra dekoderen før den gir resultatet til den delte predictoren, og den avviser et raster kortere enn den beregnede størrelsen fremfor å falle tilbake til de fortsatt komprimerte bytene — en fallback som pleide å gjøre en dekodingsfeil om til et korrupt bitkart. Den samme predictor-implementasjonen betjener nå også kryssreferansestrømmer, noe som er en nyttig konsistens hvis du også arbeider med objektstrømmer og inkrementelle oppdateringer, og den omkringliggende ekstraksjonsmekanismen er dekket i følgestykket om å hente ut lastede bilder og deres dekodefiltre. Bilder som ankommer som DCTDecode eller JPXDecode når aldri predictoren i det hele tatt; de bærer sin egen komprimerte pikselmodell

Gjennomstrømning: et sammenhengende frasearea versus per-oppføring-strenger

Å erstatte per-oppføring-strengordboken med et sammenhengende frasearea målte omtrent 1,61 ganger raskere på en patologisk input: 1558 MiB/s mot 969 MiB/s på en benchmark hvis enkeltlengste frase når 7 370 880 byte. Formen på den inputen forklarer gapet, fordi de klassiske implementasjonene velger én av to dårlige avveininger. En ordbok av AnsiString-verdier allokerer og kopierer en fersk streng for hver av opptil 4096 oppføringer, hver ny oppføring kopierer sin forelder i sin helhet; en prefiks/suffiks-stabel unngår det minnet helt men rekonstruerer hver frase ved å gå kjeden bakover én byte om gangen og reversere den, noe som er greit for vanlig tekst og smertefullt når én frase strekker seg til megabyte. HotPDF legger hver frase sammenhengende til et geometrisk voksende area, indekserer oppføringer etter offset og lengde, og utsteder en frase med en enkelt Move inn i utdatabufferen. Den ærlige kostnaden er minne: et area som holder hver frase i sin helhet er begrenset av summen av alle fraselengder fremfor av oppføringsantallet, og det er nettopp derfor MaxOutputBytes finnes på både dekompressoren og predictoren. Utled den grensen fra hva bildeordboken hevder at rasteret skal være, og en løgnaktig strøm feiler raskt

LZW-dekompressoren, den delte predictoren og den lastede-bilde-ekstraksjonsveien vist her leveres som en del av standard HotPDF Component for Delphi og C++Builder, med den fullstendige filter- og DecodeParms-referansen på produktsiden