Articolo tecnico

Allegati PDF in Delphi con PDFium Component

Gli allegati dei file PDF sono memorizzati nell'albero dei file incorporati del documento, una struttura che la maggior parte dei visualizzatori mostra tramite un pannello a forma di graffetta o una barra laterale degli allegati. Dal codice Delphi, il Componente PDFium espone tale albero attraverso un set ridotto di proprietà indicizzate in TPdf: consente di scorrere gli elementi tramite indice intero, leggere i nomi e i payload di byte, creare nuove voci ed eliminare quelle esistenti. L'API è essenziale: vi sono solo alcuni vincoli sull'ordine delle operazioni e una regola di bonifica dei percorsi da conoscere prima di scrivere codice di produzione

Lettura degli allegati da un documento aperto

La proprietà AttachmentCount fornisce il numero di file incorporati dichiarati nel documento. Questo valore deriva direttamente dalla chiamata sottostante di PDFium, per cui riflette unicamente ciò che il PDF contiene. A partire da questo dato, AttachmentName[Index] restituisce il nome visualizzato come WString, e Attachment[Index] fornisce i byte grezzi sotto forma di array TBytes. Entrambi gli indici partono da zero. Il documento deve essere aperto (Pdf.Active = True) prima di poter interrogare queste proprietà; richiamarle su un documento chiuso restituirà zero o un valore vuoto senza sollevare eccezioni

Un aspetto da considerare: Attachment[Index] alloca e restituisce l'intero payload del file ad ogni lettura. Per un documento che contiene un file incorporato di grandi dimensioni, scorrere tutti gli allegati per creare un elenco visivo comporta un costo di allocazione della memoria ad ogni chiamata. Se sono necessari solo i nomi a scopo di visualizzazione, si consiglia di leggere innanzitutto AttachmentName e rinviare il recupero dei byte al momento in cui l'utente richiede effettivamente il file

Mappa dell'albero dei file incorporati PDF alle proprietà di allegato TPdf nel PDFium Component per Delphi: AttachmentCount, AttachmentName, byte Attachment e AttachmentType
I file incorporati vivono nell'albero /Names → /EmbeddedFiles, e il componente espone gli stessi slot come proprietà TPdf indicizzate per conteggio, nome, byte e tipo MIME
procedure ListAttachments(Pdf: TPdf);
var
  I: Integer;
  Data: TBytes;
begin
  if not Pdf.Active then
    Exit;

  for I := 0 to Pdf.AttachmentCount - 1 do
  begin
    Data := Pdf.Attachment[I];
    Writeln(Format('%d: %s (%d bytes)',
      [I, Pdf.AttachmentName[I], Length(Data)]));
  end;
end;

Estrazione di un allegato su disco

L'estrazione lavora sui byte raw: li leggi e li scrivi dove ti serve, il che mette la costruzione e la bonifica del percorso interamente a carico del tuo codice. Questo conta quando i nomi degli allegati provengono da documenti non attendibili. I nomi degli allegati PDF sono stringhe memorizzate all'interno del file; possono contenere separatori di percorso, caratteri Unicode simili ad altri e altri caratteri che produrranno risultati inattesi se li passi direttamente a TFileStream.Create. Fai sempre passare il nome per ExtractFileName prima di costruire qualunque percorso di output, e valuta di rifiutare i nomi che iniziano con un punto o contengono caratteri fuori da ciò che il tuo sistema si aspetta

L'array di byte restituito da Attachment[Index] è gestito dal chiamante. Scrivilo tramite un normale TFileStream per elaborarlo liberamente, inclusa la possibilità di analizzare i primi byte per verificare l'effettivo formato del file piuttosto che fare affidamento sul nome dichiarato

Sanificare un nome di allegato PDF non fidato in Delphi con ExtractFileName prima di costruire il percorso di output, poi verificare i magic bytes invece di fidarsi del nome dichiarato quando si estrae con PDFium Component
I nomi provenienti da documenti non fidati possono portare separatori e sosia, così l'estrazione passa attraverso ExtractFileName e un fallback prima che venga creato qualsiasi TFileStream
procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
  SafeName: string;
  OutPath: string;
  Data: TBytes;
  FS: TFileStream;
begin
  SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
  if SafeName = '' then
    SafeName := Format('attachment_%d', [Index]);

  OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
  Data := Pdf.Attachment[Index];

  FS := TFileStream.Create(OutPath, fmCreate);
  try
    if Length(Data) > 0 then
      FS.WriteBuffer(Data[0], Length(Data));
  finally
    FS.Free;
  end;
end;

Inserimento di allegati e scrittura in due passaggi

L'inserimento di un allegato richiede due chiamate anziché una. CreateAttachment(Name) registra una nuova voce nell'albero dei file incorporati e restituisce True in caso di esito positivo. Questa voce iniziale è vuota. Successivamente, occorre assegnare il payload scrivendo in Attachment[AttachmentCount - 1], selezionando la voce creata più di recente. Se il metodo CreateAttachment restituisce False, la voce non è stata creata e l'assegnazione potrebbe sovrascrivere l'allegato presente all'ultimo indice disponibile

Dopo aver modificato l'elenco degli allegati, le modifiche vivono solo in memoria. Chiama SaveAs per scrivere un nuovo file con l'albero dei file incorporati aggiornato. SaveAs punta a un percorso diverso dal file attualmente aperto, perché il motore tiene un handle di lettura sulla sorgente. Lo schema standard per un aggiornamento in posto è salvare su un percorso temporaneo, chiudere il documento, eliminare o rinominare l'originale, quindi rinominare il file temporaneo al suo posto e riaprire

Creazione di allegati PDF in due passi in Delphi con PDFium Component: CreateAttachment riserva uno slot vuoto, il payload viene assegnato all'ultimo indice, e SaveAs pubblica attraverso uno scambio di file temporaneo perché il file aperto non può essere scritto sul posto
CreateAttachment riserva soltanto uno slot vuoto per l'assegnazione del payload, e la modifica resta in memoria finché SaveAs non scrive un nuovo file attraverso lo swap del file temporaneo
procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
  FS: TFileStream;
  Data: TBytes;
  AttachName: string;
begin
  if not Pdf.Active then
    Exit;

  FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Data, FS.Size);
    if FS.Size > 0 then
      FS.ReadBuffer(Data[0], FS.Size);
  finally
    FS.Free;
  end;

  AttachName := ExtractFileName(FilePath);
  if Pdf.CreateAttachment(AttachName) then
    Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;

Informazioni sul tipo di allegato

Oltre al nome e al payload di byte, AttachmentType[Index] restituisce la stringa del tipo MIME memorizzata nel dizionario del file incorporato del PDF, qualora sia stata registrata al momento dell'inserimento dell'allegato. Molti generatori lasciano questo campo vuoto o lo impostano su valori generici come application/octet-stream, per cui non è possibile fare affidamento su di esso per individuare il formato in una procedura di produzione. Per un'identificazione attendibile, analizza i primi byte del payload per verificare le firme dei file note: ad esempio %PDF per un PDF annidato, l'intestazione del file locale ZIP PK\x03\x04 per i documenti Office Open XML, o \xD0\xCF\x11\xE0 per i file binari composti legacy. Le informazioni sul tipo provenienti dal dizionario sono idonee per l'etichetta dell'interfaccia utente, ma non dovrebbero determinare le logiche di elaborazione quando si dispone dei byte effettivi del file

Eliminazione degli allegati

Il metodo DeleteAttachment(Index) rimuove la voce alla posizione indicata e restituisce True in caso di successo. A seguito dell'eliminazione, gli elementi rimanenti scalano di posizione, per cui se si eliminano più allegati all'interno di un ciclo occorre scorrere dall'ultimo indice verso il primo, e non in avanti, per evitare di saltare elementi a seguito del riposizionamento. La modifica rimane in memoria fino alla chiamata a SaveAs

Uno scenario comune nei flussi di elaborazione dei documenti consiste nel rimuovere tutti gli allegati da un PDF in ingresso prima di trasmetterlo ad altri moduli, per motivi di sicurezza o di dimensioni. Calcola il conteggio una volta prima del ciclo e scorri in ordine inverso:

procedure StripAllAttachments(Pdf: TPdf);
var
  I: Integer;
begin
  for I := Pdf.AttachmentCount - 1 downto 0 do
    Pdf.DeleteAttachment(I);
end;

Ambiti di utilizzo pratico degli allegati PDF

Le API degli allegati funzionano su qualsiasi PDF che PDFium sia in grado di aprire, ma i documenti in cui si incontrano file incorporati si concentrano su alcuni casi specifici. Lo standard PDF/A-3 (ISO 19005-3) consente esplicitamente l'inserimento di file incorporati conformi come meccanismo per allegare i dati di origine alla versione di archiviazione; le fatture elettroniche ZUGFeRD e Factur-X si basano proprio su questo principio per inserire un payload XML strutturato all'interno del layout PDF leggibile dall'utente. I PDF derivati da email a volte includono gli allegati dei messaggi originali trasferiti nell'albero dei file incorporati. Anche la documentazione tecnica prodotta da sistemi di authoring strutturati occasionalmente organizza le risorse di supporto in questo modo

Quando la tua applicazione elabora PDF in entrata provenienti dall'esterno, verificare AttachmentCount in fase di ricezione è utile per due motivi distinti. In primo luogo, i file incorporati possono contenere dati da estrarre ed elaborare, come il tracciato XML all'interno di una fattura PDF. In secondo luogo, i file incorporati possono ospitare contenuti eseguibili arbitrari, per cui è importante conoscerne la presenza anche se non si intende procedere all'estrazione. Nessuno di questi scenari richiede operazioni complesse: leggi il conteggio, verifica i nomi e stabilisci come gestire i byte associati

Le proprietà degli allegati mostrate qui fanno parte del Componente PDFium per Delphi e C++Builder