PDF tok objekata koji se proširi bez pogreške, ali se i dalje čita kao šum, obično propušta jedan korak: obrnuti ISO 32000-1 Predictor. Kada rječnik /DecodeParms toka sadrži /Predictor 2 ili veći, bajtovi koje vrati FlateDecode nisu izvorni podaci — to su vrijednosti s razlikama po retcima u stilu PNG-a ili vodoravnim razlikama u stilu TIFF-a koje prije svakog smislenog traženja u rječniku trebaju drugu fazu rekonstrukcije. PDFiumPas, izvorna VCL PDF komponenta za Delphi i C++Builder, dodao je tu fazu u v2.16.0 upravo zato što su se tokovi objekata PDF-a 1.5+ proširivali u bajtove s razlikama koje nijedan parser rječnika nije mogao pročitati
Zašto sam FlateDecode nije dovoljan
Sam FlateDecode samo je DEFLATE dekompresija (ISO 32000-1 §7.4.4.1): reproducira bajtove koje je koder predao kompresoru i ništa više. Predictor se nalazi jednu razinu više, u rječniku toka /DecodeParms, i opisuje transformaciju koju je koder primijenio prije kompresije — računanje razlika duge nizove sličnih strukturiranih vrijednosti, poput gusto pakiranih cijelih brojeva u toku unakrsnih referenci ili toku objekata, pretvara u duge nizove malih brojeva koje DEFLATE mnogo bolje komprimira. ISO 32000-1 §7.4.4.3 (Tablica 8) izričito navodi da je poništavanje te transformacije dio dekodiranja filtriranog toka, a ne neobavezno čišćenje, no lako je napisati pomoćnu funkciju FlateDecode koja samo pozove inflate i tu stane
Simptom je prepoznatljiv kada znate što tražiti. Bajtovi s razlikama Predictora nisu nasumični šum — i dalje nose oblik komprimiranog toka, pa naivni parser često prođe nekoliko tokena koji izgledaju valjano prije nego što naiđe na niz bajtova koji nikako ne može biti PDF naziv, broj ili razdjelnik, a različiti retci padaju na različitim pomacima ovisno o tome koliko su se osnovne vrijednosti razlikovale od susjednih. Upravo ta nedosljednost otežava utvrđivanje greške iz jedne neuspješne datoteke: dva PDF-a istog proizvođača mogu se razlikovati samo po tome koje se vrijednosti ponavljaju, pa se jedan raščlani gotovo slučajno, dok drugi potpuno zakaže
Što parametar PDF Predictor zapravo radi?
Unos /Predictor u /DecodeParms govori usklađenom čitaču koju inverziju treba izvršiti, a ISO 32000-1 Tablica 8 definira praktično važne vrijednosti: 1 znači da predviđanje nije primijenjeno, 2 odabire TIFF Predictor 2 (vodoravno računanje razlika), a svaka vrijednost od 10 do 15 odabire predviđanje u stilu PNG-a. Uz njega dolaze još tri ključa — /Colors, /BitsPerComponent i /Columns — koji zajedno opisuju geometriju retka prema kojoj su izračunate razlike, čak i kada tok uopće ne sadrži slikovne podatke: tok objekata nije slika, ali zapisivači PDF-a ponovno koriste isti mehanizam Predictora temeljen na retcima jer delta-pa-deflate bolje komprimira gusto pakirane cijele brojeve i pomake objekata nego izravna kompresija
TIFF Predictor 2 jednostavnija je od dviju shema: svaka se komponenta sprema kao razlika od iste komponente u prethodnom pikselu istog retka, a svaki se redak resetira na lijevom rubu umjesto da preuzima razliku iz retka iznad. Predviđanje PNG-a određenije je jer se stvarni filtar može promijeniti iz retka u redak: svaki redak počinje jednim bajtom oznake — 0 za None, 1 za Sub, 2 za Up, 3 za Average, 4 za Paeth — a ta oznaka, a ne deklarirana vrijednost /Predictor, odlučuje kako se taj redak rekonstruira. /Predictor 12 zapravo je samo koderov nagovještaj da je prednost dao filtru Up, pri kojem se svaki bajt vraća dodavanjem bajta neposredno iznad njega u prethodnom retku, ali ispravan dekoder ipak mora pročitati oznaku u svakom retku umjesto da svugdje pretpostavi Up
Zašto tokovi objekata skrivaju propušteni Predictor?
Tokovi objekata pogoršavaju problem umjesto da ga samo ponavljaju. ISO 32000-1 §7.5.7 dopušta zapisivaču PDF-a 1.5+ da više neizravnih objekata spakira u jedan komprimirani spremnik, /ObjStm, a upravo objekti koji su validatoru najpotrebniji — katalog, /OutputIntents ili XMP tok /Metadata — često prolaze kroz taj spremnik s pridruženim /Predictor 12 jer su dovoljno kratki i repetitivni da imaju koristi od razlika po retcima. Kada nedostaje korak Predictora, proširivanje toka objekata ne podiže pogrešku: proizvodi niz bajtova koji površinski izgleda prihvatljivo, ali se ne tokenizira u očekivane objekte, pa se sadržaj koji je bio spakiran jednostavno ne pojavi. Iscrtavanje to rijetko primijeti jer usklađeni mehanizam za iscrtavanje već rekonstruira podatke s razlikama Predictora prije raspoređivanja; kod koji to primijeti upravo je ona vrsta koda u kojoj se ova greška sakrila — validator, potpisnik ili provjerač inačice koji sam prolazi kroz sirove PDF bajtove kako bi odgovorio na strukturno pitanje, bez pričuvnog postupka kada mu se vlastiti prikaz toka objekata vrati pogrešan
PDFiumPas je prije v2.16.0 naišao upravo na taj kvar. Tokovi objekata izgrađeni s /Predictor 12, što je uobičajen slučaj za zapisivače PDF-a 1.5+, kroz PdfExpandObjectStreams proširivali su se u bajtove s razlikama koje strukturni skener nije mogao raščlaniti, pa su katalog, /OutputIntents i objekti /Metadata spakirani unutra praktički bili nevidljivi provjerama usklađenosti — bez iznimke i upozorenja, samo tiha provjera koja se ponašala kao da ti objekti ne postoje. Dublja mehanika kojom PDFiumPas razrješava tok objekata prema aktivnoj tablici unakrsnih referenci, uključujući hibridne i čiste slučajeve xref toka, zasebno je obrađena u članku o provjeri tokova objekata i xref tokova pomoću PDFiumPas; ovdje opisani korak Predictora izvršava se nakon tog razrješenja, nad bajtovima koje svaki komprimirani objekt stvarno sadrži
Obnavljanje redaka PNG i TIFF Predictor u Pascalu
PDFiumPas poništava razlike u jednoj rutini, PdfApplyPredictor, a njezinu geometrijsku matematiku vrijedi poznavati bez obzira na to pozivate li je ili tu ideju ponovno implementirate u vlastitom kodu Delphi. Širina retka u bajtovima iznosi ceil(Columns × Colors × BitsPerComponent ÷ 8), a širina u bajtovima po pikselu koju koriste oba algoritma iznosi ceil(Colors × BitsPerComponent ÷ 8) — ako pogriješite u bilo kojem zaokruživanju, rekonstrukcija čita preko granice retka umjesto unutar njega. /Predictor manji od 2 ostaje nepromijenjen jer 1 znači da koder nije primijenio nikakvu transformaciju; 2 odabire dolje prikazanu granu TIFF-a, a sve od 10 nadalje prelazi na rekonstrukciju filtra retka PNG-a, gdje bajt oznake na početku svakog retka — a ne deklarirana vrijednost /Predictor — odlučuje kako se taj redak 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;
Što je PDFiumPas promijenio u v2.16.0
Popravak isporučen u PDFiumPas v2.16.0 nalazi se u PdfReadAndDecodeStream, rutini koja čita sirove bajtove toka i dekodira ih za svakog pozivatelja koji treba pregledati PDF strukturu na razini bajtova, uključujući proširivanje tokova objekata; rekonstrukciju pokušava tek nakon potvrde da je /Filter obični FlateDecode, a ne lanac, jer se ulančani filtar na ovoj razini ne može sigurno ispraviti Predictorom. Čitanje vrijednosti /Predictor, /Colors, /BitsPerComponent i /Columns iz rječnika toka također ne zahtijeva opći parser rječnika: PdfDictRefNum pronalazi svaki ključ izravnim traženjem tokena naziva unutar raspona bajtova tog jednog rječnika, što je ovdje sigurno upravo zato što se ta četiri ključa ne mogu ponavljati ili ugnijezditi unutar jednog rječnika toka. Isto traženje tokena naziva mnogo je rizičnije kada se usmjeri na veći ili slabije ograničen dio PDF datoteke, što je tema pratećeg članka o sigurnom parsiranju PDF rječ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;
Prije v2.16.0 tok objekata izgrađen s /Predictor 12 proširivao se u bajtove s razlikama bez podizanja pogreške, pa su katalog, /OutputIntents ili objekt /Metadata spakiran u njemu nestajali iz strukturnih provjera PDFiumPas bez ikakva upozorenja. Nakon popravka isti se tok objekata proširi, a zatim ispravno rekonstruira i objekti u njemu ponovno postaju vidljivi tim provjerama. S popravkom su uvedene i obrambene granice: PdfApplyPredictor sada izravno odbija /Colors veći od 64, /BitsPerComponent veći od 32 i /Columns veći od 2^24 jer te kombinacije opisuju geometrije redaka koje nijedan stvarni proizvođač PDF-a ne treba i uglavnom služe tome 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 vrijedi znati
Rekonstrukcija Predictora u PDFiumPasu ima dva ograničenja koja vrijedi znati prije oslanjanja na nju. Rekonstrukcija TIFF Predictora 2 pokriva samo slučaj s 8 bitova po komponenti; PDF dopušta uža pakiranja, ali podbajtni podaci s TIFF razlikama prolaze bez rekonstrukcije umjesto da se nagađaju, pa se tok koji navodi /Predictor 2 s /BitsPerComponent 1, 2 ili 4 danas neće ispravno dekodirati ovim putem. Predviđanje PNG-a nema to ograničenje — svaki redak donosi vlastitu oznaku filtra i svih se pet definiranih vrsta rekonstruira bez obzira na deklariranu vrijednost /Predictor između 10 i 15, što odgovara stvarnom radu filtriranja u stilu PNG-a: deklarirana je vrijednost bliža naznaci onoga što je koder uglavnom koristio nego obećanju za svaki redak
Izvorni mehanizam za iscrtavanje PDFiuma već ispravno rekonstruira slikovne podatke i podatke tokova sadržaja s razlikama Predictora, upravo zato se datoteka može savršeno iscrtati u bilo kojem uobičajenom pregledniku, dok validator, potpisnik ili provjerač inačice na razini bajtova izgrađen iznad njega iste bajtove čita pogrešno. Ovdje opisano dekodiranje svjesno Predictora podržava značajke PDF/A provjere, strukturnog skeniranja i potpisivanja u proizvodu PDFiumPas, izvornoj VCL PDFium komponenti za Delphi i C++Builder