PDF objektų srautas, kuris išskleidžiama be klaidos, bet vis tiek skaitomas kaip triukšmas, paprastai praleidžia vieną žingsnį: ISO 32000-1 Predictor apvertimą. Kai srauto /DecodeParms žodynas turi /Predictor 2 ar aukščiau, baitai, kuriuos grąžina FlateDecode, nėrapradiniai duomenys — tai PNG stiliaus eilučių skirtumų arba TIFF stiliaus horizontalių skirtumų reikšmės, kurioms reikia antrojo rekonstrukcijos praėjimo, kad bet koks žodyno paieška turėtų prasmę. PDFiumPas, savoji VCL PDF komponentų biblioteka, skirta Delphi ir C++Builder, pridėjo tą rekonstrukcijos praėjimą v2.16.0 versijoje, specialiai todėl, kad PDF 1.5+ objektų srautai išsiskleidė į skirtumų baitus, kurių joks žodyno analizatorius negalėjo perskaityti
Kodėl vien FlateDecode nepakanka
Pats FlateDecode yra tik DEFLATE išskleidimas (ISO 32000-1 §7.4.4.1): jis atkuria bet kuriuos baitus, kuriuos koduotojas perdavė kompresoriui, ir nieko daugiau. Predictor gyvena vienu sluoksniu aukščiau, srauto /DecodeParms žodyne, ir jis aprašo transformaciją, kurią koduotojas pritaikė prieš suspaudimą — skirtumų skaičiavimas paverčia ilgas panašių struktūrinių reikšmių sekas, pavyzdžiui tankiai supakuotus sveikuosius skaičius kryžminio nuorodos srauto arba objektų srauto viduje, į ilgas mažų skaičių sekas, kurias DEFLATE suspaudžia daug geriau. ISO 32000-1 §7.4.4.3 (8 lentelė) aiškiai sako, kad šios transformacijos atšaukimas yra filtruoto srauto dekodavimo dalis, o ne pasirinktinis valymo praėjimas, tačiau lengva parašyti FlateDecode pagelbę, kuri tik iškviečia inflate ir ten sustoja
Simptomas yra būdingas, kai žinote, ko ieškoti. Predictor skirtumų baitai nėra atsitiktinis triukšmas — jie vis dar neša suspausto srauto formą, todėl naïve analizatorius dažnai pereina kelis teisingai atrodančius tokenus prieš pasiekdamas baitų seką, kuri negali būti PDF pavadinimas, skaičius arba skirtukas, ir skirtingos eilutės nepavyksta skirtinguose poslinkiuose priklausomai nuo to, kiek pagrindinės reikšmės atsitiktinai skyrėsi nuo savo kaimynų. Tas neatitikimas yra tai, kas daro klaidą sunkiai suvaldomą iš vieno nesėkmingo failo: du PDF iš to paties gamintojo gali skirtis tik tuo, kurios reikšmės atsitiktinai kartojasi, todėl vienas analizuojamas beveik atsitiktinai, o kitas visiškai nepavyksta
Ką iš tikrųjų daro PDF Predictor parametras?
/Predictor įrašas /DecodeParms nurodo conforming skaitytuvui, kurį apvertimą paleisti, ir ISO 32000-1 8 lentelė apibrėžia reikšmes, kurios svarbos praktikoje: 1 reiškia, kad jokio prognozavimo nebuvo taikyta, 2 pasirenka TIFF Predictor 2 (horizontalūs skirtumai), o bet kuri reikšmė nuo 10 iki 15 pasirenka PNG stiliaus prognozavimą. Trys papildomi raktai keliauja kartu su juo — /Colors, /BitsPerComponent ir /Columns — ir kartu jie aprašo eilutės geometriją, pagal kurią buvo apskaičiuoti skirtumai, net kai sraute nėra jokio vaizdo duomenų: objektų srautas nėra paveikslėlis, bet PDF rašytojai pakartotinai naudoja tą pačią eilutėmis pagrįstą prognozatoriaus mechaniką jam, nes delta-tada-deflate suspaudžia tankiai supakuotus sveikuosius skaičius ir objektų poslinkius geriau nei juos suspaudžiant žalius
TIFF Predictor 2 yra paprastesnis iš dviejų schemų: kiekvienas komponentas saugomas kaip skirtumas nuo to paties komponento ankstesniame tos pačios eilutės pikseliuje, ir kiekviena eilutė atsistato savo kairiajame krašte, užuot pernešusi skirtumą iš eilutės aukščiau. PNG prognozavimas yra konkretesnis, nes tikras filtras gali keistis iš eilutės į eilutę: kiekviena eilutė prasideda vienu žymos baitu — 0 None, 1 Sub, 2 Up, 3 Average, 4 Paeth — ir ta žyma, o ne deklaruota /Predictor reikšmė, nusprendžia, kaip ta konkreti eilutė bus rekonstruota. /Predictor 12 iš tikrųjų yra tik koduotojo užuomina, kad jis labiau mėgo Up filtrą, kur kiekvienas baitas atstatomas pridėjus baitą tiesiai virš jo ankstesnėje eilutėje, bet teisingas dekoderis vis tiek turi perskaityti žymą kiekvienoje eilutėje, užuot manydamas Up visą laiką
Kodėl objektų srautai padaro praleistą Predictor nematomą?
Objektų srautai problemą sustiprina, užuot ją tiesiog pakartoję. ISO 32000-1 §7.5.7 leidžia PDF 1.5+ rašytojui supakuoti kelis netiesioginius objektus į vieną suspaustą konteinerį, /ObjStm, ir įprasta, kad būtent tie objektai, kurių validatoriui labiausiai reikia — katalogas, /OutputIntents, arba XMP /Metadata srautas — keliauja per tą konteinerį su /Predictor 12, nes tie objektai yra trumpi ir pasikartojantys pakankamai, kad naudinga iš eilučių skirtumų. Kai prognozatoriaus žingsnis trūksta, objektų srauto išplėtimas neiškelia klaidos: jis gamina baitų seką, kuri atrodo paviršutiniškai įtikima, bet netokenizuojama į laukiamus objektus, todėl visa, kas buvo supakuota viduje, tiesiog nepasirodo. Renderavimas retai pastebi, nes conforming renderavimo variklis jau rekonstruoja prognozatoriumi skirtumus turimus duomenis dar prieš pasiekdamas išdėstymą; kodas, kuris pastebi, yra tiksliai tokios rūšies, kuriame ši klaida pasislėpė — validatorius, pasirašytojas arba versijos tikrintojas, kuris pats eina per žalius PDF baitus, kad atsakytų į struktūrinį klausimą, be jokio atsarginio varianto, kai jo paties objektų srauto vaizdas grįžta klaidingas
PDFiumPas pateko į būtent šį nesėkmę prieš v2.16.0. Objektų srautai, sukurti su /Predictor 12, įprastas PDF 1.5+ rašytojų atvejis, buvo išplėsti per PdfExpandObjectStreams į skirtumų baitus, kurių struktūrinis skaitytuvas negalėjo analizuoti, todėl katalogo, /OutputIntents ir /Metadata objektai, supakuoti viduje, buvo faktiškai nematomi atitikties tikrinimams — jokios išimties, jokio įspėjimo, tik tyrimas, kuris tyliai elgėsi taip, lyg tų objektų nebūtų. Giliosios mechanikos, kaip PDFiumPas išsprendžia objektų srautą prieš aktyvų kryžminio nuorodos lentelę, įskaitant hibridinį ir grynąjį xref-srauto atvejus, yra aptariami atskirai straipsnyje apie objektų ir xref srautų patikrinimą su PDFiumPas; prognozatoriaus žingsnis, aprašytas čia, vyksta po to sprendimo, su baitais, kuriuos iš tikrųjų turi kiekvienas suspaustas objektas
PNG ir TIFF Predictor eilučių apvertimas Paskalyje
PDFiumPas apverčia skirtumus vienoje rutinoje, PdfApplyPredictor, ir jos geometrijos matematika verta žinojimo, nesvarbu, ar ją iškviečiate, ar iš naujo įgyvendinate idėją savo Delphi kode. Eilutės plotis baitais yra ceil(Columns × Colors × BitsPerComponent ÷ 8), o vieno pikselio baito plotis, kurį naudoja abu algoritmai, yra ceil(Colors × BitsPerComponent ÷ 8) — gaukite vieną apvalinimą blogai ir rekonstrukcija skaito per eilutės ribą, užuot viduje. /Predictor žemiau 2 lieka neliestas, nes 1 reiškia, kad koduotojas netaikė jokios transformacijos; 2 pasirenka TIFF šaką, parodytą žemiau, o bet kas nuo 10 aukšyn krenta per PNG eilutės-filtro rekonstrukciją, kur žymos baitas kiekvienos eilutės pradžioje — o ne deklaruota /Predictor reikšmė — nusprendžia, kaip ta konkreti eilutė atšaukiama
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 = nėra prognozės, nėra ko atšaukti
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; // atmeskite kenkėjiškas eilučių geometrijas
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; // atkuriamas tik 8 bitų išdėstymas
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 pereina prie toliau pateikto PNG eilučių filtrų atkūrimo
end;
// Tęsiama PdfApplyPredictor viduje, kai Predictor>= 10 (PNG eilučių filtrai)
// Rows:= Length(Src) div (RowLen+ 1); kiekviena eilutė yra 1 baito filtro žyma
// po jos eina RowLen duomenų baitai, dekoduojami iš kairės į dešinę
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; // kairėje esantis baitas
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 (žyma 4) prideda tą iš A, B arba viršuje kairėje esančio baito, kuris yra
// arčiausiai tiesinio prognozuotojo A+ B- C; žyma 0 (None) nukopijuoja
// filtruotą baitą nepakeistą
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
Ką PDFiumPas pakeitė v2.16.0
Pataisa, kuri atsirado PDFiumPas v2.16.0, sėdi PdfReadAndDecodeStream viduje, rutinoje, kuri skaito srauto žalius baitus ir dekoduoja juos kiekvienam kviesčiui, kuriam reikia patikrinti PDF struktūrą baitų lygiu, įskaitant objektų srauto išplėtimą; ji bando rekonstrukciją tik po to, kai patvirtina, kad /Filter yra grynas FlateDecode, niekada kaskadas, nes grandininio filtro negalima saugiai prognozatoriumi ištaisyti šiame sluoksnyje. /Predictor, /Colors, /BitsPerComponent ir /Columns nuskaitymas iš srauto žodyno nereikalauja bendro žodyno analizatoriaus: PdfDictRefNum randa kiekvieną raktą pagal tiesioginę pavadinimo-tokeno paiešką to vieno žodyno baitų diapazone, o tai čia saugu būtent todėl, kad tie keturi raktai negali kartotis arba lizduoti viename srauto žodyne. Ta pati pavadinimo-tokeno paieška yra daug rizikingesnė, kai ji nukreipta į didesnį arba mažiau apribotą PDF failo regioną, kas yra lydinčio straipsnio apie saugų PDF žodynų analizavimą tema
// PdfReadAndDecodeStream viduje, iškart po to, kai jau įvykdyta PdfInflate():
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;
Prieš v2.16.0 objektų srautas, sukurtas su /Predictor 12, išsiskleidė į skirtumų baitus nekeldamas klaidos, todėl bet koks katalogo, /OutputIntents ar /Metadata objektas, supakuotas viduje, dingdavo iš PDFiumPas struktūrinių tikrinimų be jokio įspėjimo. Po pataisos tas pats objektų srautas išsiskleidžia ir tada teisingai rekonstruojamas, o viduje supakuoti objektai vėl tampa matomi tiems tikrinimams. Kartu su pataisa keliavo gynybinės ribos: PdfApplyPredictor dabar tiesiogiai atmeta /Colors virš 64, /BitsPerComponent virš 32 ir /Columns virš 2^24, nes tos kombinacijos aprašo eilutės geometrijas, kurių jokiam realiam PDF gamintojui nereikia, ir egzistuoja mainly kad priverstų dekoderį skirti daug daugiau atminties, nei įvesties baitai pateisina
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;
Ribos, vertas žinoti
PDFiumPas prognozatoriaus rekonstrukcija turi du kraštus, vertus žinoti prieš ja pasiklioviant. TIFF Predictor 2 rekonstrukcija apima tik 8 bitų vienam komponentui atvejį; PDF leidžia siauresnius pakavimus, bet sub-baito TIFF skirtumų duomenys praeina nerekonstruoti, užuot spėliojami, todėl srautas, deklaruojantis /Predictor 2 su /BitsPerComponent 1, 2 arba 4, šiandien per šį kelią teisingai nedekoduos. PNG prognozavimui tokio apribojimo nėra — kiekviena eilutė pateikia savo filtro žymą, ir visi penki apibrėžti tipai rekonstruojami neatsižvelgiant į tai, kokia yra deklaruota /Predictor reikšmė tarp 10 ir 15, kas atitinka, kaip PNG stiliaus filtravimas iš tikrųjų veikia: deklaruota reikšmė yra arčiau užuominos apie tai, ką koduotojas daugiausia naudojo, nei pažado apie kiekvieną eilutę
PDFium savoji renderavimo mašina jau teisingai rekonstruoja prognozatoriumi skirtumų vaizdo ir turinio-srauto duomenis, kas yra būtent tai, kodėl failas gali tobulai atvaizduoti bet kuriame įprastame žiūrovėje, o ant jo pastatytas baitų lygio validatorius, pasirašytojas arba versijos tikrintojas skaito tuos pačius baitus neteisingai. Čia aprašytas prognozatoriumi žinomas dekodavimas palaiko PDFiumPas, savajį VCL PDFium komponentą Delphi ir C++Builder, PDF/A patikros, struktūrinio skenavimo ir pasirašymo funkcijas