Articolo tecnico

Costruire un semplice documento PDF da zero: I cinque oggetti di cui hai bisogno

Un PDF è essenzialmente un contenitore di testo normale. Apri la maggior parte dei file in un editor esadecimale e la parte superiore è leggibile: un commento sulla versione, poi una serie di oggetti numerati, quindi un piccolo indice e un puntatore in fondo che dice al lettore da dove iniziare. Rimuovendo la compressione, il formato è abbastanza accessibile da poter digitare un documento funzionante in un editor di testo e farlo aprire da un visualizzatore. Farlo una volta insegna di più su come è strutturato il PDF rispetto a qualsiasi lettura delle specifiche, perché devi collegare gli oggetti tra loro manualmente e il file si rifiuta di aprirsi finché i collegamenti non sono corretti

Questa guida dettagliata costruisce il PDF più piccolo in grado di renderizzare qualcosa: una pagina, le parole "Hello, World!" in un font integrato, su carta in formato US Letter. Il file finito richiede esattamente cinque oggetti e alcune righe di contabilità intorno ad essi. Scriveremo prima gli oggetti, poi assembleremo l'intestazione, la tabella dei riferimenti incrociati e il trailer che li collegano in un file accettabile per un lettore

I cinque oggetti richiesti da un visualizzatore

Un lettore non scansiona un PDF dall'alto verso il basso cercando contenuti. Inizia dal trailer, segue un riferimento al catalogo del documento e da lì percorre una catena di oggetti. Ogni oggetto su quella catena deve esistere o l'apertura fallisce. Per un documento di una pagina la catena è breve e ogni anello ha un solo compito:

  • Catalog (Catalogo) è la radice. È l'oggetto a cui punta il trailer e la sua unica voce richiesta qui è un riferimento all'albero delle pagine
  • Pages (Pagine) è il nodo dell'albero delle pagine. Elenca le pagine del documento e segnala quante sono
  • Page (Pagina) descrive una pagina fisica: le sue dimensioni, le risorse con cui disegna e quale flusso di contenuti la dipinge
  • Content stream (Flusso di contenuti) contiene gli operatori di disegno, i comandi postfix che posizionano testo e grafica su quella pagina
  • Font (Carattere) dichiara il carattere tipografico a cui fa riferimento il flusso di contenuti. Usa uno dei 14 font standard e non dovrai incorporare nulla

Ogni oggetto è numerato e indirizzabile. Un oggetto indiretto è scritto come N 0 obj ... endobj, dove N è il numero dell'oggetto e 0 è il suo numero di generazione (sempre 0 in un file creato ex novo). In qualsiasi altra parte del file, si punta a quell'oggetto con un riferimento: 5 0 R significa "oggetto 5". Quei riferimenti sono i collegamenti. Il catalogo contiene 2 0 R nella nostra numerazione per raggiungere l'albero delle pagine, l'albero delle pagine contiene un riferimento verso il basso alla pagina e così via. Sbaglia un numero e il lettore seguirà un puntatore non valido verso il nulla

Nomi, dizionari e flussi

Tre elementi di sintassi trasportano quasi tutto. Un nome inizia con una barra: /Type, /Page, /F0. I nomi sono identificatori case-sensitive, non stringhe, e il PDF li utilizza per le chiavi dei dizionari e per etichettare cosa sia un oggetto. Un dizionario è un insieme di coppie chiave-valore racchiuse in doppie parentesi angolari, dove ogni chiave è un nome: << /Type /Page /MediaBox [0 0 612 792] >>. I valori possono essere numeri, nomi, array tra parentesi quadre, riferimenti o dizionari annidati. La maggior parte degli oggetti PDF sono dizionari

Un flusso è un dizionario seguito da un blocco di byte tra le parole chiave stream ed endstream. È lì che si trovano gli operatori per disegnare la pagina e, nei file reali, dove risiedono anche le immagini compresse e i font incorporati. Il dizionario del flusso descrive i byte; in un file di produzione deve contenere una voce /Length che fornisce il conteggio esatto dei byte e spesso un /Filter come /FlateDecode quando i dati sono compressi. Ci affideremo a uno strumento per compilare /Length, perché contare i byte a mano è la parte di questo esercizio senza alcun vantaggio educativo e con un'alta probabilità di un errore off-by-one che corromperebbe il file

Scrittura degli oggetti

Ecco i cinque oggetti in ordine. Il dettaglio delle coordinate da tenere a mente prima di leggere il flusso dei contenuti: il PDF misura dall'angolo in basso a sinistra della pagina in punti, dove un punto è 1/72 di pollice e Y cresce verso l'alto. Una pagina US Letter è 612 per 792 punti, quindi 50 700 si trova vicino in alto a sinistra, non in basso

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]
   /Resources << /Font << /F0 4 0 R >> >>
   /Contents 5 0 R
>>
endobj

4 0 obj
<< /Type /Font
   /Subtype /Type1
   /BaseFont /Helvetica
>>
endobj

5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj

Leggi i riferimenti e la struttura apparirà chiara. L'oggetto 1, il catalogo, punta la sua voce /Pages all'oggetto 2. L'oggetto 2, l'albero delle pagine, elenca l'oggetto 3 in /Kids e dichiara /Count 1. L'oggetto 3, la pagina, punta /Parent di nuovo verso l'alto all'oggetto 2 (l'albero e la pagina si fanno riferimento a vicenda, il che è richiesto), dimensiona sé stesso con /MediaBox, espone il font sotto il nome locale /F0 nelle sue /Resources e nomina l'oggetto 5 come suo contenuto. L'oggetto 4 è il font: /BaseFont /Helvetica seleziona uno dei 14 caratteri standard che ogni lettore conforme già possiede, quindi non c'è nulla da incorporare. L'oggetto 5 è il flusso di contenuti

Cosa dice effettivamente il flusso di contenuti

Il corpo del flusso è un minuscolo programma nel linguaggio di descrizione della pagina del PDF, che è postfix: gli operandi vengono per primi, poi l'operatore che li consuma. Cinque righe fanno il lavoro. BT ed ET aprono e chiudono un oggetto di testo; tutto ciò che posiziona o mostra il testo deve trovarsi tra di essi. /F0 36 Tf imposta il font corrente sulla risorsa denominata /F0 a 36 punti (Tf sta per "set text font and size"). 50 700 Td sposta la posizione del testo a (50, 700) nelle coordinate della pagina. (Hello, World!) Tj mostra la stringa, che il PDF scrive come testo letterale tra parentesi, usando Tj per disegnarla nella posizione corrente. Se ometti BT/ET un lettore rigoroso rifiuta gli operatori di testo; se dimentichi di impostare un font prima di Tj non ci sarà un font corrente con cui disegnare

La /Length 44 nel dizionario del flusso è il conteggio dei byte tra stream ed endstream e deve essere esatto. Questo è il valore che vale la pena passare a uno strumento piuttosto che contare le interruzioni di riga a mano, specialmente poiché se l'editor scrive le terminazioni di riga come LF o CRLF cambia il totale

Intestazione, xref e trailer

Gli oggetti sono il contenuto. Tre pezzi strutturali li trasformano in un file. Il primo è l'intestazione, la primissima riga, che nomina il formato e la versione:

%PDF-1.7

Il % inizia un commento nella sintassi PDF, ma un lettore tratta questo commento in particolare come la firma del formato e ne legge la versione. Un vero scrittore lo fa seguire immediatamente da una seconda riga di commento con byte ad alto bit, un suggerimento per gli strumenti di trasferimento file che il file è binario e non deve essere alterato come testo

Alla fine del file si trova la tabella dei riferimenti incrociati, l'indice che rende possibile l'accesso casuale. Registra l'offset di byte di ogni oggetto dall'inizio del file, in modo che un lettore possa cercare direttamente l'oggetto 3 senza prima analizzare gli oggetti 1 e 2. La tabella è rigida: le voci sono a larghezza fissa, 20 byte ciascuna inclusa la terminazione di riga, formattate come un offset di 10 cifre, una generazione di 5 cifre, una parola chiave (n per in-uso, f per libero) e un terminatore di due byte. Una tabella corretta per le nostre sei voci (l'oggetto 0 è sempre la testa della lista libera) è simile a questa:

xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
   /Root 1 0 R
>>
startxref
408
%%EOF

Quegli offset sono la parte delicata della scrittura manuale di un PDF. Ognuno è l'esatta posizione in byte in cui inizia il corrispondente N 0 obj e ogni offset si sposta nel momento in cui aggiungi un carattere ovunque sopra di esso. Il trailer è il punto di ingresso che un lettore usa per ultimo e per primo: /Root 1 0 R nomina il catalogo, /Size 6 dichiara il conteggio degli oggetti e startxref 408 fornisce l'offset in byte della parola xref stessa. Un lettore apre il file, salta alla fine, legge startxref, cerca la tabella dei riferimenti incrociati e da lì raggiunge il catalogo e tutto ciò che si trova al di sotto. %%EOF segna l'ultimo byte

Lasciare che uno strumento corregga i conteggi dei byte

Gli offset sopra riportati sono illustrativi; in pratica saranno sbagliati non appena avrai finito di scrivere, perché dipendono dall'esatta disposizione dei byte del tuo file. Piuttosto che ricalcolarli, scrivi la struttura con valori segnaposto e lascia che un'utilità ricostruisca la tabella dei riferimenti incrociati e le lunghezze dei flussi. Lo strumento gratuito e multipiattaforma pdftk fa questo in un solo passaggio:

pdftk hello-draft.pdf output hello.pdf

Analizza i tuoi oggetti, ricalcola ogni offset in byte, inserisce i valori /Length corretti, scrive una tabella xref e un trailer validi ed emette hello.pdf. Aprilo in qualsiasi visualizzatore e otterrai una pagina con "Hello, World!" in Helvetica 36 punti vicino all'inizio. Qpdf svolge lo stesso lavoro e molti visualizzatori ripareranno al volo un file leggermente malformato. Il punto di affidarsi a uno strumento qui non è la pigrizia; è che l'aritmetica degli offset è l'unica parte del formato con zero contenuto concettuale e il tasso di errore più alto, quindi la sua automazione permette di concentrarsi sull'apprendimento della struttura

Perché questo si adatta a documenti reali

Niente di un report di cento pagine cambia la forma che hai appena costruito. Il catalogo si trova ancora alla radice, l'albero delle pagine raccoglie ancora le pagine e ogni pagina punta ancora alle sue risorse e a un flusso di contenuti. Ciò che cresce è l'ampiezza, non la struttura portante: l'albero delle pagine si ramifica in modo che un lettore possa saltare interi sottoalberi, i flussi di contenuto trasportano centinaia di operatori invece di cinque, i font vengono incorporati come propri oggetti stream con tabelle di larghezza e codifiche e le immagini arrivano come flussi con filtri specifici. I file moderni tendono inoltre a raggruppare molti oggetti in flussi di oggetti compressi e a sostituire la semplice tabella xref con un flusso di riferimenti incrociati, motivo per cui l'apertura di un vero PDF in un editor di testo di solito mostra un muro di dati binari. Il modello sottostante è identico a quello del tuo file fatto a mano. Per il grafo degli oggetti più ampio e per come il catalogo, l'albero delle pagine e i dizionari delle risorse si relazionano in un documento più grande, il tour approfondito della struttura del documento PDF riprende da dove questo si ferma, e la panoramica della struttura del file copre gli aggiornamenti incrementali e il modo in cui il trailer si concatena attraverso le revisioni

Dalla scrittura a mano a una libreria

Digitare gli oggetti a mano è un esercizio di apprendimento, non una tecnica di produzione. Nell'istante in cui hai bisogno di font reali, testo a capo, immagini o qualcosa di più di una pagina banale, la contabilità dei byte che pdftk ha corretto per te diventa l'intero lavoro, e vorrai una libreria che se ne occupi. I medesimi cinque oggetti vengono ancora scritti, ma una libreria calcola ogni offset, gestisce i dizionari di font e risorse e comprime i flussi di contenuti senza che tu tenga traccia di un singolo byte. In Delphi e C++Builder, l'HotPDF Component riduce l'intero file a una manciata di chiamate: imposta il documento, chiama BeginDoc, SetFont e TextOut per posizionare lo stesso saluto, quindi EndDoc per scrivere un catalogo, un albero delle pagine, un xref e un trailer corretti. Capire gli oggetti sottostanti è ciò che ti permette di ragionare sull'output quando un documento non si renderizza come ti aspettavi