En PDF-objektstrøm som pakkes ut uten feil, men fortsatt leser som støy, mangler vanligvis ett trinn: å reversere ISO 32000-1 Predictor-en. Når en strøms /DecodeParms-ordbok bærer /Predictor 2 eller høyere, er bytene FlateDecode gir tilbake, ikke de opprinnelige dataene — de er PNG-stil rad-differensierte eller TIFF-stil horisontalt differensierte verdier som trenger en andre rekonstruksjonspassering før noe ordbok-oppslag gir mening. PDFiumPas, det native VCL PDF-komponentbiblioteket for Delphi og C++Builder, la til den rekonstruksjonspasseringen i v2.16.0, spesifikt fordi PDF 1.5+ objektstrømmer utvidet seg til differensierte bytes ingen ordbok-parser kunne lese
Hvorfor FlateDecode alene ikke er nok
FlateDecode i seg selv er bare DEFLATE-dekomprimering (ISO 32000-1 §7.4.4.1): den reproduserer hvilke som helst bytes koderen ga til komprimereren, ikke noe mer. Predictor-en bor ett lag over, i strømmens /DecodeParms-ordbok, og den beskriver en transformasjon koderen anvendte før komprimering — differensiering gjør lange rekker av lignende strukturerte verdier, som de tettpakkede heltallene inne i en kryssreferansestrøm eller en objektstrøm, om til lange rekker av små tall DEFLATE komprimerer langt bedre. ISO 32000-1 §7.4.4.3 (Tabell 8) er eksplisitt på at å angre denne transformasjonen er en del av å dekode en filtrert strøm, ikke en valgfri opprydningspassering, men det er lett å skrive en FlateDecode-hjelper som bare kaller inflate og stopper der
Symptomet er distinkt når man vet å se etter det. Predictor-differensierte bytes er ikke tilfeldig støy — de bærer fortsatt formen på en komprimert strøm, så en naiv parser går ofte forbi noen gyldig-utseende tokens før den treffer en bytesekvens som umulig kan være et PDF-navn, tall, eller skilletegn, og forskjellige rader feiler ved forskjellige forskyvninger avhengig av hvor mye de underliggende verdiene tilfeldigvis skilte seg fra naboene sine. Den inkonsistensen er det som gjør bugen vanskelig å feste til en enkelt mislykket fil: to PDF-er fra samme produsent kan bare skille seg i hvilke verdier som tilfeldigvis gjentar seg, slik at én parser nesten ved en tilfeldighet mens den andre feiler rett ut
Hva gjør PDF Predictor-parameteren egentlig?
/Predictor-oppføringen i /DecodeParms forteller en konform leser hvilken reversering som skal kjøres, og ISO 32000-1 Tabell 8 definerer verdiene som betyr noe i praksis: 1 betyr at ingen prediksjon ble anvendt, 2 velger TIFF Predictor 2 (horisontal differensiering), og enhver verdi fra 10 til 15 velger PNG-stil prediksjon. Tre nøkler til reiser sammen med den — /Colors, /BitsPerComponent, og /Columns — og sammen beskriver de rad-geometrien differensieringen ble beregnet mot, selv når strømmen ikke holder noen bildedata i det hele tatt: en objektstrøm er ikke et bilde, men PDF-skrivere gjenbruker det samme rad-baserte predictor-maskineriet for det fordi delta-så-deflate komprimerer tettpakkede heltall og objektforskyvninger bedre enn å deflate dem rått
TIFF Predictor 2 er den enklere av de to skjemaene: hver komponent lagres som differansen fra den samme komponenten i forrige piksel på samme rad, og hver rad tilbakestilles ved sin venstre kant i stedet for å bære en differanse inn fra raden over. PNG-prediksjon er mer nøye, fordi selve filteret kan endre seg fra rad til rad: hver rad starter med én enkelt tag-byte — 0 for None, 1 for Sub, 2 for Up, 3 for Average, 4 for Paeth — og den tagen, ikke den deklarerte /Predictor-verdien, avgjør hvordan nettopp den raden rekonstrueres. En /Predictor på 12 er egentlig bare koderens hint om at den foretrakk Up-filteret, der hver byte gjenopprettes ved å legge til byten rett over den i forrige rad, men en korrekt dekoder må fortsatt lese tagen på hver rad i stedet for å anta Up gjennomgående
Hvorfor gjør objektstrømmer en glemt Predictor usynlig?
Objektstrømmer forverrer problemet i stedet for bare å gjenta det. ISO 32000-1 §7.5.7 lar en PDF 1.5+-skriver pakke flere indirekte objekter inn i én enkelt komprimert container, en /ObjStm, og det er vanlig at nettopp objektene en validator trenger mest — katalogen, /OutputIntents, eller en XMP-/Metadata-strøm — reiser gjennom den containeren med /Predictor 12 festet, fordi de objektene er korte og repetitive nok til å nyte godt av rad-differensiering. Når predictor-trinnet mangler, kaster ikke utvidelse av objektstrømmen en feil: den produserer en bytesekvens som ser overfladisk plausibel ut, men som ikke tokeniserer til de forventede objektene, så hva som enn var pakket inni, dukker rett og slett ikke opp. Gjengivelse legger sjelden merke til det, fordi en konform gjengivelsesmotor allerede rekonstruerer predictor-differensierte data før det noensinne kommer til layout; koden som legger merke til det, er nøyaktig den typen denne bugen gjemte seg inne i — en validator, signerer, eller versjonssjekker som selv går gjennom de rå PDF-bytene for å svare på et strukturelt spørsmål, uten noen reserveløsning når dens eget syn på objektstrømmen kommer tilbake feil
PDFiumPas traff nøyaktig denne feilen før v2.16.0. Objektstrømmer bygget med /Predictor 12, det vanlige tilfellet for PDF 1.5+-skrivere, utvidet seg gjennom PdfExpandObjectStreams til differensierte bytes strukturskanneren ikke kunne parse, så katalogen, /OutputIntents, og /Metadata-objektene pakket inni, var effektivt usynlige for konformitetsskanninger — intet unntak, ingen advarsel, bare en skanning som stille oppførte seg som om de objektene var fraværende. De dypere mekanikkene i hvordan PDFiumPas løser en objektstrøm mot den aktive kryssreferansetabellen, inkludert hybrid- og rene xref-strøm-tilfellene, dekkes separat i artikkelen om å validere objekt- og xref-strømmer med PDFiumPas; predictor-trinnet beskrevet her kjører etter den løsningen, på bytene hvert komprimerte objekt faktisk inneholder
Å reversere PNG- og TIFF-Predictor-rader i Pascal
PDFiumPas reverserer differensieringen i én enkelt rutine, PdfApplyPredictor, og geometrimatematikken dens er verdt å kjenne til enten man kaller den eller reimplementerer idéen i sin egen Delphi-kode. Radbredden i bytes er ceil(Columns × Colors × BitsPerComponent ÷ 8), og per-piksel-bytebredden begge algoritmene bruker, er ceil(Colors × BitsPerComponent ÷ 8) — få enten avrundingen feil, og rekonstruksjonen leser på tvers av en radgrense i stedet for innenfor én. En /Predictor under 2 lates urørt, ettersom 1 betyr at koderen ikke anvendte noen transformasjon i det hele tatt; 2 velger TIFF-grenen vist nedenfor, og alt fra 10 og oppover faller gjennom til PNG-rad-filter-rekonstruksjon, der tag-byten ved starten av hver rad — ikke den deklarerte /Predictor-verdien — avgjør hvordan nettopp den raden angres
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;
Hva PDFiumPas endret i v2.16.0
Fiksen som ble levert i PDFiumPas v2.16.0, sitter inne i PdfReadAndDecodeStream, rutinen som leser en strøms rå bytes og dekoder dem for hver kaller som trenger å inspisere PDF-struktur på byte-nivå, inkludert objektstrøm-utvidelse; den forsøker bare rekonstruksjon etter å ha bekreftet at /Filter er en ren FlateDecode, aldri en kaskade, fordi et kjedet filter ikke trygt kan predictor-korrigeres på dette laget. Å lese /Predictor, /Colors, /BitsPerComponent, og /Columns tilbake ut av strømordboken trenger heller ingen generell ordbok-parser: PdfDictRefNum finner hver nøkkel ved direkte navn-token-søk innenfor den ene ordbokens byte-rekkevidde, noe som er trygt her nettopp fordi de fire nøklene ikke kan gjentas eller nøstes inne i én enkelt strømordbok. Det samme navn-token-søket er langt mer risikabelt når det rettes mot et større eller mindre avgrenset område av en PDF-fil, noe som er temaet for følgeartikkelen om å parse PDF-ordbøker trygt
// 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;
Før v2.16.0 utvidet en objektstrøm bygget med /Predictor 12 seg til differensierte bytes uten at noen feil ble kastet, så ethvert katalog-, /OutputIntents-, eller /Metadata-objekt pakket inni, forsvant fra PDFiumPas' strukturskanninger uten noen advarsel. Etter fiksen pakkes den samme objektstrømmen ut og rekonstrueres deretter korrekt, og objektene pakket inni blir synlige for de skanningene igjen. Defensive grenser fulgte med fiksen: PdfApplyPredictor avviser nå /Colors over 64, /BitsPerComponent over 32, og /Columns over 2^24 rett ut, fordi de kombinasjonene beskriver rad-geometrier ingen ekte PDF-produsent trenger, og eksisterer hovedsakelig for å få en dekoder til å allokere langt mer minne enn inndata-bytene rettferdiggjør
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;
Grenser verdt å kjenne til
PDFiumPas' predictor-rekonstruksjon har to kanter verdt å kjenne til før man stoler på den. TIFF Predictor 2-rekonstruksjon dekker bare 8-bit-per-komponent-tilfellet; PDF tillater smalere pakninger, men sub-byte TIFF-differensierte data passerer gjennom urekonstruert i stedet for å bli gjettet på, så en strøm som deklarerer /Predictor 2 med /BitsPerComponent 1, 2, eller 4, vil ikke dekode korrekt gjennom denne veien i dag. PNG-prediksjon har ingen slik begrensning — hver rad leverer sin egen filter-tag, og alle fem definerte typer rekonstrueres uansett hva den deklarerte /Predictor-verdien mellom 10 og 15 tilfeldigvis er, noe som stemmer med hvordan PNG-stil filtrering faktisk fungerer: den deklarerte verdien er nærmere et hint om hva koderen mest brukte enn et løfte om hver rad
PDFiums native gjengivelsesmotor rekonstruerer allerede predictor-differensierte bilde- og innholdsstrøm-data korrekt, noe som er nøyaktig grunnen til at en fil kan gjengis perfekt i enhver vanlig fremviser mens en byte-nivå-validator, -signerer, eller -versjonssjekker bygget oppå den, leser de samme bytene feil. Den predictor-bevisste dekodingen beskrevet her støtter PDF/A-valideringen, strukturskanningen, og signerings-funksjonene til PDFiumPas, den native VCL PDFium-komponenten for Delphi og C++Builder