Articolo tecnico

Alberi di Pagine PDF: Perché l'Ordine delle Pagine Non È l'Ordine degli Oggetti

La pagina 1 di un PDF non è l'oggetto 1. Questa distinzione è la fonte più comune di bug legati all'estrazione di pagine errate nei parser PDF, e la soluzione è leggere le specifiche piuttosto che i byte del file

Oggetti, riferimenti e il catalogo

Un file PDF è una raccolta di oggetti numerati. Ognuno di essi possiede un numero oggetto unico e un numero di generazione, scritti come N G obj dove G è quasi sempre 0 nei file che non sono stati aggiornati incrementalmente. Gli oggetti si fanno riferimento a vicenda con la notazione N G R, quindi 3 0 R significa "la versione corrente dell'oggetto 3". Il trailer punta a un oggetto catalogo radice la cui voce /Pages porta all'albero delle pagine. Tutto ciò che è navigabile in un PDF inizia da quella radice, non dal primo byte del corpo del file

La tabella dei riferimenti incrociati (o il flusso dei riferimenti incrociati in PDF 1.5+) mappa i numeri di oggetto agli offset del file. Il suo compito è l'accesso casuale, non l'ordinamento. Uno scrittore che compila un documento in modo incrementale può aggiungere nuovi oggetti alla fine con numeri più alti mentre quegli oggetti precedono logicamente quelli esistenti nella sequenza delle pagine. Questo non è un difetto; è una scelta di progettazione

L'albero delle pagine (ISO 32000-1 §7.7.3)

La sequenza delle pagine risiede nell'albero delle pagine. Il catalogo radice contiene un riferimento /Pages che punta a un nodo di tipo /Pages. L'array /Kids di tale nodo elenca i suoi figli nell'ordine di lettura. Ogni figlio è un nodo foglia di tipo /Page o un altro nodo intermedio /Pages contenente il proprio /Kids. La pagina 1 è la prima foglia raggiunta da una ricerca in profondità (depth-first) da sinistra a destra degli array Kids. La voce /Count su ogni nodo intermedio memorizza il numero totale di pagine foglia discendenti, cosicché un visualizzatore può saltare alla pagina 500 senza percorrere l'intero albero

Ecco l'aspetto di un albero minimo di tre pagine nella sintassi raw del PDF:

16 0 obj
<<
  /Type /Pages
  /Count 3
  /Kids [20 0 R  1 0 R  4 0 R]
  /MediaBox [0 0 612 792]
>>
endobj

20 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 21 0 R  /Resources 22 0 R >>
endobj

1 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 2 0 R   /Resources 3 0 R >>
endobj

4 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 5 0 R   /Resources 6 0 R >>
endobj

L'array Kids recita [20 0 R, 1 0 R, 4 0 R]. La pagina logica 1 è l'oggetto 20, la pagina logica 2 è l'oggetto 1, la pagina logica 3 è l'oggetto 4. Qualsiasi codice che iteri i numeri degli oggetti a partire da 1 incontrandoli nell'ordine 1, 4, 20 produrrà la sequenza pagina-2, pagina-3, pagina-1. Il documento risultante renderizzerà in un ordine mescolato che può sembrare perfettamente normale in un visualizzatore che segue l'albero e catastroficamente errato in uno che non lo fa

Ereditarietà

I nodi intermedi possono portare proprietà che i loro discendenti ereditano. Le voci ereditate più comuni sono /MediaBox (dimensioni della pagina), /CropBox, /Resources (font e immagini) e /Rotate. Una pagina foglia che omette /MediaBox non è corrotta; acquisisce il valore dal nodo antenato più vicino che lo definisce. Una pagina che definisce effettivamente /MediaBox sovrascrive qualsiasi cosa indichi il genitore, solo per quella pagina

Ciò ha rilevanza per il parsing. Leggere un oggetto /Page isolato, presumendo che le sue proprietà siano complete, indicherà dimensioni errate per qualsiasi pagina che si basi sull'ereditarietà. Un lettore corretto percorre la catena /Parent, raccogliendo le proprietà che non ha ancora visto, per fermarsi alla radice

Alberi annidati

Nulla nelle specifiche limita l'albero a un singolo livello. Un documento di grandi dimensioni potrebbe raggruppare le pagine sotto nodi intermedi che corrispondono vagamente a capitoli:

2 0 obj   % root Pages node, Count = 8
<< /Type /Pages  /Count 8  /Kids [3 0 R  4 0 R] >>
endobj

3 0 obj   % first chapter, 5 pages
<< /Type /Pages  /Parent 2 0 R  /Count 5
   /Kids [10 0 R  11 0 R  12 0 R  13 0 R  14 0 R]
   /MediaBox [0 0 612 792] >>
endobj

4 0 obj   % second chapter, 3 pages
<< /Type /Pages  /Parent 2 0 R  /Count 3
   /Kids [20 0 R  21 0 R  22 0 R]
   /MediaBox [0 0 612 792] >>
endobj

L'algoritmo di attraversamento è il medesimo: visita Kids in ordine, ricorri in qualsiasi nodo /Pages, raccogli i nodi foglia /Page. I valori di /Count permettono a un visualizzatore di saltare un intero sottoalbero quando si cerca una pagina che si trova oltre, motivo per cui tali conteggi devono essere accurati. Alcuni editor PDF della fine degli anni '90 e dei primi anni 2000 non li ricalcolavano dopo le modifiche sul posto, quindi un parser difensivo verifica /Count con il conteggio reale dei figli invece di fidarsi di esso per l'allocazione dell'array

Come questo emerge nella pratica

Il bug dell'ordine delle pagine si manifesta più di frequente in due scenari. Il primo è un parser personalizzato che esegue la scansione di oggetti di tipo /Page invece di seguire l'albero. Trova ogni pagina, ma in ordine di numero oggetto, non nell'ordine di lettura. La correzione è sempre la stessa: inizia dal trailer, risolvi il catalogo radice, segui /Pages e attraversa gli array Kids

Il secondo scenario è un file ad aggiornamento incrementale. Quando un editor PDF aggiunge modifiche senza riscrivere l'intero file, i nuovi oggetti pagina acquisiscono numeri di oggetto alti, mentre l'array Kids nell'albero originale controlla ancora la loro posizione logica. Una pagina che originariamente era l'oggetto 5 viene sostituita da un nuovo oggetto 143, ma l'array Kids si riferisce ora a 143 dove era solito indicare 5, mantenendo così l'ordine logico. Scorrere i numeri degli oggetti inserirebbe la pagina sostitutiva nella posizione sbagliata della sequenza

I file PDF linearizzati (ottimizzati per il web) presentano una terza variazione: il file viene fisicamente riorganizzato affinché il contenuto della prima pagina appaia all'inizio del file per una rapida visualizzazione in una connessione lenta. La struttura dell'albero delle pagine rimane l'autorità riguardo all'ordine, ma la tabella dei riferimenti incrociati mappa agli offset riorganizzati. Un parser che fa affidamento sulla posizione nel file invece che sulla tabella xref leggerà male perfino la prima pagina di un file linearizzato

L'HotPDF Component gestisce l'attraversamento dell'albero delle pagine, la risoluzione dell'ereditarietà e la fusione degli aggiornamenti incrementali xref in modo interno. Lavorare direttamente con i suoi oggetti pagina assicura che l'ordinamento dell'array Kids sia già stato applicato; gli indici di pagina sono associati alle pagine logiche, non ai numeri degli oggetti

La forma bilanciata dell'albero è una scelta di prestazioni: i nodi interni ereditano gli attributi, il flattening è legale ma può costare memoria e tempo, e un `/Count` non aggiornato può ingannare parser permissivi o interrompere quelli rigorosi