Un stream marcat /Predictor 12 nu înseamnă că fiecare rând folosește filtrul PNG 2. HotPDF, componenta VCL nativă pentru PDF în Delphi și C++Builder, tratează valorile de predictor 10 până la 15 ca o singură familie: tag-ul real de filtru, de la 0 la 4, e primul byte al fiecărui rând encodat, iar HPDFDecodePredictor citește și validează acel tag rând cu rând. Această distincție e forma aproape a fiecărui bug din acest colț al PDF-ului, pentru că nimic nu ridică o excepție când greșești. Lanțul de filtre rulează, rasterul are dimensiunea așteptată, iar imaginea iese ca static diagonal sau ca un gradient care se abate tot mai mult cu fiecare linie de scanare. Cele cinci numere din /DecodeParms (ISO 32000-1 §7.4.4) schimbă în cea mai mare parte sensul bytes-ilor, nu lungimea lor, așa că unul greșit produce gunoi plauzibil în loc de o eroare
De ce /Predictor 12 nu înseamnă filtrul PNG 2 pe fiecare rând?
Pentru că numărul de predictor spune doar „se folosește predicția PNG”, nu care filtru anume. Encoderele PNG aleg un filtru per linie de scanare, iar filtrul PDF moștenește asta, așa că valorile de predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) și 15 (Optimum) se decodează toate identic: byte-ul de tag de la începutul fiecărui rând e ceea ce decodorul trebuie să respecte. Consecința de layout contează la fel de mult ca semantica. Fiecare rând encodat are 1 + RowBytes bytes lungime, deci input-ul depășește output-ul cu exact numărul de rânduri, iar un stream a cărui lungime nu e un multiplu întreg al lui RowBytes + 1 e trunchiat prin definiție. HotPDF verifică acea graniță înainte de a atinge vreun byte, respinge orice tag peste 4 cu Invalid PNG predictor row tag și citește rândul anterior direct din bufferul unic de ieșire, în loc să materializeze un array de rânduri bidimensional. Filtrele 1 și 3 se întorc înapoi BytesPerPixel în rândul curent, filtrul 2 citește direct în sus, filtrul 4 rulează alegerea Paeth peste stânga, sus și sus-stânga — și toate patru operează pe output deja reconstruit, motiv pentru care rândul de sus trebuie să fie rândul decodat, niciodată input-ul filtrat
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;
Argumentul MaxOutputBytes nu e decorativ. Un stadiu de predictor e un stadiu de decompresie deghizat, iar o valoare /Columns ostilă sau pur și simplu greșită transformă câțiva kilobytes de input într-o cerere de alocare de multe gigabytes. HotPDF calculează întâi biții pe rând, bytes-ii pe rând și dimensiunea totală a rasterului în Int64, refuză geometria care depășește limitele și respectă plafonul furnizat de apelant. Dă-i o limită reală derivată din dicționarul imaginii, iar modul de eșec devine un mesaj logat, nu un dialog out-of-memory pe mașina unui client
De ce corupe TIFF Predictor 2 imaginile pe 4 biți?
Pentru că Predictor 2 e diferențiere orizontală per eșantion, nu per byte, iar la 1, 2 sau 4 biți per componentă, mai multe eșantioane împart un byte. Implementarea obișnuită adaugă byte-ul N-Colors la byte-ul N, ceea ce se întâmplă să fie corect la 8 biți per componentă și e silențios greșit oriunde altundeva. O scanare RGB pe 8 biți se decodează perfect, apoi același cod distruge o imagine indexată pe 4 biți prima dată când una apare în producție
Aritmetica corectă lucrează în interiorul câmpului de biți. HotPDF parcurge eșantioanele de la indexul Colors până la Colors * Columns - 1, extrage eșantionul și vecinul lui din stânga, aceeași componentă, cu o mască de (1 shl BitsPerComponent) - 1 la shift-ul potrivit, le adună modulo acea mască și scrie rezultatul înapoi fără să deranjeze celelalte eșantioane împachetate în același byte. Coada contează și ea: un rând e umplut până la un hotar de byte, așa că biții de umplutură de după ultimul eșantion trebuie să supraviețuiască neatinși, în loc să fie înghițiți în aritmetică. La 16 biți per componentă, fiecare eșantion e o pereche de bytes big-endian, iar adunarea se întoarce la $FFFF peste toată perechea, nu prin transport independent între bytes; la 8 biți, recurența simplă pe bytes e corectă, avansând cu Colors, astfel încât roșul se acumulează contra roșului și alfa contra alfa. În fiecare variantă, primul pixel al unui rând e un literal, niciodată o diferență, iar recurența repornește la fiecare hotar de rând — predicția TIFF nu citește niciodată rândul de deasupra, ceea ce e întreaga diferență dintre ea și familia PNG
Ce controlează de fapt EarlyChange în LZWDecode?
Controlează momentul în care cititorul își lărgește dimensiunea de cod cu un bit, iar a fi decalat cu un singur cod corupe tot ce urmează. HotPDF exprimă regula ca un singur invariant: după adăugarea unei intrări în dicționar, următoarea citire se lărgește atunci când NextCode ajunge la (1 shl CodeSize) - Ord(EarlyChange). Cu /EarlyChange 1, valoarea implicită din ISO 32000-1 §7.4.4, comutarea se întâmplă cu un cod mai devreme; cu /EarlyChange 0 se întâmplă exact la hotar. Ambele apar în fișiere reale, iar nimic din bitstream nu-ți spune pe care l-a folosit encoderul. Restul mașinii de stare trebuie să se miște în pas: un cod de clear resetează dimensiunea codului, masca de biți, următorul cod liber și stocarea de fraze, toate deodată, iar codul de sfârșit-al-informației se citește la orice lățime e curentă în acel moment, nu la cei 9 biți inițiali. HotPDF pornește la InitialCodeSize 9, plafonează dimensiunea codului la 12 și dicționarul la 4096 de intrări, și setează implicit FillOrder la foTop, pentru că PDF-ul împachetează codurile cu bitul de rang înalt primul — foBottom există pentru stream-urile în stil TIFF care nu fac asta
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;
Statisticile există pentru triaj, nu de fudulie. Când un fișier se decodează la lungimea corectă, dar cu pixelii greșiți, PeakCodeSize și DictionaryAdds îți spun imediat dacă cititorul s-a lărgit vreodată acolo unde a făcut-o și scriitorul. Comută EarlyChange, decodează din nou, compară cele două: dacă numerele se mișcă, ai răspunsul într-o singură rulare, în loc să pășești printr-un bit reader
Ramura KwKwK, și când un stream ar trebui pur și simplu să eșueze
Singurul caz legal care arată ilegal e Code = NextCode, iar HotPDF îl tratează construind intrarea înainte de a o emite. Un encoder poate emite codul pentru o frază pe care tocmai o definește în același pas, ceea ce se întâmplă oricând input-ul conține un tipar de forma K w K w K; decodorul nu poate căuta acel cod, pentru că încă nu există, așa că trebuie să construiască Previous + First(Previous), să-l adauge ca intrare nouă și să emită intrarea pe care tocmai a creat-o. HotPDF le numără pe acestea în KwKwKExpansions și verifică încrucișat că intrarea adăugată e cea cerută. Tot ce e peste NextCode e corupție, iar acolo un decodor ar trebui să se oprească, nu să improvizeze: HotPDF ridică o excepție la un cod din viitor, la un prefix de dicționar care indică în afara arenei de fraze, la un dicționar plin și la un prim cod care nu e literal. Două comutatoare de stricțe sunt dezactivate în mod deliberat implicit, RequireInitialClear și RequireEndOfInformation, pentru că multe PDF-uri de producție omit codul de clear inițial sau rămân fără date, fără terminator. Pornește-le atunci când îți validezi propriul output, lasă-le oprite atunci când consumi fișiere din lumea largă
Unde e citit de fapt /DecodeParms pe partea documentului încărcat
HotPDF rezolvă /DecodeParms sau abrevierea lui /DP pe dicționarul stream-ului de imagine, acceptă fie un dicționar, fie un array, și ia ultimul element atunci când e un array, apoi duce Predictor, Colors, BitsPerComponent, Columns și EarlyChange în calea de raster. Cazul array e cel pe care lumea îl uită: un stream filtrat prin [/ASCII85Decode /FlateDecode] poartă un array de parametri paralel, iar setările de predictor aparțin ultimului filtru, nu primului
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;
Un defect istoric pe această cale merită menționat, pentru că clasa asta de bug se repetă. Vechea rutină de Flate-cu-parametri crea un stream de decompresie și apoi copia din input-ul comprimat original, așa că stadiul de predictor primea bytes comprimați și îi de-predicta cu conștiinciozitate: mereu greșit, niciodată o excepție ridicată. Codul actual citește doar de la decodor, înainte de a preda rezultatul predictorului partajat, și respinge un raster mai scurt decât dimensiunea calculată, în loc să cadă înapoi pe bytes-ii încă comprimați — un fallback care obișnuia să transforme un eșec de decodare într-un bitmap corupt. Aceeași implementare de predictor deservește acum și stream-urile cross-reference, ceea ce e o consistență utilă dacă lucrezi și cu stream-uri de obiecte și actualizări incrementale, iar mecanismul de extracție din jur e acoperit în piesa însoțitoare despre extragerea imaginilor încărcate și a filtrelor lor de decodare. Imaginile care sosesc ca DCTDecode sau JPXDecode nu ajung deloc la predictor; poartă propriul lor model de pixeli comprimat
Debit: o arenă de fraze contiguă versus șiruri per-intrare
Înlocuirea dicționarului de șiruri per-intrare cu o arenă de fraze contiguă a măsurat aproximativ de 1,61 ori mai rapid pe un input patologic: 1558 MiB/s față de 969 MiB/s pe un benchmark a cărui cea mai lungă frază unică ajunge la 7.370.880 de bytes. Forma acelui input explică diferența, pentru că implementările clasice aleg unul din două compromisuri proaste. Un dicționar de valori AnsiString alocă și copiază un șir nou pentru fiecare dintre până la 4096 de intrări, fiecare intrare nouă copiindu-și părintele în întregime; o stivă prefix/sufix evită acea memorie în întregime, dar reconstruiește fiecare frază parcurgând lanțul înapoi, byte cu byte, și inversând-o, ceea ce e în regulă pentru text obișnuit și dureros când o frază ajunge la megabytes. HotPDF adaugă fiecare frază contiguu la o arenă crescută geometric, indexează intrările după offset și lungime, și emite o frază cu un singur Move în bufferul de ieșire. Costul onest e memoria: o arenă care ține fiecare frază în întregime e mărginită de suma lungimilor tuturor frazelor, nu de numărul de intrări, motiv exact pentru care MaxOutputBytes există atât la decompresor, cât și la predictor. Derivă acea limită din ce pretinde dicționarul imaginii că ar trebui să fie rasterul, iar un stream mincinos eșuează rapid
Decompresorul LZW, predictorul partajat și calea de extracție a imaginilor încărcate arătate aici sunt livrate ca parte a HotPDF Component standard, pentru Delphi și C++Builder, cu referința completă de filtre și DecodeParms pe pagina produsului