PDF objektni tok koji se uspešno raspakuje, ali se i dalje čita kao šum, obično propušta jedan korak: poništavanje prediktora prema standardu ISO 32000-1. Kada rečnik /DecodeParms toka sadrži /Predictor vrednost 2 ili veću, bajtovi koje vrati FlateDecode nisu izvorni podaci — to su vrednosti sa razlikama po redovima u PNG stilu ili horizontalnim razlikama u TIFF stilu, kojima je potrebna druga faza rekonstrukcije pre nego što bilo kakvo traženje rečnika dobije smisao. PDFiumPas, matična VCL biblioteka za PDF komponente za Delphi i C++Builder, dodala je tu fazu rekonstrukcije u verziji v2.16.0, upravo zato što su se objektni tokovi PDF-a 1.5+ raspakivali u bajtove sa razlikama koje nijedan parser rečnika nije mogao da pročita
Zašto FlateDecode sam po sebi nije dovoljan
Sam FlateDecode obavlja samo DEFLATE dekompresiju prema ISO 32000-1 §7.4.4.1: reprodukuje tačno one bajtove koje je koder prosledio kompresoru i ništa više. Predictor se nalazi jedan sloj iznad, u rečniku /DecodeParms toka, i opisuje transformaciju koju je koder primenio pre kompresije — računanje razlika pretvara dugačke nizove sličnih strukturisanih vrednosti, kao što su zbijeni celi brojevi u unakrsnom referentnom ili objektnom toku, u dugačke nizove malih brojeva koje DEFLATE mnogo bolje kompresuje. ISO 32000-1 §7.4.4.3 (Tabela 8) izričito navodi da je poništavanje te transformacije deo dekodiranja filtriranog toka, a ne opciono naknadno čišćenje, ali je lako napisati pomoćnu funkciju za FlateDecode koja samo pozove inflate i tu stane
Simptom je prepoznatljiv kada znate šta da tražite. Bajtovi sa razlikama koje je napravio Predictor nisu nasumičan šum — i dalje nose oblik kompresovanog toka, pa naivan parser često prođe nekoliko tokena koji izgledaju ispravno pre nego što naiđe na niz bajtova koji nikako ne može biti PDF ime, broj ili graničnik, dok različiti redovi otkazuju na različitim pomerajima u zavisnosti od toga koliko su se izvorne vrednosti razlikovale od susednih. Upravo ta nedoslednost otežava pronalaženje greške na osnovu jedne neispravne datoteke: dva PDF-a istog proizvođača mogu se razlikovati samo po tome koje se vrednosti ponavljaju, pa se jedan gotovo slučajno parsira, dok drugi potpuno otkaže
Šta parametar PDF Predictor zaista radi
Unos /Predictor u /DecodeParms govori usaglašenom čitaču koju inverziju treba primeniti, a Tabela 8 standarda ISO 32000-1 definiše praktično važne vrednosti: 1 znači da predikcija nije primenjena, 2 bira TIFF Predictor 2 (horizontalno računanje razlika), a svaka vrednost od 10 do 15 bira predikciju u PNG stilu. Uz njega dolaze još tri ključa — /Colors, /BitsPerComponent i /Columns — koji zajedno opisuju geometriju redova nad kojom su izračunate razlike, čak i kada tok uopšte ne sadrži slikovne podatke: objektni tok nije slika, ali PDF pisači ponovo koriste isti prediktor zasnovan na redovima jer kompresija delta-vrednosti pa deflate bolje sabija zbijene cele brojeve i pomeraje objekata nego direktna deflate kompresija
TIFF Predictor 2 je jednostavnija od dve šeme: svaka komponenta se čuva kao razlika u odnosu na istu komponentu prethodnog piksela u istom redu, a svaki red počinje iznova na levoj ivici umesto da prenosi razliku iz reda iznad. PNG predikcija je složenija jer se stvarni filter može menjati od reda do reda: svaki red počinje jednim bajtom oznake — 0 za None, 1 za Sub, 2 za Up, 3 za Average, 4 za Paeth — i upravo ta oznaka, a ne deklarisana vrednost /Predictor, određuje kako se taj red rekonstruiše. Vrednost /Predictor 12 samo nagoveštava da je koder davao prednost filteru Up, kod kog se svaki bajt vraća dodavanjem odgovarajućeg bajta iz prethodnog reda, ali ispravan dekoder ipak mora da pročita oznaku svakog reda umesto da svuda pretpostavi Up
Zašto propušteni Predictor ostaje nevidljiv u objektnim tokovima
Objektni tokovi dodatno usložnjavaju problem umesto da ga samo ponove. ISO 32000-1 §7.5.7 omogućava pisaču PDF-a 1.5+ da više indirektnih objekata spakuje u jedan kompresovani kontejner, /ObjStm, a upravo objekti koji su validatoru najpotrebniji — katalog, /OutputIntents ili XMP tok /Metadata — često prolaze kroz taj kontejner sa dodatom vrednošću /Predictor 12, jer su dovoljno kratki i ponavljajući da imaju koristi od razlika po redovima. Kada korak prediktora nedostaje, raspakivanje objektnog toka ne prijavljuje grešku: dobija se niz bajtova koji površno deluje uverljivo, ali se ne može tokenizovati u očekivane objekte, pa se sadržaj kontejnera jednostavno ne pojavi. Renderovanje to retko primeti jer usaglašeni mehanizam za renderovanje već rekonstruiše podatke sa razlikama prediktora pre nego što stigne do rasporeda; grešku primećuje upravo ona vrsta koda u kojoj se ovaj problem sakrio — validator, potpisivač ili proverivač verzije koji sam prolazi kroz sirove PDF bajtove da bi odgovorio na strukturno pitanje, bez rezervne mogućnosti kada njegov prikaz objektnog toka ispadne pogrešan
PDFiumPas je upravo na ovaj problem naišao pre verzije v2.16.0. Objektni tokovi napravljeni sa /Predictor 12, što je uobičajeno kod pisača PDF-a 1.5+, kroz PdfExpandObjectStreams su se raspakivali u bajtove sa razlikama koje strukturni skener nije mogao da parsira, pa su katalog, /OutputIntents i objekti /Metadata unutar toka praktično postajali nevidljivi proverama usaglašenosti — bez izuzetka i bez upozorenja, samo sa tihim ponašanjem kao da ti objekti ne postoje. Detaljniji mehanizam kojim PDFiumPas razrešava objektni tok prema aktivnoj unakrsnoj referentnoj tabeli, uključujući hibridne i čiste xref tokove, obrađen je zasebno u članku o proveri objektnih i xref tokova pomoću PDFiumPas-a; ovde opisana faza prediktora izvršava se posle tog razrešavanja, nad bajtovima koje svaki kompresovani objekat stvarno sadrži
Rekonstrukcija PNG i TIFF Predictor redova u Pascalu
PDFiumPas poništava razlike u jednoj rutini, PdfApplyPredictor, a njenu geometrijsku računicu vredi razumeti bilo da je pozivate ili ponovo implementirate u sopstvenom Delphi kodu. Širina reda u bajtovima iznosi ceil(Columns × Colors × BitsPerComponent ÷ 8), a širina bajtova po pikselu koju koriste oba algoritma iznosi ceil(Colors × BitsPerComponent ÷ 8) — ako pogrešite bilo koje zaokruživanje, rekonstrukcija će čitati preko granice reda umesto unutar njega. Vrednost /Predictor manja od 2 ostavlja se nepromenjena jer 1 znači da koder nije primenio nikakvu transformaciju; 2 bira TIFF granu prikazanu ispod, a svaka vrednost od 10 naviše prelazi na rekonstrukciju PNG filtera po redovima, gde bajt oznake na početku svakog reda — a ne deklarisana vrednost /Predictor — određuje kako se taj red poništava
function PdfApplyPredictor(const Src: TBytes;
Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
RowLen, Bpp, R, I: Integer;
begin
Result:= Src;
if Predictor< 2 then
Exit; // 1 = no prediction, nothing to undo
if Colors<= 0 then Colors:= 1;
if Bpc<= 0 then Bpc:= 8;
if Columns<= 0 then Columns:= 1;
if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
Exit; // reject hostile row geometries
RowLen:= (Columns* Colors* Bpc+ 7) div 8; // ceil(), per ISO 32000-1 Table 8
Bpp:= (Colors* Bpc+ 7) div 8;
if Predictor= 2 then
begin
if Bpc<> 8 then
Exit; // only the 8-bit layout is reconstructed
Result:= Copy(Src, 0, Length(Src));
R:= 0;
while R+ RowLen<= Length(Result) do
begin
for I:= R+ Bpp to R+ RowLen- 1 do
Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
Inc(R, RowLen);
end;
Exit;
end;
// Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
SrcOfs:= R* (RowLen+ 1);
DstOfs:= R* RowLen;
Tag:= Src[SrcOfs];
Inc(SrcOfs);
for I:= 0 to RowLen- 1 do
begin
if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0; // byte to the left
if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0; // byte above
case Tag of
1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A); // Sub
2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B); // Up
3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
// Paeth (tag 4) adds whichever of A, B or the byte above-left sits
// closest to the linear predictor A+ B- C; tag 0 (None) copies the
// filtered byte through unchanged
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
Šta je PDFiumPas promenio u v2.16.0
Ispravka isporučena u PDFiumPas-u v2.16.0 nalazi se u rutini PdfReadAndDecodeStream, koja čita sirove bajtove toka i dekodira ih za svakog pozivaoca kome je potrebna provera PDF strukture na nivou bajtova, uključujući raspakivanje objektnih tokova; rekonstrukciju pokušava tek pošto potvrdi da je /Filter samostalni FlateDecode, a ne lanac filtera, jer se ulančani filter na ovom sloju ne može bezbedno korigovati prediktorom. Čitanje vrednosti /Predictor, /Colors, /BitsPerComponent i /Columns iz rečnika toka takođe ne zahteva opšti parser rečnika: PdfDictRefNum pronalazi svaki ključ direktnim pretraživanjem tokena imena unutar opsega bajtova tog rečnika, što je ovde bezbedno upravo zato što se ta četiri ključa ne mogu ponoviti niti ugnjezditi u jednom rečniku toka. Isto pretraživanje tokena imena mnogo je rizičnije kada se usmeri na veći ili slabije ograničen opseg PDF datoteke, što je tema pratećeg članka o bezbednom parsiranju PDF rečnika
// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
Inflated:= PdfInflate(Raw);
Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
if Predictor>= 2 then
begin
PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
end
else
Result:= Inflated;
end;
Pre v2.16.0, objektni tok napravljen sa /Predictor 12 prelazio je u bajtove sa razlikama bez ikakve prijavljene greške, pa su katalog, /OutputIntents ili objekat /Metadata spakovan unutar njega nestajali iz strukturnih skeniranja PDFiumPas-a bez upozorenja. Posle ispravke isti objektni tok se prvo raspakuje, a zatim pravilno rekonstruiše, pa objekti u njemu ponovo postaju vidljivi tim skeniranjima. Uz ispravku su uvedene i zaštitne granice: PdfApplyPredictor sada odmah odbija /Colors veći od 64, /BitsPerComponent veći od 32 i /Columns veći od 2^24, jer takve kombinacije opisuju geometrije redova koje nijedan stvarni proizvođač PDF-a nema razloga da koristi i uglavnom služe da dekoderu nametnu mnogo veću alokaciju memorije nego što ulazni bajtovi opravdavaju
var
Pdf: TPdf;
Report: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'incoming.pdf';
Pdf.Active := True;
Report := Pdf.ValidatePdfA;
if not Report.IsCompliant then
LogNonCompliance(Report); // caller-supplied handler
finally
Pdf.Free;
end;
end;
Ograničenja koja treba imati na umu
Rekonstrukcija prediktora u PDFiumPas-u ima dve važne granice koje treba poznavati pre oslanjanja na nju. TIFF Predictor 2 se rekonstruiše samo u slučaju 8 bita po komponenti; PDF dozvoljava uža pakovanja, ali podbajtni TIFF podaci sa razlikama prolaze bez rekonstrukcije umesto da se nagađaju, pa se tok koji navodi /Predictor 2 uz /BitsPerComponent 1, 2 ili 4 danas neće pravilno dekodirati ovom putanjom. PNG predikcija nema to ograničenje — svaki red donosi sopstvenu oznaku filtera i svih pet definisanih tipova se rekonstruiše bez obzira na to koja je deklarisana vrednost /Predictor između 10 i 15, što odgovara stvarnom radu PNG filtera: deklarisana vrednost je bliža nagoveštaju o tome koji je filter koder uglavnom koristio nego obećanju da je isti filter primenjen na svaki red
PDFium-ov matični mehanizam za renderovanje već pravilno rekonstruiše podatke slike i tokova sadržaja sa razlikama prediktora, upravo zato datoteka može savršeno da se prikaže u svakom uobičajenom pregledaču dok validator, potpisivač ili proverivač verzije zasnovan na nivou bajtova iste bajtove čita pogrešno. Ovde opisano dekodiranje sa podrškom za prediktor predstavlja osnovu za PDF/A validaciju, strukturno skeniranje i potpisivanje u okviru PDFiumPas-a, matične VCL PDFium komponente za Delphi i C++Builder