Scrivi un piccolo validatore. Apre un PDF, cerca fino alla fine, trova startxref, legge l'offset e si aspetta di atterrare sulla parola chiave xref con una tabella di riferimenti incrociati a larghezza fissa al di sotto di essa. Da quella tabella raccoglie gli offset degli oggetti, quindi scansiona all'indietro cercando la parola chiave trailer per scoprire la /Root e la /Size. Funziona perfettamente su ogni file che hai generato per testarlo. Poi arriva un file prodotto da una versione attuale di Word, o da una libreria che ha come target PDF 1.5, e il validatore lo dichiara rotto. Non c'è alcuna parola chiave xref nel punto indicato dall'offset, nessun dizionario trailer da nessuna parte, e la tabella degli oggetti costruita dal validatore è quasi vuota. Il file è valido. È il validatore che lo sta leggendo attraverso una lente vecchia di quindici anni
Questo è in assoluto il motivo più comune per cui un controllo PDF a livello di byte, scritto basandosi sul layout classico, fallisce sui documenti moderni. La struttura da cui dipende, ovvero la tabella dei riferimenti incrociati in testo normale e la parola chiave trailer, è stata resa opzionale nel PDF 1.5 ed è frequentemente assente. Due funzionalità l'hanno sostituita: lo stream dei riferimenti incrociati (cross-reference stream) e lo stream degli oggetti compressi. Entrambi sono descritti nello standard ISO 32000-1, e un validatore che non li conosce vede un file sano come un mucchio di oggetti mancanti
Cosa ha cambiato PDF 1.5 nella parte finale del file
La sezione §7.5.8 dell'ISO 32000-1 definisce lo stream dei riferimenti incrociati, e la §7.5.7 definisce lo stream degli oggetti di tipo /ObjStm. Insieme, consentono a chi scrive un file di tralasciare le due strutture su cui fa affidamento un parser classico. Un file PDF 1.5 potrebbe terminare senza alcuna tabella xref. Al suo posto, l'oggetto a cui punta startxref è un normale oggetto stream il cui dizionario riporta /Type /XRef, e quello stream contiene i dati dei riferimenti incrociati in una forma binaria compatta. Non c'è nemmeno la parola chiave trailer, perché il trailer ora è il dizionario stesso dello stream. Le chiavi a cui un parser classico dava la caccia, /Root, /Size e /ID, risiedono all'interno di quel dizionario
Il secondo cambiamento sposta gli oggetti stessi. Invece di scrivere ogni oggetto indiretto al proprio offset di byte, si possono impacchettare molti piccoli oggetti – i dizionari delle pagine, i dizionari delle annotazioni, l'albero della struttura – in un singolo stream di oggetti e comprimere l'intero contenitore con Flate. I singoli oggetti non hanno più un offset di byte nel file. Hanno una posizione all'interno di un blob compresso. Un validatore che analizza i byte grezzi in cerca di 1 0 obj non li trova mai, perché quel testo esiste solo dopo la decompressione (inflation). Per un parser classico, metà del documento è semplicemente svanita nel nulla
Le chiavi del trailer sono in testo normale, anche in un file compresso
La parte rassicurante è che leggere il trailer di uno stream di riferimenti incrociati non richiede di decomprimere nulla. Un oggetto stream è scritto come un dizionario seguito dalla parola chiave stream e quindi dai byte compressi. Il dizionario è in testo normale. Quindi quando startxref punta a uno stream di riferimenti incrociati, i byte immediatamente successivi al numero dell'oggetto sembrano un normale dizionario, e /Root, /Size e /ID si trovano lì in chiaro, prima che inizino la parola chiave stream e i dati Flate
Ciò significa che un validatore può apprendere i tre fatti di cui ha più bisogno – dove si trova il catalogo, quanti oggetti il file dichiara di avere, e l'identificatore del file – analizzando solo il dizionario dello stream. Non deve decomprimere i dati dei riferimenti incrociati e non deve interpretare le voci binarie al suo interno. Il lavoro che sconfigge un parser ingenuo non è la lettura del trailer; è la ricerca degli oggetti. Si tratta di due problemi separabili, e risolvere il primo costa poco
Stream di oggetti: un'intestazione, poi un blob Flate
Uno stream di oggetti è un contenitore. Il suo dizionario riporta /Type /ObjStm, una voce /N che indica il numero di oggetti impacchettati all'interno, e una voce /First che fornisce l'offset in byte, all'interno dei dati decompressi, dove inizia il corpo del primo oggetto. Il payload compresso, una volta decompresso, inizia con una piccola intestazione di /N coppie di interi. Ogni coppia è un numero di oggetto e l'offset del corpo di quell'oggetto rispetto a /First. Dopo l'intestazione arrivano i corpi degli oggetti veri e propri, concatenati
L'espansione di uno di questi è meccanica una volta che i byte sono stati decompressi. Leggi il dizionario per ottenere /N e /First, decomprimi lo stream con un decodificatore Flate, scorri le prime /N coppie per capire quale numero di oggetto risiede a quale offset, e infine estrai ogni corpo come se fosse un normale oggetto indiretto. L'unica vera dipendenza è il decodificatore Flate, e ne hai già uno: Delphi include System.ZLib, e Free Pascal include l'unità zstream, ed entrambi incapsulano zlib e decomprimono un flusso Flate grezzo senza alcun codice di terze parti. Una routine che aggiunge ogni oggetto estratto alla tabella degli oggetti del validatore farà in modo che il resto del validatore, ovvero la parte che scorre /Root e controlla l'albero delle pagine, si comporti esattamente come farebbe su un file classico
Cosa non devi implementare
È facile sopravvalutare il lavoro necessario. Leggere le chiavi del trailer da un file compresso non richiede la decodifica delle voci binarie dello stream dei riferimenti incrociati. Lo stream dei riferimenti incrociati della sezione §7.5.8 utilizza tre tipi di voce, e la voce di tipo 2, quella che dice questo oggetto risiede all'interno dello stream di oggetti N all'indice i
, è ciò che dovresti decodificare per costruire una mappa completa degli offset. Ti serve quella mappa per risolvere oggetti arbitrari in base al numero. Non ti serve per leggere /Root, /Size e /ID, che si trovano nel dizionario in testo normale, e non ti serve per espandere gli stream di oggetti, perché ogni /ObjStm annuncia i propri contenuti tramite /N e /First
Inoltre, non devi gestire le funzioni dei predittori PNG e TIFF che uno stream di riferimenti incrociati può applicare tramite il suo /DecodeParms solo per ottenere le chiavi del trailer. I predittori filtrano le righe binarie dei riferimenti incrociati per farle comprimere meglio; non hanno nulla a che fare con il dizionario che precede lo stream. L'aggiornamento minimo che rende un validatore classico consapevole dei PDF moderni è pertanto ridotto: quando startxref atterra su uno stream invece che sulla parola chiave xref, analizza il dizionario dello stream per le chiavi del trailer, ed espandi qualsiasi oggetto /ObjStm che incontri in modo che i suoi contenuti entrino nella tabella degli oggetti. Decodificare le voci di tipo 2 e i predittori è un'attività separata e più ampia che puoi rimandare fino a quando non avrai realmente bisogno della risoluzione casuale degli oggetti
Perché un controllo di conformità deve espandere prima gli stream
Questo smette di essere un discorso accademico nel momento in cui esegui un controllo del profilo. Un validatore PDF/A o PDF/X ispeziona oggetti specifici: il catalogo del documento per un array /OutputIntents, lo stream /Metadata per un pacchetto XMP con il giusto identificatore, ogni descrittore di font per un file di font incorporato, il trailer per un /ID. In un file compresso, la maggior parte di questi oggetti si trova all'interno di stream di oggetti. Un validatore che non ha espanso gli stream di oggetti non può vedere le chiavi del catalogo, non può trovare i metadati e non può enumerare i font. Segnalerà un documento perfettamente conforme come mancante del suo intento di output (output intent), mancante del suo XMP e mancante di metà della sua struttura, perché le prove di cui ha bisogno si trovano ancora in un blob Flate che non ha mai decompresso
L'ordine è importante. L'espansione deve avvenire prima dell'esecuzione dei controlli, non in parallelo, perché ogni controllo presuppone di poter raggiungere un oggetto in base al numero. Se si collega un controllo del profilo direttamente a una scansione di byte grezzi, esso eredita la cecità del parser classico e produce false violazioni proprio su quei file moderni che hanno maggiori probabilità di essere ben formattati, in quanto provengono da toolchain abbastanza recenti da poter scrivere innanzitutto stream di riferimenti incrociati
Lasciare che PDFium faccia il parsing per te
Il Componente PDFium analizza gli stream dei riferimenti incrociati e gli stream degli oggetti come parte del caricamento di un documento, che è il modo pratico per evitare di dover scrivere a mano il passaggio di decompressione ed espansione. Quando carichi un file con il componente TPdf, gli oggetti impacchettati nei contenitori /ObjStm sono già risolti e i punti di ingresso della validazione vedono il documento completamente espanso. ValidatePdfA restituisce un record TPdfAValidationResult il cui campo Conformance è un valore TPdfAConformance come pac1b o pacNone, il cui campo Issues è un set dei problemi specifici trovati e il cui metodo IsCompliant è vero solo quando è stato rilevato un livello di conformità e il set di problemi è vuoto. Poiché gli oggetti sono stati espansi durante il caricamento, un array /OutputIntents o un font incorporato che viveva all'interno di uno stream di oggetti viene regolarmente trovato, non segnalato come mancante
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
Lo stesso vale per ValidatePdfX, che restituisce un TPdfXValidationResult con la medesima struttura. Lo scopo di passare tramite PDFium è che la decompressione strutturale descritta sopra avviene una volta, in modo corretto, all'interno del caricatore (loader), per cui il tuo codice di validazione non nota mai la differenza tra un file classico e uno completamente compresso. Entrambi arrivano al validatore come un set di oggetti risolti
function PdfXConformanceName(C: TPdfXConformance): string;
begin
case C of
pxc1a: Result := 'PDF/X-1a';
pxc3 : Result := 'PDF/X-3';
pxc4 : Result := 'PDF/X-4';
else
Result := 'none';
end;
end;
var
Pdf: TPdf;
R : TPdfXValidationResult;
Issue: TPdfXValidationIssue;
IssueCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', PdfXConformanceName(R.Conformance))
else
begin
IssueCount := 0;
for Issue in R.Issues do // Issues is a set: count its members
Inc(IssueCount);
Writeln('Not conformant; issue count = ', IssueCount);
end;
finally
Pdf.Free;
end;
end;
Se i byte sono già in memoria piuttosto che su disco, la stessa sequenza carica-e-valida funziona tramite l'overload LoadDocument(const Data: TBytes), che accetta il contenuto grezzo del file e analizza i suoi stream di riferimenti incrociati e oggetti nello stesso modo in cui lo fa dal percorso su file. La lezione per un validatore scritto a mano è la regola strutturale, non l'API: leggi le chiavi del trailer dal dizionario dello stream in testo normale, espandi ogni /ObjStm con un decodificatore Flate prima di scorrere il documento, e tratta la decodifica delle voci binarie dei riferimenti incrociati come l'operazione più ampia e opzionale quale in effetti è
Una volta espansa la struttura, un validatore può guidare il resto del flusso di lavoro su di essa. Per uno strumento di preflight a riga di comando che segnala la conformità in un'intera cartella di input, vedi la nostra guida passo passo sulla creazione di una CLI di report preflight in batch. Quando la validazione è un passaggio obbligato prima di suddividere un documento di grandi dimensioni, le tecniche contenute nella nostra guida su come dividere i documenti PDF in file multipli si abbinano naturalmente al modello carica-e-controlla mostrato qui. Entrambi si basano sulla superficie di caricamento e validazione del Componente PDFium per Delphi e C++Builder