Articolo tecnico

Unire moduli PDF in Delphi: regole per campi duplicati

PDF Library for Delphi unisce due documenti AcroForm con una policy esplicita per i campi che condividono un nome. MergeDocumentEx accetta l'identificatore del documento sorgente e una di tre strategie: dfsReject rifiuta l'unione, dfsMerge mantiene il nome condiviso e sincronizza i valori, e dfsAutoNumber rinomina i campi in arrivo in modo deterministico. La scansione dei nomi avviene prima che qualsiasi numero di oggetto si sposti, così un'unione rifiutata lascia entrambi i documenti completamente utilizzabili

Chiunque abbia assemblato un pacco di documentazione PDF ci si è imbattuto. Tre moduli, ciascuno con un campo chiamato Signature o Date o Total, vengono uniti in un unico file. In un AcroForm, il nome campo completamente qualificato è l'identità del campo, quindi due campi con lo stesso nome non sono affatto due campi: riempirne uno riempie l'altro, e una firma applicata a uno copre un ambito che nessuno intendeva

Perché la collisione di nomi viene decisa prima dell'unione?

Il più vecchio MergeDocument concatena i due array di campi radice AcroForm e non offre alcuna scelta. Peggio ancora, quando il risultato è inutilizzabile, la scoperta avviene dopo che i numeri degli oggetti sono stati rinumerati e gli alberi di pagina cuciti insieme, il che lascia il chiamante con un documento in uno stato in cui nessuno dei due originali si trovava

MergeDocumentEx inverte l'ordine. Raccoglie i nomi dei campi di primo livello da entrambi i documenti, li confronta, e applica la strategia prima che qualcosa si sposti. Un rifiuto è quindi un no-op pulito: il documento di destinazione resta intatto, il documento sorgente resta intatto, ed entrambi rimangono aperti e utilizzabili, cosa che il test di unione verifica leggendo un valore di campo dalla sorgente dopo un'unione rifiutata

Il confronto usa un insieme di nomi ordinato e case-sensitive, così il costo è proporzionale al conteggio combinato dei campi moltiplicato per un fattore logaritmico anziché al prodotto dei due conteggi. La sensibilità al maiuscolo/minuscolo è la scelta corretta qui perché i nomi dei campi PDF sono case-sensitive; foldarli unirebbe campi che la specifica tratta come distinti

Le tre strategie, e quando ciascuna è giusta

dfsReject è la strategia per le pipeline automatizzate che non devono produrre documenti ambigui. L'unione restituisce zero e LastErrorCode riporta 705, un codice dedicato così che i nomi duplicati possano essere distinti da ogni altro fallimento dell'unione e instradati verso un rimedio specifico, di solito rinominare i campi a monte

dfsMerge mantiene deliberatamente il nome condiviso e sincronizza il valore di destinazione e il valore predefinito nel campo sorgente, così un visualizzatore conforme tratta i diversi widget come un unico campo denominato logicamente, il che è il comportamento standard di AcroForm per un campo con più annotazioni widget. Ciò che non fa è fondere dizionari di campo diversi in un unico oggetto. Ogni campo mantiene la propria associazione di pagina, aspetto e azioni, perché collassarli scarterebbe silenziosamente formattazione e comportamento che appartengono al documento in arrivo

dfsAutoNumber rinomina i duplicati in arrivo aggiungendo un suffisso numerico che parte da _2 e prendendo il primo libero. Il risultato è riproducibile: dipende solo dai nomi presenti, mai dai numeri di oggetto dei campi, così unire la stessa coppia di documenti due volte produce gli stessi nomi entrambe le volte. Questa proprietà conta quando codice a valle, un'importazione FDF o una mappatura di database fanno riferimento ai campi per nome

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    TargetDoc := Lib.SelectedDocument;
    Lib.LoadFromFile('application-part1.pdf', '');

    SourceDoc := Lib.NewDocument;
    Lib.LoadFromFile('application-part2.pdf', '');

    Lib.SelectDocument(TargetDoc);
    if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
    begin
      if Lib.LastErrorCode = 705 then
      begin
        // Entrambi i documenti sono ancora intatti - riprova con una policy
        Log('duplicate field names; retrying with auto-numbering');
        Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
      end;
    end;

    Lib.SaveToFile('application-complete.pdf');
  finally
    Lib.Free;
  end;
end;

Nota il pattern in due passi in quel codice, possibile solo perché il rifiuto è non distruttivo. Prova prima la policy rigorosa, ispeziona l'errore, poi decidi. Con un'unione che fallisce a metà, il fallback dovrebbe ripartire da capo ricaricando entrambi i file

Come appare il modulo unito in seguito

Con dfsMerge, un campo di destinazione chiamato Shared che porta "Target value" e un campo sorgente con lo stesso nome producono due campi, entrambi chiamati Shared, entrambi che riportano il valore di destinazione, perché il valore di destinazione e il valore predefinito vengono sincronizzati nel campo in arrivo. Questa è la semantica prevista per un nome condiviso: un campo logico, diversi widget, un valore

Con dfsAutoNumber, lo stesso input produce Shared e Shared_2 come campi separati con valori indipendenti. Scegli tra i due ponendoti una sola domanda: riempire un controllo dovrebbe riempire anche l'altro? Per il nome di un firmatario ripetuto su ogni parte di un pacco, sì, e dfsMerge è giusto. Per un totale che significa qualcosa di diverso su ogni modulo, no, e l'auto-numerazione è giusta

// Dopo un'unione, enumera cosa hai effettivamente ottenuto
for I := 1 to Lib.FormFieldCount do
  Log(Format('%d: %s = %s',
    [I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));

Note pratiche per assemblare pacchi di moduli

L'unione riuscita consuma il documento sorgente: viene rimosso dall'elenco documenti della libreria, motivo per cui DocumentCount scende da due a uno. Non continuare a usare l'identificatore sorgente in seguito. La versione del documento viene elevata alla più alta delle due, quindi unire un modulo PDF 2.0 in un documento 1.7 produce un file 2.0

L'ordine conta per i nomi. Unire A in B e unire B in A producono risultati auto-numerati diversi, poiché il documento che esegue l'unione mantiene invariati i propri nomi. Quando un pacco ha un modulo principale canonico, rendi quello la destinazione

I campi firma meritano una considerazione a parte. Una firma applicata prima di un'unione copre solo la revisione che ha firmato, così l'unione la invalida nel senso pratico che il file è cambiato dalla firma. Assembla prima e firma il documento assemblato, piuttosto che unire parti già firmate. Quando l'unione riguarda il contenuto di pagina anziché i moduli, il percorso più rapido descritto in unione PDF rapida con spostamento dei riferimenti a byte è lo strumento migliore

Infine, pianifica il lato dati del pacco insieme all'unione. Se i valori dei campi arrivano da un sistema esterno, decidi se quel sistema indirizza i campi per nome prima di scegliere l'auto-numerazione, perché Shared_2 non corrisponderà a una mappatura che si aspetta Shared. I formati di importazione ed esportazione sono trattati in interscambio dati modulo FDF, XFDF e XFA, e il comportamento di scripting a livello di campo che può anche essere influenzato dalla rinomina è trattato in azioni modulo interattive e JavaScript

L'unione di moduli, l'interscambio dati e la firma funzionano nella stessa libreria per Delphi, C++Builder e Free Pascal; l'elenco completo delle funzionalità si trova nella pagina di PDF Library per Delphi