Een PDF-objectstream die zonder fout opblaast maar toch als ruis leest, mist meestal één stap: het omkeren van de ISO 32000-1 Predictor. Wanneer de /DecodeParms-dictionary van een stream /Predictor 2 of hoger draagt, zijn de bytes die FlateDecode teruggeeft niet de oorspronkelijke data — het zijn PNG-stijl rij-gedifferentieerde of TIFF-stijl horizontaal gedifferentieerde waarden die een tweede reconstructieronde nodig hebben voordat enige dictionary-opzoeking zinnig is. PDFiumPas, de native VCL-PDF-componentbibliotheek voor Delphi en C++Builder, voegde die reconstructieronde toe in v2.16.0, specifiek omdat PDF 1.5+-objectstreams uitbreidden naar gedifferentieerde bytes die geen enkele dictionary-parser kon lezen
Waarom FlateDecode alleen niet genoeg is
FlateDecode zelf is alleen DEFLATE-decompressie (ISO 32000-1 §7.4.4.1): het reproduceert welke bytes de encoder ook aan de compressor gaf, niets meer. De Predictor zit een laag hoger, in de /DecodeParms-dictionary van de stream, en beschrijft een transformatie die de encoder toepaste vóór compressie — differencing verandert lange reeksen gelijksoortig gestructureerde waarden, zoals de strak gepakte gehele getallen binnen een cross-reference-stream of een objectstream, in lange reeksen kleine getallen die DEFLATE veel beter comprimeert. ISO 32000-1 §7.4.4.3 (Tabel 8) is expliciet dat het ongedaan maken van deze transformatie deel uitmaakt van het decoderen van een gefilterde stream, geen optionele opschoonronde, en toch is het gemakkelijk om een FlateDecode-helper te schrijven die alleen inflate aanroept en daarbij stopt
Het symptoom is kenmerkend zodra u weet waarnaar te zoeken. Predictor-gedifferentieerde bytes zijn geen willekeurige ruis — ze dragen nog steeds de vorm van een gecomprimeerde stream, dus een naïeve parser loopt vaak voorbij een paar geldig ogende tokens voordat het een bytesequentie tegenkomt die onmogelijk een PDF-naam, getal, of scheidingsteken kan zijn, en verschillende rijen falen op verschillende offsets, afhankelijk van hoeveel de onderliggende waarden toevallig van hun buren verschilden. Die inconsistentie is precies wat de bug moeilijk vast te pinnen maakt aan de hand van één enkel falend bestand: twee PDF's van dezelfde producent kunnen alleen verschillen in welke waarden toevallig herhalen, dus de ene parst bijna per ongeluk terwijl de andere regelrecht faalt
Wat doet de PDF-Predictor-parameter eigenlijk?
Het /Predictor-item in /DecodeParms vertelt een conforme lezer welke omkering te draaien, en ISO 32000-1 Tabel 8 definieert de waarden die er in de praktijk toe doen: 1 betekent dat er geen predictie is toegepast, 2 selecteert TIFF Predictor 2 (horizontale differencing), en elke waarde van 10 tot en met 15 selecteert PNG-stijl predictie. Nog drie sleutels reizen erlangs mee — /Colors, /BitsPerComponent, en /Columns — en samen beschrijven ze de rijgeometrie waartegen de differencing is berekend, zelfs wanneer de stream helemaal geen beelddata bevat: een objectstream is geen plaatje, maar PDF-schrijvers hergebruiken dezelfde rijgebaseerde predictor-machinerie ervoor omdat delta-dan-deflate strak gepakte gehele getallen en objectoffsets beter comprimeert dan deze ruw te deflaten
TIFF Predictor 2 is het eenvoudigere van de twee schema's: elke component wordt opgeslagen als het verschil ten opzichte van dezelfde component in de vorige pixel op dezelfde rij, en elke rij reset bij zijn linkerrand in plaats van een verschil van de rij erboven mee te dragen. PNG-predictie is preciezer, omdat het werkelijke filter per rij kan veranderen: elke rij begint met één tagbyte — 0 voor None, 1 voor Sub, 2 voor Up, 3 voor Average, 4 voor Paeth — en die tag, niet de gedeclareerde /Predictor-waarde, bepaalt hoe die specifieke rij wordt gereconstrueerd. Een /Predictor van 12 is eigenlijk gewoon de hint van de encoder dat het het Up-filter verkoos, waarbij elke byte wordt hersteld door de byte direct erboven in de vorige rij op te tellen, maar een correcte decoder moet toch bij elke rij de tag lezen in plaats van aan te nemen dat het overal Up is
Waarom maken objectstreams een gemiste Predictor onzichtbaar?
Objectstreams verergeren het probleem in plaats van het gewoon te herhalen. ISO 32000-1 §7.5.7 staat een PDF 1.5+-schrijver toe om meerdere indirecte objecten in één gecomprimeerde container te pakken, een /ObjStm, en het komt vaak voor dat precies de objecten die een validator het meest nodig heeft — de catalogus, /OutputIntents, of een XMP-/Metadata-stream — door die container reizen met /Predictor 12 eraan vast, omdat die objecten kort en repetitief genoeg zijn om baat te hebben bij rijdifferencing. Wanneer de predictor-stap ontbreekt, werpt het uitbreiden van de objectstream geen fout op: het produceert een bytesequentie die oppervlakkig plausibel oogt maar niet tokeniseert tot de verwachte objecten, dus wat er ook binnenin was gepakt duikt eenvoudigweg niet op. Rendering merkt dit zelden op, omdat een conforme render-engine predictor-gedifferentieerde data al reconstrueert voordat het ooit bij de layout aankomt; de code die het opmerkt is precies het soort waarin deze bug zich verschool — een validator, ondertekenaar, of versiechecker die de ruwe PDF-bytes zelf doorloopt om een structurele vraag te beantwoorden, zonder terugvaloptie zodra zijn eigen kijk op de objectstream verkeerd terugkomt
PDFiumPas liep precies tegen deze storing aan vóór v2.16.0. Objectstreams gebouwd met /Predictor 12, het gangbare geval voor PDF 1.5+-schrijvers, breidden via PdfExpandObjectStreams uit tot gedifferentieerde bytes die de structurele scanner niet kon parsen, dus de catalogus-, /OutputIntents-, en /Metadata-objecten die erin waren gepakt, waren effectief onzichtbaar voor conformiteitsscans — geen uitzondering, geen waarschuwing, gewoon een scan die zich stilzwijgend gedroeg alsof die objecten afwezig waren. De diepere mechanica van hoe PDFiumPas een objectstream oplost tegen de actieve cross-reference-tabel, inclusief de hybride en pure xref-stream-gevallen, wordt apart behandeld in het artikel over het valideren van object- en xref-streams met PDFiumPas; de hier beschreven predictor-stap draait na die oplossing, op de bytes die elk gecomprimeerd object daadwerkelijk bevat
PNG- en TIFF-Predictor-rijen omkeren in Pascal
PDFiumPas keert de differencing om in één enkele routine, PdfApplyPredictor, en de geometriewiskunde ervan is de moeite waard om te kennen, of u het nu aanroept of het idee in uw eigen Delphi-code herimplementeert. De rijbreedte in bytes is ceil(Columns × Colors × BitsPerComponent ÷ 8) en de breedte per pixel in bytes die beide algoritmen gebruiken is ceil(Colors × BitsPerComponent ÷ 8) — krijg een van beide afrondingen verkeerd en de reconstructie leest over een rijgrens heen in plaats van binnen één rij. Een /Predictor onder 2 wordt ongemoeid gelaten, aangezien 1 betekent dat de encoder helemaal geen transformatie toepaste; 2 selecteert de hieronder getoonde TIFF-tak, en alles vanaf 10 valt door naar PNG-rijfilter-reconstructie, waar de tagbyte aan het begin van elke rij — niet de gedeclareerde /Predictor-waarde — bepaalt hoe die specifieke rij wordt teruggedraaid
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;
Wat PDFiumPas veranderde in v2.16.0
De fix die uitkwam in PDFiumPas v2.16.0 zit binnen PdfReadAndDecodeStream, de routine die de ruwe bytes van een stream leest en decodeert voor elke aanroeper die PDF-structuur op byteniveau moet inspecteren, inclusief objectstream-uitbreiding; het probeert reconstructie pas na te bevestigen dat /Filter een kale FlateDecode is, nooit een cascade, omdat een gekoppelde filterreeks op dit niveau niet veilig predictor-gecorrigeerd kan worden. Het teruglezen van /Predictor, /Colors, /BitsPerComponent, en /Columns uit de streamdictionary heeft ook geen algemene dictionary-parser nodig: PdfDictRefNum vindt elke sleutel door directe naam-token-zoekopdracht binnen het bytebereik van die ene dictionary, wat hier veilig is precies omdat die vier sleutels niet kunnen herhalen of nesten binnen één enkele streamdictionary. Diezelfde naam-token-zoekopdracht is veel riskanter zodra deze wordt gericht op een groter of minder begrensd gebied van een PDF-bestand, wat het onderwerp is van het begeleidende artikel over het veilig parsen van PDF-dictionaries
// 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;
Vóór v2.16.0 breidde een objectstream gebouwd met /Predictor 12 uit tot gedifferentieerde bytes zonder dat er een fout werd opgeworpen, dus elk catalogus-, /OutputIntents-, of /Metadata-object dat erin was gepakt, ontbrak in de structurele scans van PDFiumPas zonder enige waarschuwing. Na de fix blaast dezelfde objectstream op en reconstrueert correct, en worden de objecten die erin zijn gepakt weer zichtbaar voor die scans. Defensieve grenzen reisden mee met de fix: PdfApplyPredictor weigert nu regelrecht /Colors boven 64, /BitsPerComponent boven 32, en /Columns boven 2^24, omdat die combinaties rijgeometrieën beschrijven die geen enkele echte PDF-producent nodig heeft en vooral bestaan om een decoder veel meer geheugen te laten toewijzen dan de invoerbytes rechtvaardigen
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;
Grenzen die de moeite waard zijn om te kennen
De predictor-reconstructie van PDFiumPas heeft twee randjes die de moeite waard zijn om te kennen voordat u erop vertrouwt. TIFF Predictor 2-reconstructie dekt alleen het geval van 8 bits per component; PDF staat smallere packings toe, maar sub-byte TIFF-gedifferentieerde data loopt ongereconstrueerd door in plaats van geraden te worden, dus een stream die /Predictor 2 declareert met /BitsPerComponent 1, 2, of 4 zal vandaag via dit pad niet correct decoderen. PNG-predictie kent geen dergelijke beperking — elke rij levert zijn eigen filtertag, en alle vijf gedefinieerde typen worden gereconstrueerd ongeacht wat de gedeclareerde /Predictor-waarde tussen 10 en 15 toevallig is, wat overeenkomt met hoe PNG-stijl-filtering daadwerkelijk werkt: de gedeclareerde waarde is meer een hint over wat de encoder meestal gebruikte dan een belofte over elke rij
De native render-engine van PDFium reconstrueert predictor-gedifferentieerde beeld- en inhoudsstroomdata al correct, wat precies is waarom een bestand perfect kan renderen in elke gewone viewer terwijl een validator, ondertekenaar, of versiechecker op byteniveau die daarbovenop is gebouwd dezelfde bytes verkeerd leest. De hier beschreven predictor-bewuste decodering ondersteunt de PDF/A-validatie-, structurele-scan-, en ondertekeningsfuncties van PDFiumPas, de native VCL-PDFium-component voor Delphi en C++Builder