Articolo tecnico

PDF senza dizionario Pages: implicazioni per l'analisi

Il dizionario del catalogo PDF (Catalog) ha esattamente una chiave di navigazione richiesta: /Pages. Tale chiave deve puntare a un oggetto indiretto di tipo /Pages, che a sua volta contiene l'array /Kids e il /Count totale delle pagine. Rimuovendo questo puntatore, nessun lettore conforme sarà in grado di individuare una singola pagina nel file. La specifica ISO 32000-1 §7.7.2 è inequivocabile su questo punto: il Catalog deve contenere una voce /Pages e l'oggetto a cui si fa riferimento deve essere di tipo /Pages. I file che violano questo requisito non sono semplicemente non conformi; sono strutturalmente danneggiati in un modo che la maggior parte dei parser gestisce con difficoltà

Cosa dice effettivamente la specifica

Un PDF minimo conforme ha almeno tre oggetti. L'oggetto 1 è il Catalog, l'oggetto 2 è la radice Pages e dall'oggetto 3 in poi si hanno i singoli dizionari Page. Il Catalog punta alla radice Pages; la radice Pages elenca i propri figli in /Kids; ogni Page porta un riferimento inverso /Parent. L'intera catena è bidirezionale per progettazione, in modo che un parser possa iniziare da una qualsiasi delle due estremità e scorrere fino a qualsiasi pagina in un tempo O(log n) nel caso di alberi bilanciati

% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj

3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj

4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

L'albero Pages può essere annidato. Un documento con migliaia di pagine raggruppa tipicamente le pagine in oggetti nodo intermedi che portano a loro volta il tipo /Pages, ciascuno con il proprio array /Kids e un valore /Count che riflette il sottoalbero sottostante. Il valore /Count del nodo radice equivale sempre al conteggio totale delle pagine. Questo conteggio è ciò che i visualizzatori mostrano nel campo del numero di pagina prima ancora di aver analizzato una singola pagina, poiché la lettura di un singolo intero dall'oggetto 2 è molto meno onerosa rispetto al percorso dell'intero albero

Come si presenta un file senza dizionario Pages

I file a cui manca il dizionario Pages derivano in genere da generatori PDF che scrivono gli oggetti pagina direttamente senza assemblarli in un albero, oppure da un danneggiamento che rimuove il nodo radice lasciando intatti gli oggetti foglia Page. Il Catalog in un file di questo tipo non ha affatto la chiave /Pages, oppure contiene un riferimento a un oggetto che non esiste più nella tabella dei riferimenti incrociati (cross-reference table)

% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj

% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj

25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj

Un parser che segue la specifica leggerà il Catalog, tenterà di risolvere la chiave /Pages, non troverà nulla (o troverà un riferimento interrotto) e genererà un errore o segnalerà zero pagine. Ciò che non deve assolutamente fare è continuare come se il file avesse zero pagine e completare l'operazione in modo silenzioso; questo produce un output vuoto che sembra corretto agli strumenti automatizzati, ma risulta errato a qualsiasi persona lo apra

Perché i parser si bloccano

La maggior parte dei parser PDF alloca la propria tabella interna delle pagine all'avvio in base al valore /Count della radice Pages. Quando tale radice è assente, il parser legge zero e non alloca nulla, per poi dereferenziare un puntatore nullo la prima volta che il codice richiede la pagina 1, oppure legge dati spazzatura e alloca un buffer del tutto errato. Nessuno dei due risultati è gestibile. La violazione di accesso all'indirizzo 0x008E5D78 che compare nei log di arresto anomalo durante l'elaborazione di un file del genere è proprio questa: una dereferenziazione di un puntatore nullo all'interno del percorso di accesso alla pagina, causata dall'assenza della struttura che il parser presumeva essere sempre presente

L'assunzione alla base del codice è ragionevole. La stragrande maggioranza dei PDF esistenti dispone di un dizionario Pages. I parser che saltano il controllo di esistenza per risparmiare poche istruzioni non stanno agendo con imprudenza; stanno ottimizzando per il caso d'uso comune. I file che mettono a dura prova questa ottimizzazione sono talmente rari che il codice di produzione potrebbe non incontrarne mai uno, fino al momento in cui si verifica il blocco; a quel punto il crash è sia riproducibile che sconcertante per lo sviluppatore che non ha letto la sezione §7.7.2

Ripristino senza albero Pages

Se un parser deve gestire questi file anziché rifiutarli, il ripristino segue un percorso prevedibile: scansionare ogni oggetto indiretto nella tabella dei riferimenti incrociati, raccogliere quelli con /Type /Page e ordinarli in base al numero di oggetto. L'ordine dei numeri di oggetto non garantisce necessariamente la corrispondenza con l'ordine di lettura della specifica ma, all'atto pratico, i generatori che omettono l'albero Pages tendono a produrre le pagine in sequenza, quindi l'ordinamento per numero di oggetto si rivela corretto nella maggior parte dei casi

Il controllo in sé è rapido. Prima di seguire il puntatore /Pages del Catalog, verificare che il puntatore esista, che faccia riferimento a un oggetto effettivo e che il /Type dell'oggetto risolto sia pari a /Pages. Se una qualsiasi di queste tre condizioni non è soddisfatta, si passa alla scansione lineare. Per documenti di grandi dimensioni la scansione is più lenta rispetto al percorso ad albero, poiché legge l'intestazione di ogni singolo oggetto anziché seguire un cammino bilanciato, ma è efficace e, per un file che è già malformato, l'accuratezza ha la precedenza sulla velocità

Un caso limite che la scansione lineare non risolve in automatico è l'ordine delle pagine. Senza un array /Kids a definire la sequenza, l'ordine "corretto" non è definito dalla specifica. L'ordine per numero di oggetto rappresenta l'impostazione predefinita più pragmatica; se il file è abbastanza importante da richiedere un'elaborazione accurata, vale la pena verificare se gli oggetti Page contengono un elemento /StructParents esplicito o riferimenti di annotazione che suggeriscano una sequenza di lettura

Implicazioni per i generatori PDF

Per chiunque si occupi di scrivere un generatore PDF piuttosto che un parser, la lezione è semplice: generare sempre la radice Pages prima di chiudere il file. Un Catalog privo della voce /Pages non costituisce un PDF valido in base a nessuna revisione della specifica. I generatori che compilano gli oggetti pagina al volo e assemblano l'albero al momento della finalizzazione (l'approccio utilizzato dalla maggior parte dei sistemi di scrittura in streaming) funzionano correttamente a patto che la finalizzazione venga effettivamente eseguita. Il caso tipico di errore risiede in un'eccezione o in un ritorno anticipato che interrompe la scrittura prima del completamento del trailer, lasciando un file che si apre in alcuni visualizzatori (dotati di euristiche di ripristino) ma fallisce in altri (che ne sono privi)

Gli standard PDF/A e PDF/UA impongono vincoli aggiuntivi all'albero delle pagine rispetto a quanto richiesto dalla specifica di base, ma nessuno dei due allenta il requisito del dizionario /Pages. Un validatore che verifichi la conformità alle norme ISO 19005 o ISO 14289 rileverà l'assenza del dizionario Pages come una violazione della specifica di base prima ancora di applicare le regole specifiche del profilo