En strøm mærket /Predictor 12 betyder ikke, at hver række bruger PNG-filter 2. HotPDF, den native VCL PDF-komponent til Delphi og C++Builder, behandler predictor-værdier 10 til 15 som én familie: det egentlige filtertag, 0 til 4, er den første byte af hver kodet række, og HPDFDecodePredictor læser og validerer det tag række for række. Den skelnen er formen på næsten hver eneste fejl i dette hjørne af PDF, fordi intet rejser en fejl, når man får det galt. Filterkæden kører, rasteret er den størrelse, man forventede, og billedet kommer ud som diagonal støj eller en gradient, der driver længere væk for hver scanlinje. De fem tal i /DecodeParms (ISO 32000-1 §7.4.4) ændrer for det meste betydningen af bytesne frem for deres længde, så et forkert tal producerer plausibelt skrammel i stedet for en fejl
Hvorfor betyder /Predictor 12 ikke PNG-filter 2 på hver række?
Fordi predictor-tallet kun siger "PNG-forudsigelse er i brug", ikke hvilket filter. PNG-encodere vælger et filter per scanlinje, og PDF-filteret arver det, så predictor-værdierne 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) og 15 (Optimum) afkodes alle identisk: den indledende tag-byte i hver række er det, afkoderen skal adlyde. Layout-konsekvensen betyder lige så meget som semantikken. Hver kodet række er 1 + RowBytes bytes lang, inputtet overstiger derfor outputtet med præcis rækkeantallet, og en strøm hvis længde ikke er et helt multiplum af RowBytes + 1 er per definition afkortet. HotPDF tjekker den grænse, før den rører en byte, afviser ethvert tag over 4 med Invalid PNG predictor row tag, og læser den forrige række direkte ud af den samme outputbuffer i stedet for at materialisere et to-dimensionelt rækkearray. Filtre 1 og 3 rækker BytesPerPixel tilbage inden for den nuværende række, filter 2 læser lige op, filter 4 kører Paeth-valget over venstre, op og op-venstre — og alle fire opererer på allerede rekonstrueret output, hvilket er hvorfor op-rækken skal være den afkodede række og aldrig det filtrerede 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;
MaxOutputBytes-argumentet er ikke dekoration. Et predictor-trin er et dekompressionstrin i forklædning, og en fjendtlig eller blot ødelagt /Columns-værdi gør nogle få kilobyte input til en flere-gigabyte-allokeringsanmodning. HotPDF beregner bits per række, bytes per række og total rasterstørrelse i Int64 først, afviser geometri der overløber, og respekterer det kalder-leverede loft. Overgiv en reel grænse udledt af billeddictionaryet, og fejltilstanden bliver en logget besked frem for en out-of-memory-dialog på en kundemaskine
Hvorfor korrumperer TIFF Predictor 2 4-bit-billeder?
Fordi Predictor 2 er horisontal differencing per sample, ikke per byte, og ved 1, 2 eller 4 bit per komponent deler flere samples en byte. Den almindelige implementering lægger byte N-Colors til byte N, hvilket tilfældigvis er korrekt ved 8 bit per komponent og stille forkert alle andre steder. Et 8-bit RGB-scan afkoder perfekt, og så ødelægger den samme kode et 4-bit indekseret billede første gang et dukker op i produktion
Den korrekte aritmetik virker inden i bitfeltet. HotPDF gennemgår samples fra indeks Colors til Colors * Columns - 1, udtrækker samplen og dens samme-komponent venstre nabo med en maske på (1 shl BitsPerComponent) - 1 ved det passende shift, lægger dem sammen modulo den maske, og skriver resultatet tilbage uden at forstyrre de andre samples pakket ind i den samme byte. Halen betyder også noget: en række paddes til en bytegrænse, så padding-bittene efter den sidste sample skal overleve urørt frem for at blive foldet ind i aritmetikken. Ved 16 bit per komponent er hver sample et big-endian-par af bytes, og additionen wrapper ved $FFFF på tværs af parret frem for at bære mellem bytes uafhængigt; ved 8 bit er den simple byte-rekurrens korrekt, med skridt på Colors så rød akkumulerer mod rød og alfa mod alfa. I hver variant er den første pixel i en række en literal, aldrig en forskel, og rekurrensen genstarter ved hver rækkegrænse — TIFF-forudsigelse læser aldrig rækken ovenover, hvilket er hele forskellen mellem den og PNG-familien
Hvad styrer EarlyChange egentlig i LZWDecode?
Det styrer, hvornår læseren udvider sin kodestørrelse med én bit, og at være én kode ude af trit korrumperer alt, hvad der følger efter. HotPDF udtrykker reglen som én enkelt invariant: efter at have tilføjet en dictionary-post, udvider den næste læsning, når NextCode når (1 shl CodeSize) - Ord(EarlyChange). Med /EarlyChange 1, ISO 32000-1 §7.4.4-standarden, sker skiftet én kode for tidligt; med /EarlyChange 0 sker det præcis ved grænsen. Begge optræder i rigtige filer, og intet i bitstrømmen fortæller dig hvilken encoderen brugte. Resten af tilstandsmaskinen skal bevæge sig i lockstep: en clear-kode nulstiller kodestørrelse, bitmaske, næste fri kode og frase-lageret sammen, og end-of-information-koden læses ved hvilken bredde der er aktuel på det tidspunkt, ikke ved de indledende 9 bit. HotPDF starter ved InitialCodeSize 9, sætter loft for kodestørrelsen ved 12 og dictionaryet ved 4096 poster, og lader FillOrder være foTop som standard, fordi PDF pakker koder høj-ordens-bit først — foBottom findes til TIFF-stil-strømme, der ikke gør
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;
Statistikkerne findes til triage, ikke forfængelighed. Når en fil afkoder til den rigtige længde, men de forkerte pixels, fortæller PeakCodeSize og DictionaryAdds dig med det samme, om læseren nogensinde udvidede, hvor skriveren gjorde. Flip EarlyChange, afkod igen, sammenlign de to: hvis tallene flytter sig, har du dit svar på én kørsel i stedet for at træde gennem en bitlæser
KwKwK-grenen, og hvornår en strøm blot bør fejle
Det ene lovlige tilfælde, der ser ulovligt ud, er Code = NextCode, og HotPDF håndterer det ved at konstruere posten, før den udsendes. En encoder kan udsende koden for en frase, den definerer i samme skridt, hvilket sker, når inputtet indeholder et mønster af formen K w K w K; afkoderen kan ikke slå den kode op, fordi den endnu ikke findes, så den må bygge Previous + First(Previous), tilføje den som den nye post, og udsende posten, den lige oprettede. HotPDF tæller dem i KwKwKExpansions og krydstjekker at koden, den tilføjede, er koden, den blev bedt om. Alt over NextCode er korruption, og der bør en afkoder stoppe frem for at improvisere: HotPDF rejser en fejl på en fremtidig kode, på et dictionary-prefiks der peger uden for frase-arenaen, på et fuldt dictionary, og på en første kode der ikke er en literal. To strenghedskontakter er bevidst slået fra som standard, RequireInitialClear og RequireEndOfInformation, fordi mange produktions-PDF'er udelader den indledende clear-kode eller løber tør for data uden en terminator. Slå dem til, når du validerer dit eget output, lad dem være slået fra, når du konsumerer filer fra det vilde
Hvor /DecodeParms faktisk læses på den indlæste-dokument-side
HotPDF opløser /DecodeParms eller dens forkortelse /DP på billed-strøm-dictionaryet, accepterer enten en dictionary eller et array og tager det sidste element, når det er et array, og fører derefter Predictor, Colors, BitsPerComponent, Columns og EarlyChange ind i raster-stien. Array-tilfældet er det, folk glemmer: en strøm filtreret af [/ASCII85Decode /FlateDecode] bærer et parallelt parameterarray, og predictor-indstillingerne hører til det sidste filter, 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;
En historisk defekt på den sti er værd at nævne, fordi fejlklassen gentager sig. Den gamle Flate-med-parametre-rutine oprettede en dekompressionsstrøm og kopierede derefter fra det oprindelige komprimerede input, så predictor-trinet modtog komprimerede bytes og pligtskyldigt un-forudsagde dem: altid forkert, aldrig fejlmeldt. Den nuværende kode læser kun fra afkoderen, før den overgiver resultatet til den delte predictor, og den afviser et raster kortere end den beregnede størrelse i stedet for at falde tilbage på de stadig komprimerede bytes — et fallback, der plejede at gøre en afkodningsfejl til et korrupt bitmap. Den samme predictor-implementering betjener nu også krydsreference-strømme, hvilket er en nyttig konsistens, hvis man også arbejder med objektstrømme og inkrementelle opdateringer, og den omkringliggende udtræksmaskine dækkes i følgestykket om at udtrække indlæste billeder og deres decode-filtre. Billeder der ankommer som DCTDecode eller JPXDecode når slet aldrig predictoren; de bærer deres egen komprimerede pixelmodel
Gennemløb: en sammenhængende frase-arena versus per-post-strenge
At erstatte per-post-streng-dictionaryet med en sammenhængende frase-arena målte omkring 1,61 gange hurtigere på et patologisk input: 1558 MiB/s mod 969 MiB/s på et benchmark, hvis enkelt længste frase når 7.370.880 bytes. Formen på det input forklarer forskellen, fordi de klassiske implementeringer vælger en af to dårlige afvejninger. Et dictionary af AnsiString-værdier allokerer og kopierer en frisk streng for hver af op til 4096 poster, hvor hver ny post kopierer sin forælder helt; en prefix/suffix-stak undgår den hukommelse helt, men rekonstruerer hver frase ved at gå kæden baglæns én byte ad gangen og vende den om, hvilket er fint for almindelig tekst og smertefuldt når én frase løber op i megabyte. HotPDF tilføjer hver frase sammenhængende til en geometrisk voksende arena, indekserer poster efter offset og længde, og udsender en frase med et enkelt Move ind i outputbufferen. Den ærlige omkostning er hukommelse: en arena der holder hver frase i fuld længde er begrænset af summen af alle frase-længder frem for af postantallet, hvilket er præcis hvorfor MaxOutputBytes findes på både dekompressoren og predictoren. Udled den grænse fra hvad billeddictionaryet hævder rasteret bør være, og en løgnagtig strøm fejler hurtigt
LZW-dekompressoren, den delte predictor og den indlæste-billede-udtræksstien vist her leveres som del af standard-HotPDF Component til Delphi og C++Builder, med den fulde filter- og DecodeParms-reference på produktsiden