Un object stream PDF che si decomprime senza errori ma continua a leggersi come rumore manca di solito di un passaggio: invertire il Predictor di ISO 32000-1. Quando il dizionario /DecodeParms di uno stream porta /Predictor 2 o superiore, i byte che FlateDecode restituisce non sono i dati originali — sono valori differenziati in stile PNG per riga o differenziati orizzontalmente in stile TIFF che necessitano di un secondo passaggio di ricostruzione prima che qualsiasi lookup di dizionario abbia senso. PDFiumPas, la libreria componente PDF VCL nativa per Delphi e C++Builder, ha aggiunto quel passaggio di ricostruzione nella v2.16.0, specificamente perché gli object stream PDF 1.5+ si espandevano in byte differenziati che nessun parser di dizionario riusciva a leggere
Perché FlateDecode da solo non basta
FlateDecode di per sé è solo decompressione DEFLATE (ISO 32000-1 §7.4.4.1): riproduce qualunque byte l'encoder abbia passato al compressore, nulla di più. Il Predictor risiede un livello più su, nel dizionario /DecodeParms dello stream, e descrive una trasformazione che l'encoder ha applicato prima della compressione — la differenziazione trasforma lunghe sequenze di valori strutturati simili, come gli interi strettamente impacchettati dentro uno stream cross-reference o un object stream, in lunghe sequenze di numeri piccoli che DEFLATE comprime molto meglio. ISO 32000-1 §7.4.4.3 (Tabella 8) è esplicita nel dire che annullare questa trasformazione fa parte della decodifica di uno stream filtrato, non un passaggio di pulizia opzionale, eppure è facile scrivere un helper FlateDecode che chiama solo inflate e si ferma lì
Il sintomo è distintivo una volta che sai cosa cercare. I byte differenziati dal Predictor non sono rumore casuale — portano ancora la forma di uno stream compresso, quindi un parser ingenuo spesso percorre alcuni token dall'aspetto valido prima di incontrare una sequenza di byte che non può assolutamente essere un nome, numero o delimitatore PDF, e righe diverse falliscono a offset diversi a seconda di quanto i valori sottostanti capitassero di differire dai loro vicini. Questa incoerenza è ciò che rende difficile individuare il bug da un singolo file che fallisce: due PDF dello stesso produttore possono differire solo in quali valori capitano di ripetersi, quindi uno viene analizzato quasi per caso mentre l'altro fallisce apertamente
Cosa fa realmente il parametro Predictor del PDF?
La voce /Predictor in /DecodeParms dice a un lettore conforme quale inversione eseguire, e la Tabella 8 di ISO 32000-1 definisce i valori che contano nella pratica: 1 significa che non è stata applicata alcuna predizione, 2 seleziona TIFF Predictor 2 (differenziazione orizzontale), e qualsiasi valore da 10 a 15 seleziona la predizione in stile PNG. Altre tre chiavi viaggiano al suo fianco — /Colors, /BitsPerComponent, e /Columns — e insieme descrivono la geometria di riga contro cui è stata calcolata la differenziazione, anche quando lo stream non contiene alcun dato immagine: un object stream non è un'immagine, ma gli scrittori PDF riutilizzano lo stesso meccanismo predictor basato su righe per esso perché delta-poi-deflate comprime interi e offset di oggetto strettamente impacchettati meglio che deflazionarli grezzi
TIFF Predictor 2 è il più semplice dei due schemi: ogni componente viene memorizzato come differenza dallo stesso componente nel pixel precedente sulla stessa riga, e ogni riga si azzera al proprio bordo sinistro invece di portare una differenza dalla riga sopra. La predizione PNG è più particolare, perché il filtro effettivo può cambiare riga per riga: ogni riga inizia con un singolo byte tag — 0 per None, 1 per Sub, 2 per Up, 3 per Average, 4 per Paeth — e quel tag, non il valore /Predictor dichiarato, decide come quella specifica riga venga ricostruita. Un /Predictor di 12 è in realtà solo il suggerimento dell'encoder che ha favorito il filtro Up, dove ogni byte viene ripristinato aggiungendo il byte direttamente sopra di esso nella riga precedente, ma un decoder corretto deve comunque leggere il tag su ogni riga invece di presupporre Up ovunque
Perché gli object stream rendono invisibile un Predictor mancato?
Gli object stream compongono il problema invece di limitarsi a ripeterlo. ISO 32000-1 §7.5.7 permette a uno scrittore PDF 1.5+ di impacchettare più oggetti indiretti in un unico contenitore compresso, un /ObjStm, ed è comune che proprio gli oggetti di cui un validatore ha più bisogno — il catalogo, /OutputIntents, o uno stream /Metadata XMP — viaggino attraverso quel contenitore con /Predictor 12 allegato, perché quegli oggetti sono abbastanza corti e ripetitivi da beneficiare della differenziazione per riga. Quando il passaggio predictor manca, espandere l'object stream non solleva un errore: produce una sequenza di byte che sembra superficialmente plausibile ma non si tokenizza negli oggetti attesi, quindi qualunque cosa fosse impacchettata all'interno semplicemente non emerge. Il rendering raramente se ne accorge, perché un motore di rendering conforme ricostruisce già i dati differenziati dal predictor prima ancora che arrivino al layout; il codice che se ne accorge è esattamente il tipo in cui questo bug si nascondeva — un validatore, firmatario, o controllore di versione che percorre da sé i byte PDF grezzi per rispondere a una domanda strutturale, senza alcun fallback una volta che la propria vista dell'object stream torna sbagliata
PDFiumPas ha incontrato esattamente questo fallimento prima della v2.16.0. Gli object stream costruiti con /Predictor 12, il caso comune per gli scrittori PDF 1.5+, si espandevano tramite PdfExpandObjectStreams in byte differenziati che lo scanner strutturale non riusciva ad analizzare, quindi il catalogo, /OutputIntents, e gli oggetti /Metadata impacchettati all'interno erano effettivamente invisibili alle scansioni di conformità — nessuna eccezione, nessun avviso, solo una scansione che si comportava silenziosamente come se quegli oggetti fossero assenti. La meccanica più profonda di come PDFiumPas risolve un object stream contro la tabella cross-reference attiva, inclusi i casi ibridi e xref-stream puri, è trattata separatamente nell'articolo sulla validazione degli object e xref stream con PDFiumPas; il passaggio predictor descritto qui viene eseguito dopo quella risoluzione, sui byte che ogni oggetto compresso effettivamente contiene
Invertire le righe Predictor PNG e TIFF in Pascal
PDFiumPas inverte la differenziazione in un'unica routine, PdfApplyPredictor, e la sua matematica geometrica vale la pena conoscerla, che tu la chiami o reimplementi l'idea nel tuo codice Delphi. La larghezza di riga in byte è ceil(Columns × Colors × BitsPerComponent ÷ 8) e la larghezza in byte per pixel che entrambi gli algoritmi usano è ceil(Colors × BitsPerComponent ÷ 8) — sbaglia l'uno o l'altro arrotondamento e la ricostruzione legge attraverso un confine di riga invece che dentro una. Un /Predictor sotto 2 viene lasciato intatto, poiché 1 significa che l'encoder non ha applicato alcuna trasformazione; 2 seleziona il ramo TIFF mostrato sotto, e qualsiasi valore da 10 in su cade nella ricostruzione dei filtri di riga PNG, dove il byte tag all'inizio di ogni riga — non il valore /Predictor dichiarato — decide come quella specifica riga venga annullata
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;
Cosa ha cambiato PDFiumPas nella v2.16.0
La correzione distribuita in PDFiumPas v2.16.0 risiede dentro PdfReadAndDecodeStream, la routine che legge i byte grezzi di uno stream e li decodifica per ogni chiamante che ha bisogno di ispezionare la struttura PDF a livello di byte, inclusa l'espansione degli object stream; tenta la ricostruzione solo dopo aver confermato che /Filter è un semplice FlateDecode, mai una cascata, perché un filtro concatenato non può essere corretto in sicurezza dal predictor a questo livello. Anche leggere /Predictor, /Colors, /BitsPerComponent, e /Columns dal dizionario dello stream non richiede un parser di dizionario generale: PdfDictRefNum trova ogni chiave tramite ricerca diretta a token di nome dentro quell'intervallo di byte di un unico dizionario, il che è sicuro qui proprio perché quelle quattro chiavi non possono ripetersi o annidarsi dentro un singolo dizionario stream. Quella stessa ricerca a token di nome è molto più rischiosa una volta puntata verso una regione più grande o meno delimitata di un file PDF, che è l'oggetto dell'articolo di approfondimento sull'analisi sicura dei dizionari 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;
Prima della v2.16.0, un object stream costruito con /Predictor 12 si espandeva in byte differenziati senza sollevare alcun errore, quindi qualsiasi oggetto catalogo, /OutputIntents, o /Metadata impacchettato al suo interno spariva dalle scansioni strutturali di PDFiumPas senza alcun avviso. Dopo la correzione, lo stesso object stream si decomprime e poi si ricostruisce correttamente, e gli oggetti impacchettati al suo interno tornano visibili a quelle scansioni. Dei limiti difensivi hanno viaggiato insieme alla correzione: PdfApplyPredictor ora rifiuta apertamente /Colors sopra 64, /BitsPerComponent sopra 32, e /Columns sopra 2^24, perché quelle combinazioni descrivono geometrie di riga di cui nessun vero produttore PDF ha bisogno ed esistono principalmente per far allocare a un decoder molta più memoria di quanto i byte in input giustifichino
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;
Limiti utili da conoscere
La ricostruzione predictor di PDFiumPas ha due confini utili da conoscere prima di affidarvisi. La ricostruzione TIFF Predictor 2 copre solo il caso a 8 bit per componente; il PDF permette impacchettamenti più stretti, ma i dati differenziati TIFF sub-byte passano attraverso non ricostruiti invece di essere indovinati, quindi uno stream che dichiara /Predictor 2 con /BitsPerComponent 1, 2, o 4 non si decodificherà correttamente tramite questo percorso oggi. La predizione PNG non ha tale restrizione — ogni riga fornisce il proprio tag di filtro, e tutti e cinque i tipi definiti vengono ricostruiti indipendentemente da quale valore /Predictor dichiarato tra 10 e 15 capiti di essere, il che corrisponde a come funziona realmente il filtraggio in stile PNG: il valore dichiarato è più vicino a un suggerimento su cosa l'encoder abbia usato per lo più che a una promessa su ogni riga
Il motore di rendering nativo di PDFium già ricostruisce correttamente i dati immagine e content-stream differenziati dal predictor, il che è esattamente il motivo per cui un file può renderizzarsi perfettamente in qualsiasi visualizzatore ordinario mentre un validatore, firmatario, o controllore di versione a livello di byte costruito sopra di esso legge male gli stessi byte. La decodifica consapevole del predictor descritta qui sostiene le funzionalità di validazione PDF/A, scansione strutturale e firma di PDFiumPas, il componente PDFium VCL nativo per Delphi e C++Builder