Articolo tecnico

Riempimento batch atomico dei form PDF in Delphi con PDFium

PDFium Component for Delphi riempie un AcroForm atomicamente con TPdf.SetFormFieldValues: passate da 1 a 256 richieste tipizzate indirizzate per pagina e widget, e il componente o applica ogni valore in un'unica sostituzione del documento o non applica niente. Quando una richiesta non è valida, TPdfFormValuesWriteReport.FailedRequest nomina la richiesta che ha affondato il batch

Quella garanzia conta di più nei lavori che nessuno guarda. Un import notturno spinge righe CRM dentro una form di sinistro, o un servizio di stampa sovrappone dati di polizza a un template prestampato. Eseguite quelli campo per campo e il primo valore cattivo, un codice che non è un export radio valido, una stringa più lunga del MaxLen del campo, vi lascia in mano un PDF mezzo con dati vecchi e mezzo con nuovi. Niente è andato in crash, quindi niente ha allertato nessuno. L'API batch esiste perché un fallimento sia un rifiuto pulito invece di un documento mezzo scritto

Perché il riempimento campo per campo è pericoloso per l'import di dati?

Il riempimento campo per campo è pericoloso perché ogni scrittura committa da sola, quindi un errore a metà del ciclo lascia un documento che non somiglia né al record sorgente né al template originale. Il pattern classico è un ciclo di assegnamenti FormField[Index], ognuno passato per l'ambiente nativo di form fill di PDFium un widget alla volta. I valori da 1 a 17 atterrano, il valore 18 solleva, e chi chiama ora deve sapere esattamente quali valori sono entrati per disfarli

La maggior parte di chi chiama non scrive mai quel codice di disfacimento, e chi lo scrive di solito lo sbaglia in modo sottile: un campo condiviso è già cambiato su ogni pagina in cui compare, e gli stati dei checkbox sono già ribaltati. Se il salvataggio gira in un blocco finally, il file mezzo riempito va su disco con l'aspetto del completo. In un lavoro di overprint è peggio che nessun file, perché si stampa e qualcuno lo firma

L'altro problema silenzioso è il focus. Il setter classico guida l'editor interattivo di PDFium: uccide qualunque focus esistente, focalizza il widget obiettivo, seleziona tutto il testo, lo rimpiazza e uccide di nuovo il focus. Va bene per un job headless, ma in un viewer chiude anche qualunque sessione di modifica l'utente aveva aperta in un altro campo. Il percorso di scrittura tipizzato arrivato in PDFium Component 3.231.0 non passa affatto per il focus nativo dei campi, e il 3.233.0 lo ha esteso da campi singoli a batch tra pagine

Come SetFormFieldValues applica un batch in una sola sostituzione

TPdf.SetFormFieldValues mette in scena ogni richiesta contro un'unica istantanea catturata del documento, scrive tutti i dizionari dei campi in una passata, e scambia il risultato finito con un'unica sostituzione. Il documento originale resta vivo fino all'ultimo passo, quindi qualunque fallimento prima di quel passo lo lascia intatto

La sequenza di riempimento form batch atomico in cinque stadi in PDFium Component for Delphi, dal preflight alla verifica e sostituzione con esiti tutto-o-nulla
ogni richiesta viene messa in scena contro una sola istantanea e il documento vivo viene toccato solo alla sostituzione finale verificata

La sequenza spiega sia la garanzia sia il costo:

  1. prima girano i controlli di preflight: il documento deve essere attivo, il numero di richieste deve essere 1..256, nessuna transazione di page-object o edit journal può essere aperta, nessun campo nativo può tenere il focus, e il documento deve concedere il permesso fill-forms o modify
  2. ogni richiesta viene validata per intervallo di pagine e budget di input, e FailedRequest traccia l'indice in verifica
  3. il componente cattura un'istantanea di modifica, normalizza l'AcroForm in quella copia, e applica ogni richiesta in ordine, scrivendo /V, /I per le selezioni choice e /AS per ogni widget che condivide il campo
  4. le modifiche vengono accodate all'istantanea come un unico incremental update sparso nel senso di ISO 32000-1 §7.5.6, così gli oggetti non correlati vengono riportati byte per byte
  5. un'istanza verificatrice separata carica prima i byte candidate; solo dopo che si apre pulitamente il componente scarica il documento vivo e carica il nuovo, ripiegando sull'istantanea originale se quel caricamento fallisce

Il passo interno di sostituzione è la stessa macchina che il componente usa per le istantanee del suo edit journal, ecco perché il journal di undo e recupero da crash e una scrittura batch non possono sovrapporsi: chiudete o annullate prima la transazione del journal. Le azioni automatiche del ciclo di vita vengono sopprese mentre il documento viene scambiato, così uno script document-open non può riscrivere campi o afferrare il focus in mezzo alla sostituzione

Costruire un batch di richieste tipizzate in Delphi

Un batch è un array dinamico di record TPdfFormValueRequest, ognuno con un Page a base 1, un FieldIndex locale alla pagina a base 0 e un TPdfFormValue tipizzato. Il record valore porta un Kind tra fvkText, fvkChoice, fvkCheckBox o fvkRadioButton, più i membri Text, ChoiceIndices e Checked usati secondo quel kind

I dati di import di solito arrivano con chiave il nome del campo, quindi il primo lavoro è tradurre i nomi in indirizzi pagina e widget. FieldIndex conta le annotation widget su una pagina nello stesso ordine di FormFieldCount e FormFieldInfo, quindi una scansione semplice funziona. L'helper prende il primo widget che trova per un nome, il che conta per i campi condivisi, come spiega la sezione successiva

type
  TFieldAddress = record
    Page, FieldIndex: Integer;
  end;

function FindFieldAddress(Pdf: TPdf; const FieldName: WString;
  out Address: TFieldAddress): Boolean;
var
  P, I: Integer;
begin
  Result := False;
  for P := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := P;
    for I := 0 to Pdf.FormFieldCount - 1 do
      if Pdf.FormFieldInfo[I].Name = FieldName then
      begin
        Address.Page := P;
        Address.FieldIndex := I;
        Exit(True);      // solo il primo widget: una richiesta per campo
      end;
  end;
end;

procedure AddRequest(var Requests: TPdfFormValueRequests;
  const FieldName: WString; Pdf: TPdf; const Value: TPdfFormValue);
var
  Address: TFieldAddress;
  N: Integer;
begin
  if not FindFieldAddress(Pdf, FieldName, Address) then
    raise Exception.CreateFmt('Template has no field "%s"', [FieldName]);
  N := Length(Requests);
  SetLength(Requests, N + 1);
  Requests[N].Page := Address.Page;
  Requests[N].FieldIndex := Address.FieldIndex;
  Requests[N].Value := Value;
end;

Notate che AddRequest solleva su un nome sconosciuto invece di saltarlo. Il batch valida gli indirizzi, non la vostra mappatura, quindi un campo template mancante deve fallire qui o non fallirà mai. Con gli indirizzi risolti, costruire i valori e committarli è breve

var
  Pdf: TPdf;
  Requests: TPdfFormValueRequests;
  Report: TPdfFormValuesWriteReport;
  V: TPdfFormValue;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'claim-template.pdf';
    Pdf.Active := True;

    V := Default(TPdfFormValue);
    V.Kind := fvkText;
    V.Text := 'Maria Lopez';
    AddRequest(Requests, 'Claimant.Name', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkCheckBox;
    V.Checked := True;
    AddRequest(Requests, 'Consent', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkRadioButton;
    V.Text := 'Express';                // valore export del pulsante da selezionare
    AddRequest(Requests, 'Shipping', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkChoice;
    SetLength(V.ChoiceIndices, 1);
    V.ChoiceIndices[0] := 2;            // indice a base 0 nell'array Opt del campo
    AddRequest(Requests, 'Region', Pdf, V);

    if Pdf.SetFormFieldValues(Requests, Report) then
      Pdf.SaveAs('claim-filled.pdf')
    else
      LogBatchFailure(Requests, Report);
  finally
    Pdf.Free;
  end;
end;

I kind tipizzati vengono confrontati col tipo del campo prima che venga scritto qualsiasi cosa. Un valore testo puntato su un checkbox, un valore radio puntato su un checkbox semplice, o una scrittura su un push button rifiutano tutti. Gli indici choice devono essere validi, unici e crescenti; più di un indice richiede la flag MultiSelect, e il testo libero in un campo choice è accettato solo da una combo box editabile. Un valore testo più lungo del MaxLen del campo rifiuta invece di essere troncato, e i campi read-only rifiutano in toto. Per i gruppi radio con la flag NoToggleToOff, un valore export vuoto, che significa "svuota il gruppo", rifiuta anch'esso

Perché scrivere due volte un campo condiviso fa fallire l'intero batch?

TPdf.SetFormFieldValues rifiuta due richieste che si risolvono nello stesso campo terminale perché scriverebbero entrambe un singolo valore, e il componente rifiuta di lasciare che una richiesta scavalchi in silenzio un'altra. Il controllo gira sul proprietario del campo, non sul widget, quindi due widget diversi di un campo su due pagine diverse contano comunque come duplicato

La ragione è il modello dei campi in ISO 32000-1 §12.7.3.1. Un campo terminale possiede il valore; le sue annotation widget sono solo luoghi in cui quel valore viene mostrato. Un campo di nome PolicyNumber che compare nell'header di ogni pagina è un campo con diversi widget, e ognuno di quei widget mostra lo stesso /V. Un gruppo radio funziona allo stesso modo: ogni pulsante è un widget, il gruppo è il campo. Scrivere PolicyNumber attraverso il widget della pagina 1 e di nuovo attraverso quello della pagina 9 è un campo scritto due volte, e qualunque regola che scelga un vincitore nasconde un bug nella vostra mappatura

Così il batch lo tratta come errore e nomina la seconda richiesta in FailedRequest. In pratica la cura sta a monte: risolvete ogni nome campo a esattamente un indirizzo, come fa FindFieldAddress, e lasciate che il componente propaghi il valore agli altri widget. Lo fa come parte della scrittura, impostando /AS su ogni widget di un checkbox condiviso o gruppo radio così che lo stato di appearance concordi col valore del campo, la stessa ripartizione trattata in articolo su valori dei campi checkbox contro stati dei widget

Un campo terminale possiede il valore, widget su due pagine lo mostrano, e una richiesta duplicata fa fallire il batch in PDFium Component for Delphi
una richiesta per campo terminale — il componente scrive /V una volta e imposta /AS su ogni widget che lo condivide

Leggere il report di fallimento

Un batch fallito restituisce False, non solleva, e lascia il documento originale e ogni valore dei campi esattamente com'erano. TPdfFormValuesWriteReport quindi riporta Succeeded = False, zero sia per UpdatedFields sia per UpdatedWidgets, l'indice a base 0 della richiesta colpevole in FailedRequest, e una ragione leggibile in ErrorMessage

procedure LogBatchFailure(const Requests: TPdfFormValueRequests;
  const Report: TPdfFormValuesWriteReport);
var
  R: Integer;
begin
  R := Report.FailedRequest;
  if R >= 0 then
    Log(Format('Request %d (page %d, widget %d) rejected: %s',
      [R, Requests[R].Page, Requests[R].FieldIndex, Report.ErrorMessage]))
  else
    Log('Batch rejected as a whole: ' + Report.ErrorMessage);
end;

Un FailedRequest di -1 significa che il rifiuto appartiene alla transazione invece che a una richiesta: il focus nativo era attivo, il documento è firmato, la form è XFA, o un controllo di permesso è fallito. Un valore non negativo punta a una voce. Per uno sforamento del budget aggregato punta alla richiesta che ha spinto il batch oltre il limite, che non è necessariamente la richiesta sbagliata. Tenete un array parallelo dei nomi delle colonne sorgente accanto a Requests e potete trasformare l'indice 14 direttamente in "riga 2087, colonna ShipDate" nel log di import

In caso di successo, UpdatedFields eguaglia il numero di richieste e UpdatedWidgets conta ogni widget toccato, condivisi inclusi, un controllo di sanità economico contro il template

Precondizioni e limiti che incontrerete

Il rifiuto più comune in una applicazione viewer è il focus nativo: se un utente sta digitando in un campo, SetFormFieldValues rifiuta prima ancora di catturare l'istantanea, e il buffer dell'editor in attesa sopravvive intatto. Il componente non lascia cadere il focus alle vostre spalle, perché ciò committerebbe o perderebbe testo che l'utente non ha finito. Chiamate ClearFormFieldFocus deliberatamente, che chiude la sessione dell'editor come farebbe un click fuori dal campo, e poi ritentate

I budget sono fissi e documentati, così potete dimensionare gli import su di essi:

  • da 1 a 256 richieste per batch; spezzate i record più grandi in più batch e accettate che l'atomicità allora valga per batch
  • al massimo 262.144 unità UTF-16 di testo e 4.096 indici choice per richiesta
  • al massimo 1.048.576 unità UTF-16 di testo e 16.384 indici choice sull'intero batch
  • i soliti budget documento per il parser delle istantanee valgono ancora, inclusa una sorgente da 64 MiB e 65.536 oggetti

Una sostituzione riuscita invalida anche gli handle nativi, le ricerche e le selezioni ottenute prima della chiamata, perché il documento sotto è nuovo. Attivazioni di pagina e vista e cursori logici dei campi vengono conservati; qualunque altra cosa abbiate in cache, come handle di annotation o page-object, va richiesta di nuovo

In cosa differisce dall'import XFDF?

TPdf.ImportXFDF e TPdf.SetFormFieldValues mettono entrambi dati nei campi, ma l'import XFDF è una fusione tollerante mentre il batch è una transazione severa. L'import XFDF abbina i valori ai campi per nome, salta i nomi che non trova, ignora i duplicati, e restituisce quante voci ha applicato. È esattamente giusto per fondere i commenti di un revisore, dove un risultato parziale è pur sempre utile

Il batch fa l'opposto su ogni punto. Indirizza i widget esplicitamente, rifiuta obiettivi sconosciuti o sbagliati, tratta i duplicati come errori, e non ha esiti parziali. Usate XFDF quando i dati sono un file di interscambio da un altro strumento; usate SetFormFieldValues quando il vostro codice possiede la mappatura e un record mezzo applicato sarebbe un difetto

Matrice decisionale che confronta l'import XFDF con il batch atomico SetFormFieldValues in PDFium Component for Delphi: indirizzamento, obiettivi sconosciuti e duplicati, esito e ambito migliore
XFDF fonde i file di interscambio con tolleranza mentre il batch rifiuta nomi sconosciuti, duplicati ed esiti parziali

Cosa non copre la scrittura batch

Il batch è un profilo AcroForm delimitato, e dice no invece di tirare a indovinare fuori da quel profilo. Le form XFA rifiutano e vogliono il loro editor dedicato. I documenti firmati rifiutano, dato che riscrivere dizionari dei campi sotto una firma esistente è un flusso di firma, non un'operazione di riempimento; anche le istantanee cifrate sono fuori dal profilo. Gli array export dei pulsanti malformati o ambigui rifiutano, e i valori export radio che combaciano con più di un pulsante vengono rifiutati a meno che il gruppo imposti RadiosInUnison. La transazione normalizza anche i dizionari campo diretti prima di scrivere, il caso dietro valori di campo che sembrano tornare indietro

Le appearance meritano una frase di attenzione. Per i widget testo e choice modificati il batch butta il /AP vecchio invece di spedire una appearance che mostra il valore vecchio, e spegne NeedAppearances così che PDFium non ricostruisca ogni widget non correlato della form. PDFium rigenera le appearance mancanti quando renderizza. Se il PDF riempito va a una pipeline di stampa severa che non sintetizza mai le appearance, renderizzatelo o appiattitelo attraverso PDFium prima, e testate quel percorso con i vostri template

SetFormFieldValues e i record valore tipizzati viaggiano nel PDFium Component for Delphi and Lazarus, dove la pagina del prodotto elenca l'API completa di riempimento form e AcroForm accanto ai controlli di rendering, editing e viewer