Technical Article

Caricamento di PDF con riferimenti ibridi da Word ed Excel in Delphi

Apri un PDF prodotto da Microsoft Word o Excel, sfoglialo e nulla sembrerà insolito. Caricalo in un programma Delphi, leggi il numero di pagine e il valore sarà corretto. Poi salvalo di nuovo con la crittografia attiva e l'operazione fallirà con un errore EListError, oppure l'output mostrerà un avviso di riferimento incrociato danneggiato. Il file non è mai stato corrotto. Si tratta di un file a riferimento ibrido, e la struttura stessa che consente a un visualizzatore di quindici anni fa di aprirlo è la struttura che mette in difficoltà un loader che interrompe la lettura troppo presto.

Questo è uno dei modi più comuni in cui una pipeline PDF che ha superato ogni test interno incontra un file che non riesce a salvare correttamente. Gli input venivano tutti generati internamente, quindi non erano mai ibridi. Il primo file ibrido arriva il giorno in cui un cliente inoltra una fattura esportata da un foglio di calcolo.

Cosa scrivono effettivamente Word ed Excel

La norma ISO 32000-1 descrive il layout dei riferimenti ibridi al paragrafo §7.5.8.4. Un'applicazione che necessita di funzionalità PDF 1.5 come gli stream di oggetti, consentendo comunque a un lettore PDF 1.4 di aprire il file, scrive le informazioni di riferimento incrociato due volte. Esiste una classica tabella di riferimento incrociato, le righe ASCII a larghezza fissa che chiudevano ogni PDF fino alla versione 1.4, e c'è uno stream di riferimento incrociato che indicizza il resto. Il trailer della sezione classica contiene una voce /XRefStm il cui valore è l'offset in byte di tale stream.

La divisione del lavoro è deliberata. Gli oggetti che un vecchio lettore deve raggiungere, tra cui il catalogo e l'albero delle pagine, sono indirizzabili dalla tabella classica. Gli oggetti raggruppati in stream di oggetti compressi sono contrassegnati come liberi nella tabella classica, con una voce di tipo f, in modo che un lettore 1.4 li salti direttamente e non si imbatta mai in una struttura che non può analizzare. Le loro posizioni reali risiedono solo nello stream di riferimento incrociato. La firma di un file di questo tipo è la sua coda: una breve sezione classica, spesso nient'altro che xref seguito da un'intestazione di sottosezione 0 0, il cui trailer punta al /XRefStm in cui risiedono i dati di ripristino effettivi.

Perché un conteggio corretto delle pagine non prova nulla

Poiché il catalogo e l'albero delle pagine sono deliberatamente raggiungibili dalla tabella classica, un loader che legge solo tale tabella trova /Root, scorre l'albero delle pagine e restituisce il numero corretto di pagine. Tutto ciò di cui un vecchio lettore ha bisogno è presente, quindi il file appare integro. Gli oggetti mancanti sono quelli racchiusi negli stream di oggetti: dizionari di campi AcroForm, elementi strutturali di PDF taggati e la lunga serie di piccoli dizionari che non dovevano essere visibili a un visualizzatore legacy.

Non si nota questa mancanza finché qualcosa non tocca tali oggetti, e un salvataggio completo li tocca tutti. Scorrere il documento per crittografarlo nuovamente o riscriverlo è esattamente l'operazione che richiede a turno ogni numero di oggetto, motivo per cui il sintomo emerge al momento del salvataggio anziché del caricamento, lontano dalla sua causa originaria.

La trappola è un rilevatore che vede xref e si ferma

Il modo più semplice per stabilire come sia indicizzato un file è seguire startxref ed esaminare i primi byte a cui punta. La parola chiave xref indica una tabella classica; un oggetto stream indica uno stream di riferimento incrociato. Questo test è corretto per qualsiasi file che adotti un solo schema. È invece errato per un file ibrido, il cui startxref punta a una sezione classica al solo scopo di soddisfare i vecchi lettori, mentre il /XRefStm nel trailer di quella sezione è il punto in cui viene effettivamente indicizzata la maggior parte del documento. Un rilevatore che restituisce "classic" al primo xref che incontra non leggerà mai /XRefStm, e ogni oggetto che risiede solo nello stream diventerà invisibile.

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count is correct
    // inspect or edit the loaded document here
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // walks every object
  finally
    Pdf.Free;
  end;
end;

Con il rilevatore a uscita anticipata attivo, il caricamento sembra corretto ed è al momento del salvataggio che gli oggetti assenti si fanno notare. La soluzione non consiste nel leggere più byte all'inizio, ma nel riconoscere il trailer ibrido e seguire /XRefStm prima di considerare completato il file.

L'ordine di unione non è negoziabile

Una volta letti entrambi gli indici, questi possono essere combinati in un'unica direzione. Lo stream di riferimento incrociato deve essere unito per primo, con le voci classiche organizzate attorno a esso. Il motivo risiede nel piccolo inganno alla base del formato. Un file ibrido contrassegna i suoi oggetti compressi come liberi nella tabella classica, in modo che i vecchi lettori li ignorino. Un loader che segue la politica in cui la prima voce trovata prevale, leggendo prima la tabella classica registrerà quei numeri di oggetto come liberi, scartando poi le voci dello stream che effettivamente li posizionano, poiché gli slot sono già occupati. Invertendo l'ordine, le voci di tipo 2 dello stream (ognuna delle quali rappresenta un numero di stream di oggetti più un indice) ottengono gli slot che devono possedere, e le voci classiche si sistemano intorno ad esse.

La stessa regola protegge dal rischio che una revisione precedente ripristini un oggetto eliminato. Gli aggiornamenti incrementali si collegano a ritroso tramite /Prev, e una voce libera di tipo 0 è una sentinella che indica che una sezione più recente ha disattivato un numero di oggetto. A una sezione successiva, ma più vecchia nella catena, non deve essere consentito di sovrascrivere tale sentinella con una posizione obsoleta. Se si considera la prima occorrenza rilevata come autorevole per i marcatori liberi, l'oggetto eliminato rimane tale; se si agisce con leggerezza, la cronologia del file riattiverà contenuti rimossi dall'ultima revisione.

Cosa significa questo in HotPDF

Il motore risolve i file con riferimenti ibridi al posto tuo, e lo fa in ogni percorso in cui deve analizzare i dati di riferimento incrociato. Carica un documento con LoadFromFile o LoadFromStream, apporta le modifiche e chiama SaveLoadedDocument; oppure esegui un'operazione rapida come EncryptFile che legge un input e scrive un output. In ogni caso, il ripristino legge /XRefStm, unisce la sezione dello stream prima delle voci classiche e risolve gli oggetti che risiedono negli stream prima che la scrittura li elenchi. Il percorso di crittografia AES-256 è il punto in cui il problema si è manifestato per la prima volta, poiché la crittografia di un documento riscrive ogni oggetto e richiede quindi che ogni oggetto sia già stato posizionato.

// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Il dettaglio importante da ricordare si trova a monte dell'API. I file generati da Word, Excel, PowerPoint e da una lunga lista di pipeline "Salva come PDF" sono normalmente ibridi, quindi un loader testato solo sull'output del proprio generatore potrebbe non incontrarne mai uno in fase di test. Inserisci nei tuoi test documenti esportati da reali applicazioni Office, non solo file generati dal tuo stesso codice.

Verifica di un file sospetto

Due verifiche possono chiarire rapidamente la questione. Apri il file in un visualizzatore esadecimale e leggi i byte successivi all'ultimo startxref; un file ibrido mostra una breve sezione classica il cui dizionario trailer contiene /XRefStm. In alternativa, confronta il conteggio degli oggetti restituito da un'analisi completa con il numero di oggetto più alto dichiarato da /Size nel trailer. Un divario significativo indica che vi sono oggetti nascosti in stream che il loader non ha aperto, lo stesso problema che si trasforma in un errore in fase di salvataggio.

La prospettiva del writer, ovvero come vengono prodotti in primo luogo gli stream di oggetti e i riferimenti incrociati compressi, è trattata nel nostro articolo sugli stream di oggetti e aggiornamenti incrementali. Quando il file ibrido in questione è anche molto grande, le tecniche di caricamento descritte nella guida alle API Direct File per flussi di lavoro PDF di grandi dimensioni consentono di esaminarlo senza doverlo caricare interamente in memoria. Entrambe si integrano con il ripristino descritto qui, fornito come parte di HotPDF Component per Delphi e C++Builder alongside the loading, editing, encryption, and signing APIs covered elsewhere on this blog.