Articolo tecnico

Caricamento di PDF con Riferimenti Ibridi da Word ed Excel in Delphi

Apri un PDF prodotto da Microsoft Word o Excel, scorri le pagine e nulla sembra insolito. Caricalo in un programma Delphi, leggi il conteggio delle pagine e il numero è corretto. Poi salvalo di nuovo con la crittografia attivata e il lavoro fallisce con un EListError, o l'output si apre con un avviso di riferimenti incrociati danneggiati. Il file non è mai stato corrotto. È un file con riferimenti ibridi, e la struttura stessa che consente a un visualizzatore di quindici anni fa di aprirlo è la struttura che sconfigge un caricatore che smette di leggere troppo presto

Questo è uno dei modi più comuni in cui una pipeline PDF che ha superato ogni test interno incontra un file che non può gestire in un ciclo completo. Gli input sono stati tutti generati internamente, quindi non sono mai stati 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

ISO 32000-1 descrive il layout a riferimenti ibridi nel §7.5.8.4. Un'applicazione che desidera le funzionalità di PDF 1.5 come gli stream di oggetti, pur permettendo a un lettore PDF 1.4 di aprire il file, scrive le informazioni dei riferimenti incrociati due volte. C'è una tabella classica dei riferimenti incrociati, le righe ASCII a larghezza fissa che terminavano ogni PDF fino alla versione 1.4, e c'è uno stream di riferimenti incrociati che indicizza il resto. Il trailer della sezione classica contiene una voce /XRefStm il cui valore è l'offset in byte di quello 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 che sono stati 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 a piè pari e non inciampi mai in una struttura che non può analizzare. Le loro posizioni reali risiedono solo nello stream dei riferimenti incrociati. La firma di un file del genere è 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 recupero effettivi

Perché un conteggio delle pagine corretto non prova nulla

Poiché il catalogo e l'albero delle pagine sono raggiungibili di proposito dalla tabella classica, un caricatore che legge solo quella tabella trova /Root, percorre l'albero delle pagine e riporta il numero corretto di pagine. Tutto ciò di cui un vecchio lettore ha bisogno è presente, quindi il file appare sano. Gli oggetti andati perduti sono quelli raggruppati negli stream di oggetti: i dizionari dei campi AcroForm, gli elementi della struttura dei PDF taggati, la lunga serie di piccoli dizionari che non hanno mai dovuto essere visibili a un visualizzatore obsoleto

Non noti la lacuna finché qualcosa non tocca quegli oggetti, e un salvataggio completo li tocca tutti. Percorrere il documento per crittografarlo nuovamente o riscriverlo è esattamente l'operazione che richiede ogni numero di oggetto a sua volta, motivo per cui il sintomo affiora al momento del salvataggio piuttosto che al caricamento, lontano dalla sua causa

La trappola è un rilevatore che vede xref e si ferma

Il modo economico per decidere come è indicizzato un file è seguire startxref e ispezionare i primi byte a cui punta. La parola chiave xref significa una tabella classica; un oggetto stream significa uno stream di riferimenti incrociati. Quel test è corretto per qualsiasi file che si affidi a un solo schema. È sbagliato per un file ibrido, il cui startxref mira a una sezione classica al solo scopo di soddisfare i vecchi lettori, mentre il /XRefStm nel trailer di quella sezione è il punto in cui la maggior parte del documento è effettivamente indicizzata. Un rilevatore che restituisce "classico" al primo xref che incontra non legge mai /XRefStm e ogni oggetto che risiede solo nello stream diventa invisibile

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // il conteggio è corretto
    // ispeziona o modifica qui il documento caricato
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // percorre ogni oggetto
  finally
    Pdf.Free;
  end;
end;

Con il rilevatore ad uscita anticipata in atto, il caricamento sembra corretto e il momento del salvataggio è dove gli oggetti assenti si manifestano. La correzione non sta nel leggere più byte all'inizio; sta nel riconoscere il trailer ibrido e seguire /XRefStm prima di decidere che il file è finito

L'ordine di unione non è negoziabile

Una volta che entrambi gli indici sono stati letti, possono essere combinati solo in una direzione. Lo stream dei riferimenti incrociati deve essere unito per primo, con le voci classiche riempite attorno a esso. La ragione è il piccolo inganno al centro del formato. Un file ibrido contrassegna i suoi oggetti compressi come liberi nella tabella classica in modo che i vecchi lettori li ignorino. Un caricatore che onora una politica del tipo 'il primo visto vince' e legge per prima la tabella classica registrerà quei numeri di oggetto come liberi, poi scarterà le voci dello stream che effettivamente li localizzano, perché gli slot sono già occupati. Inverti l'ordine e le voci di tipo 2 dallo stream, ciascuna con un numero di stream di oggetti più un indice, conquistano gli slot che sono destinate a possedere, e le voci classiche si sistemano intorno a loro

La stessa disciplina evita che una revisione più vecchia resusciti un oggetto eliminato. Gli aggiornamenti incrementali si concatenano all'indietro attraverso /Prev e una voce libera di tipo 0 è una sentinella che indica che una sezione più recente ha ritirato un numero di oggetto. Non si deve permettere a una sezione successiva, ma cronologicamente più vecchia nella catena, di sovrascrivere quella sentinella con una posizione non più valida. Considera il 'primo visto' come autorevole per i marcatori liberi e l'oggetto cancellato rimarrà cancellato; trattalo con noncuranza e la storia stessa del file rianimerà i contenuti che l'ultima revisione aveva rimosso

Cosa significa questo in HotPDF

Il motore risolve per te i file a riferimenti ibridi, e lo fa su ogni percorso che deve analizzare i dati dei riferimenti incrociati. Carica un documento con LoadFromFile o LoadFromStream, apporta le tue modifiche e chiama SaveLoadedDocument; o esegui un'operazione singola come EncryptFile che legge un input e scrive un output. In entrambi i casi il recupero legge /XRefStm, unisce la sezione dello stream prima delle voci classiche, e risolve gli oggetti che risiedono negli stream prima che la scrittura li enumeri. Il percorso di crittografia AES-256 è dove il problema si è manifestato per la prima volta, perché crittografare un documento riscrive ogni oggetto e richiede quindi che ogni oggetto sia già stato localizzato

// Operazione singola: leggi l'input ibrido, scrivi una copia crittografata AES-256
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Il dettaglio che vale la pena di portare con sé si trova a monte dell'API. I file che arrivano da Word, Excel, PowerPoint e da una lunga serie di pipeline "Salva come PDF" sono regolarmente ibridi, quindi un caricatore che eserciti solo contro l'output del tuo generatore potrebbe non incontrarne mai uno nei test. Popola i tuoi set di test con documenti esportati da applicazioni Office reali, non solo con file prodotti dal tuo stesso codice

Controllare un file sospetto

Due ispezioni risolvono rapidamente la questione. Apri il file in una vista esadecimale e leggi i byte dopo l'ultimo startxref; un file ibrido mostra una breve sezione classica il cui dizionario trailer contiene /XRefStm. Oppure confronta il conteggio degli oggetti riportato da un'analisi completa con il numero di oggetto più alto che /Size dichiara nel trailer. Una grande lacuna significa che gli oggetti sono nascosti in stream che il caricatore non ha aperto, il che è lo stesso ammanco che si trasforma in un fallimento al momento del salvataggio in seguito

La coda di un tipico export di Excel rende concreto il primo controllo. Tutto ciò che si trova dopo l'ultima parola chiave xref è in puro ASCII, quindi la firma è leggibile direttamente da una vista esadecimale (offset puramente indicativi, annotazioni aggiunte)

xref
0 0                          % sottosezione classica vuota: nessuna riga del tutto
trailer
<< /Size 216                 % uno oltre al numero di oggetto più alto in uso
   /Root 1 0 R
   /Info 15 0 R
   /ID [<5C9A...> <5C9A...>]
   /XRefStm 87325            % offset in byte dello stream dei riferimenti incrociati
>>
startxref
88710                        % punta alla sezione classica soprastante
%%EOF

La sottosezione 0 0 è l'indizio: una tabella classica con zero voci esiste solo per trasportare il trailer, e il trailer esiste principalmente per dire /XRefStm 87325. Un rilevatore che si ferma alla parola chiave xref ha, a questo punto, visto un indice del nulla. Quando preferiresti scrivere uno script per il controllo piuttosto che farlo a occhio, il marcatore si trova sempre all'interno dell'ultimo paio di kilobyte del file, quindi una lettura a ritroso limitata è sufficiente

// Restituisce l'offset di /XRefStm dalla coda del file, o -1 se il
// marcatore è assente (il file non è ibrido, o non è affatto un PDF)
function FindXRefStm(const FileName: string): Int64;
var
  FS: TFileStream;
  Tail: AnsiString;
  Len, P: Integer;
begin
  Result := -1;
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Len := 2048;                        // il trailer risiede nella coda
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // lettura a ritroso limitata: 2 KB max
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  if P = 0 then
    Exit;                               // nessun marcatore ibrido nella coda
  Inc(P, Length('/XRefStm'));
  while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
    Inc(P);                             // salta gli spazi bianchi dopo la chiave
  Result := 0;
  while (P <= Len) and (Tail[P] in ['0'..'9']) do
  begin
    Result := Result * 10 + Ord(Tail[P]) - Ord('0');
    Inc(P);
  end;
end;

// Utilizzo: un risultato non negativo indica il byte da cui inizia lo stream
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('file a riferimenti ibridi: il nuovo salvataggio richiederà la sezione /XRefStm');

Tratta la sonda come un triage, non come un parser: ti dice quali file in un batch meritano attenzione prima dell'esecuzione di un processo di salvataggio, e nient'altro. Ciò che un caricatore deve poi fare con l'offset che trova, seguendo la catena delle sezioni, unendo le voci dello stream prima di quelle classiche, rispettando le sentinelle delle voci libere, è illustrato passo dopo passo nel nostro articolo correlato sulla gestione dei PDF a riferimenti ibridi dalle applicazioni Office

Il lato dello scrittore di questa storia, ovvero come gli stream di oggetti e i riferimenti incrociati compressi vengono prodotti in primo luogo, è trattato nel nostro articolo sugli stream di oggetti e gli aggiornamenti incrementali. Quando il file ibrido in questione è anche molto grande, le tecniche di caricamento nella panoramica della Direct File API per i flussi di lavoro PDF di grandi dimensioni ti permettono di ispezionarlo senza leggere l'intero file in memoria. Entrambe si abbinano in modo naturale al ripristino descritto qui, che viene fornito come parte del Componente HotPDF per Delphi e C++Builder insieme alle API di caricamento, modifica, crittografia e firma trattate altrove in questo blog