La nostra guida di approfondimento sull'ordine delle pagine PDF copre la regola di base: l'ordine di visualizzazione deriva da una visita in profondità, da sinistra a destra, degli array /Kids nell'albero /Pages, mai dai numeri degli oggetti. Questo articolo osserva l'albero da un'altra angolazione: la sua forma. Perché i writer PDF maturi generano gerarchie di nodi intermedi quando un singolo array piatto sarebbe perfettamente legale? Cosa cambia realmente quando uno strumento appiattisce o ricostruisce l'albero? E cosa succede quando la contabilità del /Count, che rende veloce l'intera struttura, smette di dire la verità
Il fan-out è una decisione di prestazioni
Nulla obbliga uno scrittore ad annidare. Un documento di 10.000 pagine con un unico nodo radice /Pages e 10.000 riferimenti foglia in un singolo array /Kids è conforme alla specifica. La PDF Reference raccomanda comunque un albero bilanciato per i documenti di grandi dimensioni, e i generatori più diffusi seguono questo consiglio con un fan-out modesto, tipicamente qualche decina di figli per nodo intermedio
Il motivo sta in ciò che un visualizzatore deve leggere prima di poter mostrare qualcosa. Si consideri un salto diretto alla pagina 8.214 di quel file da 10.000 pagine. Con un albero piatto, il visualizzatore deve prima analizzare il nodo radice, e quel nodo radice è un array enorme: a circa otto byte per riferimento indiretto, un oggetto di 80 KB che deve essere tokenizzato interamente prima che la voce 8.213 possa essere risolta. Con un albero bilanciato di fan-out 32, lo stesso salto legge la radice, confronta i totali /Count correnti per scegliere il figlio corretto, e scende — in totale tre o quattro piccoli dizionari, ciascuno di poche centinaia di byte. Questo è l'accesso casuale O(log n) per cui l'albero è stato progettato, ed è l'intera ragione per cui il /Count esiste sui nodi intermedi: permette a un lettore di saltare un intero sottoalbero senza aprire un solo oggetto al suo interno
La forma dell'albero determina anche il costo della modifica. Un aggiornamento incrementale che inserisce una pagina deve riscrivere ogni nodo il cui /Kids o /Count è cambiato, cioè il percorso dal genitore della nuova foglia fino alla radice. In un albero bilanciato, quel percorso è una manciata di piccoli dizionari aggiunti al file. In un albero piatto il "percorso" è l'unico gigantesco array radice, duplicato per intero a ogni revisione. Un contratto che attraversa trenta cicli di revisione e annotazione può finire per trascinare trenta copie superate dello stesso array da 80 KB nel proprio flusso di byte
I nodi interni portano attributi ereditati
I nodi intermedi non servono solo per l'instradamento. I quattro attributi di pagina ereditabili — /Resources, /MediaBox, /CropBox e /Rotate — possono essere issati su qualsiasi nodo /Pages, dove si applicano a ogni foglia sottostante a meno che un discendente non li sovrascriva. Uno scrittore che produce un rapporto con un'appendice orizzontale può esprimere quella disposizione direttamente nell'albero:
5 0 obj % radice del documento
<< /Type /Pages /Count 6 /Kids [6 0 R 7 0 R] >>
endobj
6 0 obj % corpo del rapporto: A4 verticale, font del corpo
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [30 0 R 31 0 R 32 0 R]
/MediaBox [0 0 595 842]
/Resources << /Font << /F1 8 0 R >> >> >>
endobj
7 0 obj % appendice: A4 orizzontale, ruotata, font proprio
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [40 0 R 41 0 R 42 0 R]
/MediaBox [0 0 842 595] /Rotate 90
/Resources << /Font << /F2 9 0 R >> >> >>
endobj
40 0 obj % pagina dell'appendice: eredita dimensione, rotazione, font
<< /Type /Page /Parent 7 0 R /Contents 43 0 R >>
endobj
Gli oggetti da 40 a 42 sono quasi vuoti. Le loro dimensioni di pagina, la rotazione e le risorse dei font arrivano tutte per eredità dal nodo 7, il che mantiene il file compatto e autosufficiente: si aggiunga una quarta pagina sotto il nodo dell'appendice e risulterà automaticamente orizzontale
Lo stesso meccanismo crea il classico rischio dello spostamento di pagina. Supponiamo che uno strumento sposti l'oggetto 40 nel corpo del rapporto modificando i due array /Kids e ripuntando /Parent al nodo 6. Lo spostamento è strutturalmente valido, eppure l'oggetto 40 ora eredita il /MediaBox verticale, nessuna rotazione e il font /F1 — mentre il suo flusso di contenuto continua a selezionare /F2, che non si risolve più. La pagina si restringe, si disruota e perde il proprio testo in un'unica modifica. Il codice di riordino robusto quindi materializza i valori risolti di tutti e quattro gli attributi ereditabili sul dizionario della pagina prima di riassegnarne il genitore. Se avete mai trascinato una pagina in un editor e l'avete vista cambiare dimensione o orientamento, questo è il meccanismo di cui siete stati testimoni
Flattening: legale, comune, a volte costoso
Molti strumenti procedono nella direzione opposta. Gli scrittori minimali generano un albero a livello singolo perché è semplice, e molte utility di unione e divisione ricostruiscono qualsiasi albero leggano in un unico array /Kids piatto, perché generare una struttura bilanciata è lavoro extra e l'output piatto è sempre conforme. Una ricostruzione corretta deve risolvere l'ereditarietà allo stesso tempo: ogni attributo che una foglia stava ereditando deve essere copiato sulla foglia, oppure issato sulla nuova radice se è uniforme in tutto il documento — altrimenti l'output cambia la geometria esattamente come nel caso dello spostamento di pagina
Per i documenti tipici il flattening è innocuo. Fa male su larga scala, nei due modi già descritti: l'array radice diventa un unico grande oggetto che ogni apertura e ogni salto di pagina deve analizzare per intero, e ogni modifica strutturale lo riscrive completamente. Ciò che il flattening non distrugge è la condivisione tramite riferimenti indiretti — un albero piatto in cui tutte le 10.000 pagine puntano allo stesso oggetto dizionario /Resources resta comunque deduplicato. Ciò che si perde è solo la possibilità di omettere la voce dalla pagina e lasciare che un antenato la fornisca
Quando /Count mente
/Count è pura contabilità: deve essere uguale al numero di pagine foglia nel sottoalbero del nodo, e nulla nel formato del file lo impone. Due schemi di corruzione spiegano la maggior parte dei conteggi mendaci osservati sul campo
Il primo è il conteggio obsoleto lasciato da un aggiornamento incrementale. Un editor inserisce una pagina, riscrive il genitore immediato con un nuovo /Kids e un /Count aggiornato, aggiunge entrambi al file — e non tocca mai gli antenati:
% Revisione originale
12 0 obj
<< /Type /Pages /Count 9 /Kids [13 0 R 14 0 R 15 0 R] >>
endobj
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 3
/Kids [50 0 R 51 0 R 52 0 R] >>
endobj
% Revisione aggiunta: una pagina inserita nel ramo centrale.
% L'oggetto 14 viene sostituito; l'oggetto 12 non viene mai riscritto
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 4
/Kids [50 0 R 51 0 R 90 0 R 52 0 R] >>
endobj
L'albero ora contiene dieci foglie, ma la radice dice ancora nove. Un visualizzatore che si fida della radice riporta nove pagine nel suo contatore di pagine. Uno che usa i conteggi interni per una ricerca binaria di un salto di pagina calcola un indice errato per ogni pagina successiva al punto di inserimento. Una visita completa ne trova dieci. Tre risposte diverse, un solo file
Il secondo schema è il conteggio che non potrebbe mai essere corretto: negativo, zero su un nodo popolato, o assurdamente enorme. Questi derivano da fuzzing, da danni di trasmissione, e occasionalmente da bug aritmetici negli editor. Sono pericolosi specificamente per il codice che si fida del /Count per l'allocazione — dimensionare un array a partire da un /Count di -3 solleva nel migliore dei casi un errore di intervallo, e farlo a partire da un /Count di due miliardi è un'allocazione da denial-of-service. Il valore è input non affidabile, come ogni altro numero nel file
I parser si dividono in due schieramenti su tutto questo. I consumatori rigorosi — strumenti di preflight, validatori PDF/A, pipeline di archiviazione — confrontano il /Count con il risultato della visita e rifiutano o segnalano il file. I visualizzatori interattivi sono quasi universalmente permissivi: attraversano l'albero, derivano il conteggio reale, e ignorano silenziosamente quello memorizzato, motivo per cui un file con conteggio obsoleto può circolare per anni senza reclami finché non incontra un parser più rigoroso all'interno di qualche flusso di lavoro automatizzato. La via di mezzo difensiva per il codice di libreria è trattare il /Count come un suggerimento — utile per la preallocazione, e per saltare i sottoalberi una volta verificato — lasciando che la visita resti la fonte di verità
Per l'algoritmo di attraversamento stesso, le regole di ricerca dell'ereditarietà e il percorso dal catalogo alla foglia, si parta dalla guida sull'ordine delle pagine. Per capire come appaiono queste modalità di guasto quando un documento reale di un cliente raggiunge il codice di produzione, si legga lo studio di caso sul debug dell'ordine delle pagine, che segue un incidente di pagine mescolate dal sintomo alla causa radice
L'HotPDF Delphi Component gestisce tutto questo internamente: attraversa alberi annidati di qualsiasi profondità, risolve gli attributi ereditati quando le pagine vengono copiate o spostate, e verifica il /Count rispetto ai conteggi reali delle foglie invece di fidarsene, così gli indici di pagina nella sua API significano sempre pagine logiche