Articolo tecnico

Navigazione dei campi modulo PDF in Delphi (PDFium Component)

Il bug report arriva con uno screenshot: "Il vostro strumento ha compilato il modulo, ma ogni campo in Acrobat è vuoto. Quando clicco dentro un campo, il valore appare all'improvviso." I dati sono nel file, lo screenshot lo dimostra persino, eppure il modulo sembra vuoto a chiunque lo riceva. Questo è il difetto più comune nel lavoro programmatico sui moduli PDF, e non è un bug di una libreria: è ciò che accade quando i valori dei campi vengono scritti senza rigenerare le appearance dei campi. Capire il perché richiede una sezione della specifica PDF; correggerlo richiede una chiamata di metodo. Gli esempi seguenti usano PDFium Component, un componente VCL/LCL basato su PDFium per Delphi, C++Builder e Lazarus, ma la meccanica del formato file sottostante vale per qualsiasi tooling AcroForm.

Un campo, due rappresentazioni: /V e /AP

Un campo testo AcroForm memorizza il proprio valore nella voce /V del dizionario campo (ISO 32000-1 §12.7.3.3). Ciò che i viewer dipingono davvero, però, è l'appearance stream del widget, un piccolo stream di contenuto prerenderizzato archiviato sotto /AP (§12.5.5). Scrivi /V senza ricostruire /AP e le due cose divergono: il dato esiste, l'immagine del dato no. Acrobat ridipinge l'appearance di un campo quando il campo riceve il focus, ed è esattamente il motivo per cui cliccare dentro un campo "rivela" il valore nel bug report precedente.

La via d'uscita storica, il flag NeedAppearances che chiede ai viewer di rigenerare autonomamente le appearance, non è mai stata rispettata in modo coerente tra viewer ed è deprecata in PDF 2.0 (ISO 32000-2). Pipeline di stampa e generatori di thumbnail non l'hanno mai rispettata: dipingono ciò che contiene /AP, cioè nulla. Il contratto affidabile è quindi: chi scrive il valore ricostruisce anche l'appearance.

La generazione dell'appearance è anche il punto in cui font e allineamento diventano visibili. Uno stream rigenerato impagina il valore dentro il rettangolo del widget usando font, dimensione e quadding del campo, motivo per cui un valore che entra nel tuo modulo di test può essere troncato o ridotto nella copia più stretta dello stesso campo ricevuta dal cliente. I campi con dimensione automatica, font size zero, riducono il testo per farlo entrare; i campi a dimensione fissa troncano. Entrambi sono risultati legali, e l'unico modo per sapere quale produca un dato modulo è ispezionare l'output rigenerato, non il valore scritto: quando un cliente segnala testo tagliato, questo paragrafo è spesso l'intera spiegazione.

Aprire un modulo: FormFill, FormType e la domanda XFA

L'accesso ai campi richiede che il sottosistema form-fill, controllato dalla proprietà FormFill, sia abilitato prima dell'apertura del documento. Una volta attivo, FormType indica che tipo di modulo stai affrontando, e la risposta cambia il set di funzionalità che puoi promettere:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Due note pratiche. AcroForm è il modello di modulo standard ISO 32000 ed è il bersaglio di ogni API in questo articolo; i documenti XFA incorporano una propria architettura di modulo XML, e promettere ai clienti editing XFA completo basandosi su una rapida demo AcroForm è un impegno che rimpiangerai. Secondo, FormFill inizializza anche il JavaScript del documento, che è ciò che vuoi in un viewer di data entry dove gli script di calcolo mantengono aggiornati i totali, e ciò che esplicitamente non vuoi in un'anteprima di file non attendibili. L'articolo sull'anteprima PDF sicura copre il lato FormFill := False di questo trade-off.

Attraversamento da tastiera prevedibile per gli utenti

Gli utenti di data entry vivono sul tasto Tab, quindi l'attraversamento dei campi deve comportarsi come qualsiasi altro modulo che usano. La famiglia di API di focus, FocusFormField, FocusNextFormField, FocusPreviousFormField, FocusedFormFieldIndex e ClearFormFieldFocus, sposta il focus del modulo senza simulare input mouse:

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

Conosci il comportamento ai bordi: le chiamate di attraversamento percorrono il tab order della pagina corrente e vi fanno wrap, quindi avanzare oltre l'ultimo campo torna al primo, ed entrambe le funzioni restituiscono il nuovo indice campo, o -1 quando la pagina non ha campi. Passare alla pagina successiva è una tua decisione: rileva il wrap confrontando gli indici e avanza tu stesso PageNumber se l'obiettivo è l'attraversamento dell'intero documento. Abbina l'attraversamento all'evento OnFormFieldEnter e, sul viewer, a OnFormFieldFocusChange per mantenere sincronizzato un pannello laterale con il documento, e usa la proprietà indicizzata FormFieldAt quando serve hit-testing, cioè mappare una posizione del mouse a un valore di campo per anteprime tooltip o pannelli click-to-edit. Gli utenti di screen reader ottengono gratis il beneficio dell'attraversamento: il focus si muove nell'ordine dei campi proprio del documento, quindi il percorso costruito per il tasto Tab è anche quello seguito dalla tecnologia assistiva.

Per UI guidate da metadati, la proprietà FormFieldInfo[] restituisce un record TPdfFormFieldInfo per indice, ed è così che etichetti i campi in una lista di navigazione invece di mostrare semplici numeri di indice. I gruppi radio meritano qui un file di regressione dedicato: diversi widget condividono un solo nome campo, quindi una lista costruita ingenuamente dai widget mostra duplicati apparenti che confondono gli utenti.

La sequenza compila-e-salva che sopravvive ad Acrobat

Tutto quanto sopra converge su una sequenza in tre passaggi, il cui passaggio centrale è quello che i team saltano:

procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances è l'intera correzione per il bug report iniziale. Ricostruisce gli appearance stream dei widget dai valori correnti, dai font e dal quadding, così ogni viewer, inclusi quelli che non eseguono mai eventi di focus, come server di stampa e generatori di thumbnail, dipinge lo stato compilato. Chiamalo una volta dopo il batch di assegnazioni invece che campo per campo; la generazione delle appearance tocca font e layout, e chiamate per campo moltiplicano quel costo sui moduli grandi senza alcun beneficio.

La verifica appartiene alla definizione di done: apri il file salvato in Acrobat e conferma che i valori siano visibili senza cliccare alcun campo, poi stampa in PDF o immagine da un secondo viewer e conferma che i valori sopravvivano a una pipeline che ignora completamente la logica dei moduli. Questi due controlli insieme catturano ogni variante della divergenza /V contro /AP.

Moduli di produzione che rompono implementazioni pulite

Un breve elenco di configurazioni campo che superano i test demo e falliscono con file cliente:

  • Valori export delle checkbox. Lo stato "on" non è universalmente Yes: i moduli definiscono valori export arbitrari, e scrivere quello sbagliato lascia la casella visivamente non selezionata mentre il codice crede di aver avuto successo.
  • Gruppi radio con nome condiviso. Un campo, molti widget. L'assegnazione del valore seleziona quale widget appare spuntato, e codice UI per-widget che presume un rapporto uno-nome-uno-rettangolo disegna il focus ring sbagliato.
  • Campi calcolati. I totali calcolati dal JavaScript del documento si aggiornano sugli eventi di campo. Una compilazione programmatica che aggira gli eventi dovrebbe attivare il ricalcolo oppure sovrascrivere esplicitamente i campi calcolati: consegnare un modulo in cui voci e totale non concordano è peggio di entrambe le alternative.
  • Campi required nascosti. I moduli condizionali nascondono campi che restano marcati required. Decidi se la validazione rispetta la visibilità o il flag grezzo, e documenta la decisione dove il supporto possa trovarla.

FAQ

Perché i valori compilati appaiono solo quando si clicca un campo?

I valori sono stati scritti in /V ma gli appearance stream /AP non sono mai stati rigenerati, quindi i viewer dipingono l'appearance obsoleta, vuota, finché un evento di focus forza una ricostruzione. Chiama GenerateFormAppearances dopo aver assegnato i valori e prima di SaveAs.

La navigazione dei campi funziona sui moduli XFA?

Controlla prima FormType. ftAcroForm offre tutta la superficie di navigazione ed editing descritta qui; ftXfaFull significa che il documento renderizza dal proprio template XML e l'interazione a livello di campo è limitata. Rilevalo e comunicalo invece di lasciarlo scoprire agli utenti.

Il flattening è la stessa cosa della generazione delle appearance?

No. GenerateFormAppearances mantiene i campi interattivi rendendo i loro valori visibili ovunque. Il flattening converte l'appearance in contenuto statico della pagina e rimuove permanentemente l'interattività: corretto per output archivistico, sbagliato per un modulo che la persona successiva deve modificare.

Il sottosistema form-fill, l'attraversamento del focus e la generazione delle appearance mostrati qui fanno parte di PDFium Component per Delphi, C++Builder e Lazarus/FPC. Se il tuo viewer gestisce anche markup di revisione insieme ai dati modulo, l'articolo sulla revisione annotazioni copre quel modello adiacente.