Egy PDF-objektumfolyam, amely hiba nélkül felfúvódik, de mégis zajként olvasható, jellemzően egy lépést hiányol: az ISO 32000-1 Predictor visszafordítását. Amikor egy stream /DecodeParms szótára 2-es vagy magasabb /Predictor-t hordoz, a bájtok, amiket a FlateDecode visszaad, nem az eredeti adatok — PNG-stílusú sorkülönbségesek, vagy TIFF-stílusú vízszintesen differenciált értékek, amelyeknek egy második rekonstrukciós átfutásra van szükségük, mielőtt bármely szótár-keresésnek értelme lenne. A PDFiumPas, a Delphihez és C++Builderhez készült natív VCL PDF-komponens könyvtár, v2.16.0-ban adta hozzá ezt a rekonstrukciós átfutást, kifejezetten azért, mert a PDF 1.5+ objektumfolyamok differenciált bájtokká bővültek, amiket egyetlen szótárelemző sem tudott olvasni
Miért nem elég önmagában a FlateDecode
Maga a FlateDecode csak DEFLATE-dekompresszió (ISO 32000-1 §7.4.4.1): reprodukálja azokat a bájtokat, amiket a kódoló átadott a tömörítőnek, semmi mást. A Predictor egy réteggel feljebb él, a stream /DecodeParms szótárában, és egy transzformációt ír le, amit a kódoló alkalmazott a tömörítés előtt — a differenciálás hasonló strukturált értékek hosszú futamait, mint a szorosan csomagolt egészek egy kereszthivatkozás-streamben vagy egy objektumfolyamban, kis számok hosszú futamaivá alakítja, amiket a DEFLATE sokkal jobban tömörít. Az ISO 32000-1 §7.4.4.3 (8. táblázat) egyértelmű abban, hogy e transzformáció visszavonása egy szűrt stream dekódolásának része, nem egy opcionális takarítási átfutás, mégis könnyű egy FlateDecode-segédfüggvényt írni, amely csak az inflate-et hívja, és ott megáll
A tünet jellegzetes, ha tudod, mit keress. A predictor-differenciált bájtok nem véletlenszerű zaj — még mindig hordozzák egy tömörített stream alakját, így egy naiv elemző gyakran elsétál néhány érvényesnek látszó token mellett, mielőtt egy olyan bájtsorozatba ütközne, amely lehetetlenül nem lehet PDF-név, szám, vagy elválasztó, és különböző sorok különböző eltolásoknál buknak el, attól függően, mennyire tértek el az alapul szolgáló értékek véletlenül a szomszédjaiktól. Ez az inkonzisztencia teszi nehézzé a hiba behatárolását egyetlen elbukott fájlból: két PDF ugyanattól az előállítótól csak abban különbözhet, mely értékek ismétlődnek véletlenül, így az egyik szinte véletlenül elemezhető, míg a másik teljesen elbukik
Mit csinál ténylegesen a PDF Predictor paraméter?
A /DecodeParms-ban lévő /Predictor bejegyzés megmondja egy szabványkövető olvasónak, melyik visszafordítást futtassa, és az ISO 32000-1 8. táblázata definiálja a gyakorlatban számító értékeket: 1 azt jelenti, hogy nem alkalmaztak predikciót, 2 a TIFF Predictor 2-t választja (vízszintes differenciálás), és bármely érték 10-től 15-ig PNG-stílusú predikciót választ. Három további kulcs utazik vele — /Colors, /BitsPerComponent, és /Columns — és együtt leírják a sorgeometriát, amely ellen a differenciálást számították, még akkor is, ha a stream egyáltalán semmilyen képadatot nem tart: egy objektumfolyam nem kép, de a PDF-írók újrahasznosítják ugyanazt a sor-alapú predikciós gépezetet hozzá, mert a delta-majd-deflate szorosabban tömöríti a csomagolt egészeket és objektum-eltolásokat, mint a nyers deflate-elés
A TIFF Predictor 2 a két séma egyszerűbbike: minden komponens az ugyanazon sor előző pixelének ugyanazon komponensétől vett különbségként van tárolva, és minden sor a bal szélén visszaáll, ahelyett hogy egy különbséget hordozna át a fenti sorból. A PNG-predikció specifikusabb, mert a tényleges szűrő soronként változhat: minden sor egyetlen jelzőbájttal kezdődik — 0 None-hoz, 1 Sub-hoz, 2 Up-hoz, 3 Average-hez, 4 Paeth-hez — és az a jelző, nem a deklarált /Predictor érték, dönti el, hogyan rekonstruálódik az adott konkrét sor. Egy 12-es /Predictor valójában csak a kódoló utalása arra, hogy az Up szűrőt részesítette előnyben, ahol minden bájtot úgy állítanak vissza, hogy hozzáadják a közvetlenül fölötte lévő bájtot az előző sorban, de egy helyes dekódolónak még mindig el kell olvasnia a jelzőt minden sornál, ahelyett hogy Up-ot feltételezne mindvégig
Miért teszi láthatatlanná egy elmulasztott Predictort az objektumfolyam?
Az objektumfolyamok súlyosbítják a problémát ahelyett hogy csak megismételnék. Az ISO 32000-1 §7.5.7 lehetővé teszi, hogy egy PDF 1.5+ író több közvetett objektumot csomagoljon egyetlen tömörített konténerbe, egy /ObjStm-be, és gyakori, hogy pontosan azok az objektumok — a katalógus, az /OutputIntents, vagy egy XMP /Metadata stream —, amelyekre egy validátornak leginkább szüksége van, azon a konténeren keresztül utaznak, csatolt 12-es /Predictor-ral, mert ezek az objektumok elég rövidek és ismétlődőek ahhoz, hogy hasznot húzzanak a sor-differenciálásból. Amikor a predictor-lépés hiányzik, az objektumfolyam kibontása nem dob hibát: egy bájtsorozatot állít elő, amely felületesen hihetőnek tűnik, de nem tokenizálódik a várt objektumokba, így bármi is volt csomagolva belül, egyszerűen nem bukkan fel. A renderelés ritkán veszi észre, mert egy szabványkövető renderelő motor már rekonstruálja a predictor-differenciált adatokat, mielőtt bármi elrendezéshez érne; a kód, amely észreveszi, pontosan az a fajta, amiben ez a hiba elrejtőzött — egy validátor, aláíró, vagy verzió-ellenőrző, amely maga járja be a nyers PDF-bájtokat egy strukturális kérdés megválaszolásához, semmilyen visszaesés nélkül, amint saját nézete az objektumfolyamról rosszul jön vissza
A PDFiumPas pontosan ebbe a hibába ütközött v2.16.0 előtt. A 12-es /Predictor-ral épített objektumfolyamok, a gyakori eset a PDF 1.5+ íróknál, differenciált bájtokká bővültek a PdfExpandObjectStreams-en keresztül, amiket a strukturális szkenner nem tudott elemezni, így a katalógus, az /OutputIntents, és a /Metadata objektumok, csomagolva belül, gyakorlatilag láthatatlanok voltak a megfelelőségi vizsgálatok számára — nincs kivétel, nincs figyelmeztetés, csak egy vizsgálat, amely csendben úgy viselkedett, mintha azok az objektumok hiányoznának. Annak mélyebb mechanikáját, hogyan oldja fel a PDFiumPas egy objektumfolyamot az aktív kereszthivatkozás-tábla ellen, beleértve a hibrid és tiszta xref-stream eseteket, külön tárgyalja az objektum- és xref-streamek PDFiumPasszal történő érvényesítéséről szóló cikk; az itt leírt predictor-lépés e feloldás után fut, azokon a bájtokon, amiket minden tömörített objektum ténylegesen tartalmaz
PNG- és TIFF-Predictor sorok visszafordítása Pascalban
A PDFiumPas egyetlen rutinban fordítja vissza a differenciálást, a PdfApplyPredictor-ban, és a geometriai matematikáját érdemes ismerni, akár hívod, akár saját Delphi-kódodban valósítod meg az ötletet. A sor szélessége bájtokban ceil(Columns × Colors × BitsPerComponent ÷ 8), és a mindkét algoritmus által használt pixelenkénti bájtszélesség ceil(Colors × BitsPerComponent ÷ 8) — rontsd el bármelyik kerekítést, és a rekonstrukció egy sorhatáron át olvas, nem azon belül. Egy 2 alatti /Predictor érintetlen marad, mivel 1 azt jelenti, hogy a kódoló egyáltalán nem alkalmazott transzformációt; a 2 kiválasztja az alább látható TIFF-ágat, és bármi 10-től felfelé átesik a PNG sor-szűrő rekonstrukcióba, ahol a minden sor elején lévő jelzőbájt — nem a deklarált /Predictor érték — dönti el, hogyan vonódik vissza az adott konkrét sor
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;
Mit változtatott a PDFiumPas v2.16.0-ban
A javítás, amely a PDFiumPas v2.16.0-jában érkezett, a PdfReadAndDecodeStream-en belül ül, azon a rutinon, amely beolvassa egy stream nyers bájtjait, és dekódolja azokat minden hívó számára, akinek szüksége van a PDF-struktúra bájt-szintű vizsgálatára, beleértve az objektumfolyam-bővítést; csak azután kísérli meg a rekonstrukciót, hogy megerősítette, a /Filter egyszerű FlateDecode, soha nem egy láncolat, mert egy láncolt szűrő nem korrigálható biztonságosan predictorral ezen a rétegen. A /Predictor, /Colors, /BitsPerComponent, és /Columns visszaolvasása a stream szótárból sem igényel egy általános szótárelemzőt: a PdfDictRefNum minden kulcsot közvetlen névtoken-kereséssel talál meg azon az egy szótár bájttartományán belül, ami itt biztonságos, pontosan azért, mert ez a négy kulcs nem ismétlődhet és nem ágyazódhat be egyetlen stream-szótáron belül. Ugyanaz a névtoken-keresés sokkal kockázatosabb, amint egy PDF-fájl nagyobb vagy kevésbé körülhatárolt régiójára irányítják, ami a PDF-szótárak biztonságos elemzéséről szóló kísérőcikk tárgya
// 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;
v2.16.0 előtt egy 12-es /Predictor-ral épített objektumfolyam differenciált bájtokká bővült, hiba dobása nélkül, így bármely katalógus-, /OutputIntents-, vagy /Metadata-objektum, amit belé csomagoltak, eltűnt a PDFiumPas strukturális vizsgálataiból, bármilyen figyelmeztetés nélkül. A javítás után ugyanaz az objektumfolyam felfúvódik, majd helyesen rekonstruálódik, és a belé csomagolt objektumok ismét láthatóvá válnak ezen vizsgálatok számára. Védekező korlátok utaztak a javítással együtt: a PdfApplyPredictor most egyenesen elutasítja a 64 fölötti /Colors-t, a 32 fölötti /BitsPerComponent-et, és a 2^24 fölötti /Columns-t, mert ezek a kombinációk olyan sorgeometriákat írnak le, amikre egyetlen valódi PDF-előállítónak sincs szüksége, és főleg azért léteznek, hogy egy dekódolót sokkal több memória allokálására kényszerítsenek, mint amit a bemeneti bájtok indokolnának
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;
Korlátok, amiket érdemes ismerni
A PDFiumPas predictor-rekonstrukciójának két éle van, amiket érdemes ismerni, mielőtt rá támaszkodsz. A TIFF Predictor 2 rekonstrukció csak a 8-bites-komponensenkénti esetet fedi le; a PDF megenged szűkebb csomagolásokat, de a bájt-alatti TIFF-differenciált adat rekonstrukció nélkül halad át, ahelyett hogy találgatnánk rá, így egy stream, amely 2-es /Predictor-t deklarál 1, 2, vagy 4 /BitsPerComponent-tel, ma nem fog helyesen dekódolódni ezen az útvonalon keresztül. A PNG-predikciónak nincs ilyen korlátozása — minden sor a saját szűrő-jelzőjét szolgáltatja, és mind az öt definiált típus rekonstruálódik, függetlenül attól, mi a deklarált /Predictor érték 10 és 15 között, ami illeszkedik ahhoz, ahogyan a PNG-stílusú szűrés ténylegesen működik: a deklarált érték közelebb áll egy utaláshoz arról, mit használt a kódoló többnyire, mint egy ígérethez minden sorra
A PDFium natív renderelő motorja már helyesen rekonstruálja a predictor-differenciált kép- és tartalomfolyam-adatokat, ami pontosan az oka annak, hogy egy fájl tökéletesen renderelhető bármely közönséges megjelenítőben, miközben egy rá épülő bájt-szintű validátor, aláíró, vagy verzió-ellenőrző rosszul olvassa ugyanazokat a bájtokat. Az itt leírt predictor-tudatos dekódolás támogatja a PDF/A-érvényesítést, a strukturális szkennelést, és az aláírási funkcióit a PDFiumPas-nak, a Delphihez és C++Builderhez készült natív VCL PDFium-komponensnek