Prúd objektov PDF, ktorý sa bez chyby rozbalí, no aj tak sa číta ako šum, zvyčajne postráda jeden krok: zvrátenie Predictora podľa ISO 32000-1. Keď slovník /DecodeParms prúdu nesie /Predictor 2 alebo vyššie, bajty, ktoré FlateDecode vráti, nie sú pôvodné dáta — sú to hodnoty diferencované po riadkoch v štýle PNG alebo horizontálne diferencované v štýle TIFF, ktoré potrebujú druhý rekonštrukčný prechod ešte predtým, než má akékoľvek vyhľadávanie v slovníku zmysel. PDFiumPas, natívna knižnica komponentu VCL PDF pre Delphi a C++Builder, pridala tento rekonštrukčný prechod vo v2.16.0, konkrétne preto, lebo prúdy objektov PDF 1.5+ sa rozbaľovali do diferencovaných bajtov, ktoré žiadny parser slovníkov nedokázal prečítať
Prečo samotné FlateDecode nestačí
Samotné FlateDecode je iba dekompresia DEFLATE (ISO 32000-1 §7.4.4.1): reprodukuje akékoľvek bajty, aké enkodér odovzdal kompresoru, nič viac. Predictor žije o vrstvu vyššie, v slovníku /DecodeParms prúdu, a opisuje transformáciu, ktorú enkodér aplikoval ešte pred kompresiou — diferencovanie mení dlhé série podobných štruktúrovaných hodnôt, ako sú tesne zbalené celé čísla vnútri prúdu krížových odkazov alebo prúdu objektov, na dlhé série malých čísel, ktoré DEFLATE komprimuje oveľa lepšie. ISO 32000-1 §7.4.4.3 (tabuľka 8) je explicitná v tom, že zrušenie tejto transformácie je súčasťou dekódovania filtrovaného prúdu, nie voliteľný čistiaci prechod, no aj tak je ľahké napísať pomocníka FlateDecode, ktorý iba volá inflate a tam sa zastaví
Príznak je výrazný, hneď ako viete, čo hľadať. Bajty diferencované Predictorom nie sú náhodný šum — stále nesú tvar komprimovaného prúdu, takže naivný parser často prejde popri niekoľkých na prvý pohľad platných tokenoch skôr, než narazí na sekvenciu bajtov, ktorá nemôže byť menom, číslom, alebo oddeľovačom PDF, a rôzne riadky zlyhajú na rôznych posunoch podľa toho, ako veľmi sa podkladové hodnoty náhodou líšili od svojich susedov. Práve táto nekonzistentnosť sťažuje pripísanie tejto chyby jedinému zlyhávajúcemu súboru: dve PDF od toho istého producenta sa môžu líšiť iba v tom, ktoré hodnoty sa náhodou opakujú, takže jedno sa naparsuje takmer náhodou, zatiaľ čo druhé úplne zlyhá
Čo v skutočnosti robí parameter Predictor v PDF?
Záznam /Predictor v /DecodeParms hovorí normu rešpektujúcej čítačke, ktoré zvrátenie spustiť, a tabuľka 8 ISO 32000-1 definuje hodnoty, na ktorých v praxi záleží: 1 znamená, že sa neaplikovala žiadna predikcia, 2 vyberá TIFF Predictor 2 (horizontálne diferencovanie), a ktorákoľvek hodnota od 10 do 15 vyberá predikciu v štýle PNG. Tri ďalšie kľúče cestujú spolu s ním — /Colors, /BitsPerComponent, a /Columns — a spolu opisujú geometriu riadkov, voči ktorej bolo diferencovanie vypočítané, aj vtedy, keď prúd nenesie žiadne obrazové dáta: prúd objektov nie je obrázok, no zapisovače PDF znova používajú tú istú riadkovo založenú mašinériu predictora preň, pretože delta-a-potom-deflate komprimuje tesne zbalené celé čísla a posuny objektov lepšie, než ich deflatovať surové
TIFF Predictor 2 je jednoduchšia z týchto dvoch schém: každá zložka je uložená ako rozdiel od tej istej zložky v predchádzajúcom pixeli na tom istom riadku, a každý riadok sa resetuje na svojom ľavom okraji namiesto toho, aby si niesol rozdiel z riadka nad ním. Predikcia PNG je precíznejšia, pretože samotný filter sa môže meniť riadok od riadka: každý riadok začína jediným tagovacím bajtom — 0 pre None, 1 pre Sub, 2 pre Up, 3 pre Average, 4 pre Paeth — a práve tento tag, nie deklarovaná hodnota /Predictor, rozhoduje o tom, ako sa daný konkrétny riadok znova zostaví. /Predictor hodnoty 12 je v skutočnosti iba nápoveda enkodéra, že uprednostnil filter Up, kde sa každý bajt obnoví pridaním bajtu priamo nad ním v predchádzajúcom riadku, no správny dekodér stále musí prečítať tag na každom riadku namiesto toho, aby predpokladal Up naprieč všetkými
Prečo prúdy objektov robia zmeškaný Predictor neviditeľným?
Prúdy objektov problém znásobujú namiesto toho, aby ho iba opakovali. ISO 32000-1 §7.5.7 dovoľuje zapisovaču PDF 1.5+ zbaliť viacero nepriamych objektov do jediného komprimovaného kontajnera, /ObjStm, a je bežné, že práve tie objekty, ktoré validátor najviac potrebuje — katalóg, /OutputIntents, alebo prúd XMP /Metadata — cestujú cez tento kontajner s pripojeným /Predictor 12, pretože tieto objekty sú dosť krátke a opakujúce sa na to, aby z riadkového diferencovania profitovali. Keď krok predictora chýba, rozbalenie prúdu objektov nevyvolá chybu: vyprodukuje sekvenciu bajtov, ktorá vyzerá povrchne vierohodne, no netokenizuje sa do očakávaných objektov, takže čokoľvek bolo zbalené vnútri, sa jednoducho neobjaví. Vykresľovanie si to málokedy všimne, pretože normu rešpektujúci vykresľovací engine už zrekonštruuje dáta diferencované predictorom skôr, než sa vôbec dostanú k rozloženiu; kód, ktorý si to všimne, je presne ten typ, v ktorom sa táto chyba skrývala — validátor, signer, alebo kontrolór verzie, ktorý sám prechádza surové bajty PDF na zodpovedanie štrukturálnej otázky, bez akéhokoľvek záložného mechanizmu, keď sa jeho vlastný pohľad na prúd objektov vráti nesprávny
PDFiumPas narazil presne na toto zlyhanie ešte pred v2.16.0. Prúdy objektov postavené s /Predictor 12, bežný prípad pre zapisovače PDF 1.5+, sa cez PdfExpandObjectStreams rozbaľovali do diferencovaných bajtov, ktoré štrukturálny skener nedokázal naparsovať, takže katalóg, /OutputIntents, a objekty /Metadata zbalené vnútri boli pre skeny zhody efektívne neviditeľné — žiadna výnimka, žiadne varovanie, iba sken, ktorý sa ticho správal, akoby tieto objekty vôbec neexistovali. Hlbšia mechanika toho, ako PDFiumPas rieši prúd objektov voči aktívnej tabuľke krížových odkazov, vrátane hybridných a čistých prípadov prúdov xref, je opísaná samostatne v článku o validácii prúdov objektov a xref pomocou PDFiumPas; krok predictora opísaný tu beží po tomto vyriešení, na bajtoch, ktoré každý komprimovaný objekt v skutočnosti obsahuje
Zvrátenie riadkov Predictor PNG a TIFF v Pascale
PDFiumPas zvracia diferencovanie v jedinej rutine, PdfApplyPredictor, a jej geometrická matematika sa oplatí poznať bez ohľadu na to, či ju voláte, alebo túto myšlienku znova implementujete vo vlastnom kóde Delphi. Šírka riadka v bajtoch je ceil(Columns × Colors × BitsPerComponent ÷ 8) a šírka bajtu na pixel, akú používajú oba algoritmy, je ceil(Colors × BitsPerComponent ÷ 8) — pomýlite jedno z týchto zaokrúhlení a rekonštrukcia bude čítať naprieč hranicou riadka namiesto vnútri jedného. /Predictor pod 2 sa ponechá nedotknutý, keďže 1 znamená, že enkodér neaplikoval žiadnu transformáciu; 2 vyberá vetvu TIFF zobrazenú nižšie, a čokoľvek od 10 nahor spadne do rekonštrukcie riadkových filtrov PNG, kde tagovací bajt na začiatku každého riadka — nie deklarovaná hodnota /Predictor — rozhoduje o tom, ako sa daný konkrétny riadok zvráti
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;
Čo PDFiumPas zmenil vo v2.16.0
Oprava, ktorá vyšla v PDFiumPas v2.16.0, sedí vnútri PdfReadAndDecodeStream, rutiny, ktorá číta surové bajty prúdu a dekóduje ich pre každého volajúceho, ktorý potrebuje inšpekovať štruktúru PDF na úrovni bajtov, vrátane rozbalenia prúdu objektov; pokúsi sa o rekonštrukciu iba po potvrdení, že /Filter je holé FlateDecode, nikdy nie kaskáda, pretože reťazený filter sa na tejto vrstve nedá bezpečne opraviť predictorom. Prečítanie /Predictor, /Colors, /BitsPerComponent, a /Columns späť zo slovníka prúdu tiež nepotrebuje všeobecný parser slovníkov: PdfDictRefNum nájde každý kľúč priamym vyhľadávaním tokenu mena vnútri bajtového rozsahu tohto jedného slovníka, čo je tu bezpečné práve preto, lebo tieto štyri kľúče sa nemôžu opakovať ani vnárať vnútri jedného slovníka prúdu. To isté vyhľadávanie tokenu mena je oveľa riskantnejšie vo chvíli, keď je namierené na väčšiu alebo menej ohraničenú oblasť súboru PDF, čo je predmetom sprievodného článku o bezpečnom parsovaní slovníkov PDF
// 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;
Pred v2.16.0 sa prúd objektov postavený s /Predictor 12 rozbalil do diferencovaných bajtov bez vyvolania chyby, takže akýkoľvek katalóg, /OutputIntents, alebo objekt /Metadata zbalený vnútri sa zo štrukturálnych skenov PDFiumPas stratil bez akéhokoľvek varovania. Po oprave sa ten istý prúd objektov rozbalí a potom správne zrekonštruuje, a objekty zbalené vnútri sa pre tieto skeny znova stanú viditeľnými. Spolu s opravou pribudli aj obranné hranice: PdfApplyPredictor teraz rovno odmieta /Colors nad 64, /BitsPerComponent nad 32, a /Columns nad 2^24, pretože tieto kombinácie opisujú geometrie riadkov, aké žiadny skutočný producent PDF nepotrebuje, a existujú najmä preto, aby prinútili dekodér alokovať oveľa viac pamäte, než vstupné bajty odôvodňujú
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;
Limity, ktoré sa oplatí poznať
Rekonštrukcia predictora v PDFiumPas má dve hranice, ktoré sa oplatí poznať ešte pred tým, než sa na ňu spoľahnete. Rekonštrukcia TIFF Predictor 2 pokrýva iba prípad 8 bitov na zložku; PDF povoľuje aj užšie zbalenia, no dáta diferencované TIFF pod úrovňou bajtu prejdú nezrekonštruované namiesto toho, aby sa hádali, takže prúd deklarujúci /Predictor 2 s /BitsPerComponent 1, 2, alebo 4 sa touto cestou dnes nedekóduje správne. Predikcia PNG takéto obmedzenie nemá — každý riadok si dodáva vlastný tag filtra, a všetkých päť definovaných typov sa rekonštruuje bez ohľadu na to, akou náhodou je deklarovaná hodnota /Predictor medzi 10 a 15, čo zodpovedá tomu, ako filtrovanie v štýle PNG v skutočnosti funguje: deklarovaná hodnota je bližšie k nápovede o tom, čo enkodér väčšinou použil, než k sľubu o každom riadku
Natívny vykresľovací engine PDFium už dáta obrázka a obsahového prúdu diferencované predictorom rekonštruuje správne, čo je presne dôvod, prečo sa súbor môže v akomkoľvek obyčajnom prehliadači vykresliť dokonale, zatiaľ čo validátor, signer, alebo kontrolór verzie na úrovni bajtov postavený nad ním prečíta tie isté bajty nesprávne. Dekódovanie vedomé si predictora opísané tu podkladá validáciu PDF/A, štrukturálne skenovanie, a funkcie podpisovania PDFiumPas, natívneho komponentu PDFium VCL pre Delphi a C++Builder