Articolo tecnico

Allegati PDF in Delphi con PDFium Component: lettura, inserimento ed eliminazione

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

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

Non esiste una funzione helper del tipo SaveAttachment. Occorre leggere i byte e scriverli nella destinazione desiderata, lasciando la creazione e la bonifica del percorso interamente a carico del tuo codice. Questo aspetto è fondamentale 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 elementi che potrebbero produrre risultati imprevisti se passati direttamente a TFileStream.Create. Si consiglia di elaborare sempre il nome tramite ExtractFileName prima di generare il percorso di output, valutando l'eventualità di rifiutare nomi che iniziano con un punto o che contengono caratteri non conformi

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

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 sono applicate unicamente in memoria. Chiama SaveAs per scrivere un nuovo file contenente l'albero dei file incorporati aggiornato. Il Componente PDFium non supporta il salvataggio diretto sullo stesso file attualmente aperto, poiché il motore mantiene un handle di lettura sul file sorgente. La procedura standard per un aggiornamento consiste nel salvare il file in un percorso temporaneo, chiudere il documento, eliminare o rinominare il file originale, quindi rinominare il file temporaneo nella posizione finale e riaprirlo

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