Salta ai contenuti
getnextpdf.com

L'anatomia di un file PDF

Apri un qualsiasi PDF in un editor di testo semplice e la prima cosa che vedi è rassicurante: un header %PDF-1.x o %PDF-2.0. L’ultima cosa che vedi è %%EOF. Tutto ciò che sta tra queste due righe è una piccola macchina ordinata per trovare gli oggetti per numero. Questa pagina è una dissezione. Apriamo il file, diamo un nome a ciascun organo e mostriamo come si collegano.

È il compagno strutturale di due pagine vicine. What a PDF actually is tratta il file come un grafo di oggetti; incremental updates coprono come cresce nel tempo. Questa pagina resta vicina ai byte — le regioni fisiche che un parser attraversa, nell’ordine in cui stanno su disco.

Quasi mai serve sapere questo per usare un PDF. Serve il giorno in cui uno va storto. Un file si apre in un visualizzatore e non in un altro; un validatore segnala una «tabella dei riferimenti incrociati danneggiata»; un documento firmato improvvisamente non si verifica più. Nessuno di questi è un mistero, una volta che sai leggere l’anatomia. Sono un numero che non corrisponde più a una posizione, una regione nel posto sbagliato, o una coda che punta al nulla.

Conoscere il layout trasforma «il PDF è corrotto» in una diagnosi su cui puoi agire. È la differenza tra alzare le spalle davanti a una scatola nera e indicare il byte esatto che mente.

Un PDF conforme ha quattro parti fisiche, in questo ordine nel file (Spec: ISO 32000-2, §7.5.1):

  1. Un header — una riga, %PDF-2.0, che indica la versione.
  2. Un corpo — il grosso del file: una sequenza di oggetti indiretti numerati.
  3. Una sezione dei riferimenti incrociati — un indice dal numero di oggetto all’offset di byte dove quell’oggetto si trova. I PDF classici usano una tabella di testo; PDF 2.0 usa un xref stream compresso.
  4. Un trailer — un piccolo dizionario che indica il punto di ingresso, seguito da startxref, un offset e %%EOF.

Il colpo di scena: un lettore non parte dall’alto. Parte dal basso, legge startxref per trovare l’indice, e usa quell’indice per raggiungere direttamente qualsiasi oggetto. Il file è scritto dall’inizio alla fine ma letto dalla fine all’inizio.

Percorriamo le quattro regioni in ordine, con i byte davanti a noi.

L’header è una riga. NextPDF scrive %PDF-2.0, e per convenzione una seconda riga di commento fatta di byte a bit alto, così che i naive strumenti di trasferimento trattino il file come binario, non come testo. Quella seconda riga è il motivo per cui un PDF aperto come testo semplice mostra un piccolo guazzabuglio subito dopo la versione.

Il corpo è dove vive il documento. Ogni oggetto indiretto è un numero, una generazione, la keyword obj, un valore e endobj (Spec: ISO 32000-2, §7.3.10). Il valore è una di poche forme — ma due portano quasi tutto il peso:

  • Un dizionario, << /Key value … >>, è una mappa da nomi a valori. L’albero delle pagine, il catalogo, i descrittori di font: tutti dizionari.
  • Uno stream è un dizionario seguito da stream, un blocco di byte arbitrari e endstream. Il contenuto delle pagine, i font incorporati e le immagini sono stream, quasi sempre compressi. (I loro filtri sono una storia a sé, raccontata in streams and filters.)

Gli oggetti puntano l’uno all’altro tramite riferimento indiretto2 0 R significa «oggetto 2, generazione 0». È il cablaggio che trasforma un elenco piatto di oggetti in un grafo.

La sezione dei riferimenti incrociati è la parte che la maggior parte delle persone non immagina correttamente. Nella forma classica è testo semplice: la keyword xref, poi sottosezioni di righe a larghezza fissa di 20 byte (Spec: ISO 32000-2, §7.5.4). Ogni riga è un offset di byte a dieci cifre, una generazione a cinque cifre e un singolo flag — n per in uso, f per libero — riempito fino a esattamente venti byte, così che un lettore possa posizionarsi su qualsiasi voce con la sola aritmetica. PDF 2.0 sostituisce questo con un xref stream: lo stesso indice, ma binario e compresso dentro un oggetto stream marcato /Type /XRef (Spec: ISO 32000-2, §7.5.8). Più piccolo, e capace di descrivere oggetti impacchettati dentro object stream.

Il trailer è l’indice del file (Spec: ISO 32000-2, §7.5.5). Indica /Root — il catalogo del documento, l’unico oggetto da cui pende tutto il resto — e /Size, il conteggio degli oggetti. Poi arriva la stretta di mano che rende possibile la lettura all’indietro: startxref, un offset di byte su una riga a sé, e %%EOF. Un lettore si posiziona alla fine, legge quell’offset, salta direttamente alla sezione dei riferimenti incrociati, e parte.

  1. HeaderUna riga, %PDF-2.0, che indica la versione. Di solito segue un commento marcatore binario.
  2. CorpoOggetti indiretti numerati — dizionari e stream — referenziati con N G R.
  3. Sezione dei riferimenti incrociatiUna tabella di testo di voci da 20 byte, o uno stream /Type /XRef compresso in PDF 2.0.
  4. TrailerIndica /Root e /Size, poi startxref + offset + %%EOF.
  5. Ordine di letturaUn lettore parte da %%EOF, segue startxref fino all'indice, poi raggiunge direttamente ogni oggetto.
Le quattro regioni fisiche di un PDF nell'ordine del file, e il percorso che un lettore prende davvero attraverso di esse — partendo dal trailer e procedendo verso l'interno tramite la sezione dei riferimenti incrociati.

C’è una quinta regione che un file di lunga vita fa crescere: un aggiornamento incrementale. Una modifica non viene scritta sul posto. Gli oggetti modificati, una nuova sezione dei riferimenti incrociati e un nuovo trailer vengono aggiunti dopo il primo %%EOF, e quel nuovo trailer porta /Prev — l’offset della sezione dei riferimenti incrociati precedente (Spec: ISO 32000-2, §7.5.6). Le sezioni formano una catena all’indietro; per qualsiasi numero di oggetto, vince la voce più recente. Poiché i byte originali non si spostano mai, il controllo crittografico sull’intervallo di byte che una firma effettivamente copre regge ancora dopo un aggiornamento. Un successivo aggiornamento incrementale può comunque cambiare ciò che un validatore riporta sul documento nel suo insieme — se le modifiche successive alla firma sono consentite, e cosa si ritiene che la firma certifichi — ma non può alterare i byte firmati stessi. Quella proprietà è l’intero argomento di incremental updates.

Ecco l’intera anatomia in un solo file minimo. I numeri sotto xref sono offset di byte, e devono essere esatti — punta un carattere oltre il punto in cui un oggetto inizia e un lettore rigoroso si arrende.

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
186
%%EOF

Leggilo nel modo in cui lo fa un parser. Ultima riga: %%EOF. Sopra di essa: startxref 186, quindi posizionati al byte 186, dove inizia xref. La tabella dice che l’oggetto 1 vive al byte 9. Il /Root 1 0 R del trailer punta lì — il catalogo — e dal catalogo percorri /Pages fino all’albero delle pagine e trovi la singola pagina. L’oggetto 0 è sempre la testa della free-list con generazione 65535, un fossile del primo design del formato che ogni lettore si aspetta ancora di vedere.

La trappola è leggere un PDF come una storia — dall’alto in basso, in ordine. Non è una storia; è un indice con un puntatore all’indietro. I numeri di oggetto non devono essere sequenziali nel file, gli oggetti possono comparire in qualsiasi ordine fisico, e un lettore non si affida mai alla loro posizione. L’unica mappa autorevole è la sezione dei riferimenti incrociati, e l’unico modo per trovare quella mappa è l’offset startxref proprio in fondo.

La conseguenza sorprende le persone. Un PDF con un corpo impeccabile e una sola cifra sbagliata in startxref è illeggibile — il lettore non riesce a trovare l’indice. Un PDF con i suoi oggetti in ordine sparso ma con una sezione dei riferimenti incrociati corretta va perfettamente bene. La posizione fisica non porta alcun significato. La posizione registrata lo porta tutto.

Questa pagina descrive la struttura fisica, non il contenuto della pagina. Come i segni atterrano su una pagina — operatori del content stream, mostrare testo, stato grafico — è un argomento separato. Descrive anche un file ben formato. I PDF del mondo reale sono spesso un po’ rotti e sopravvivono solo perché i visualizzatori indulgenti ricostruiscono la tabella dei riferimenti incrociati scandagliando alla ricerca delle keyword obj. Quel recupero è un comportamento del visualizzatore, non qualcosa che il formato garantisce.

Reading and repairing arbitrary third-party PDFs — edition availability
EditionAvailability
CoreNextPDF è un writer. Registra ogni offset dal buffer di output nel momento in cui ciascun oggetto è emesso, così che i file che produce abbiano una sezione dei riferimenti incrociati che corrisponde al corpo per costruzione.
ProL’analisi, la ricostruzione o la riparazione di una tabella dei riferimenti incrociati danneggiata in un file che NextPDF non ha scritto è fuori dall’ambito in ogni edizione.
EnterprisePer l’ispezione della struttura di un file esistente, usa un parser o un validatore dedicato; NextPDF garantisce la correttezza di ciò che scrive, non di ciò che legge.

Perché un PDF ha una riga marcatore binario dopo l’header? Alcuni strumenti di trasferimento più datati storpiavano un file che credevano fosse testo semplice. Il commento a bit alto fa apparire il file inequivocabilmente binario, così che sopravviva al viaggio immutato.

L’xref stream è solo una tabella più piccola? Per lo più, con un potere in più. Oltre a essere compresso, un xref stream può descrivere oggetti memorizzati dentro gli object stream — voci che la classica tabella di testo da 20 byte non ha modo di esprimere.

Posso avere sia una tabella che uno stream in un solo file? Una singola revisione usa l’una o l’altro. Ma un file ibrido può abbinare una tabella classica per i vecchi lettori a un xref stream per quelli nuovi, così che ogni tipo di lettore trovi un indice che comprende.

  • Header — la prima riga, %PDF-2.0, che indica la versione; di solito seguita da un commento marcatore binario.
  • Oggetto indiretto — un oggetto numerato nel corpo, scritto N G obj … endobj, dove N è il numero di oggetto e G la generazione.
  • Dizionario — una mappa << /Key value … >> da nomi a valori; la forma di oggetto più comune.
  • Stream — un dizionario più un blocco di byte tra stream ed endstream, usato per contenuto, font e immagini.
  • Tabella dei riferimenti incrociati (xref) — l’indice dal numero di oggetto all’offset di byte; una tabella di testo da 20 byte per voce nella forma classica, uno stream /Type /XRef in PDF 2.0.
  • Trailer — il dizionario che indica /Root e /Size, individuato tramite l’offset startxref alla fine del file.
  • Aggiornamento incrementale — oggetti modificati, una nuova sezione dei riferimenti incrociati e un nuovo trailer aggiunti dopo %%EOF, con /Prev che si concatena alla sezione precedente.