Articolo tecnico

Campi PDF multi-select nei round trip FDF e XFDF (Delphi)

Un valore di export di una list box composto da Deep, un line feed e Blue è PDF perfettamente legale, e lo è anche una seconda opzione chiamata Red. Scegli entrambe, appiattisci la selezione in un'unica stringa separata da LF per un file di scambio, e l'altra parte riceve Deep\nBlue\nRed: due selezioni o tre, e nulla nei byte dice quale. I valori multi-select tornano intatti da FDF o XFDF solo quando restano array per tutta la strada, dal momento in cui lasciano il PDF fino a quando non ci vengono riscritti dentro

Perché un join con interruzioni di riga non può mai portare un valore multi-select?

Una stringa unita perde i confini tra i valori, e poiché un valore può contenere il separatore, nessun importer può rimetterli a posto. ISO 32000-1 §12.7.4.4 consente alla voce /V di un campo choice di contenere o una singola stringa di testo o un array di stringhe di testo; una list box col flag MultiSelect (bit 22 di /Ff) usa la forma array appena più di una opzione è scelta. La stessa sezione definisce /I, un array di indici di opzione 0-based in ordine crescente, che è come un viewer distingue due opzioni che condividono un valore di export. Nessuna delle due strutture passa attraverso una API tipizzata a stringa. GetFormFieldValue in HotPDF restituisce un AnsiString e legge la forma stringa di /V, quindi qualsiasi export costruito su di lui può scrivere al massimo uno scalare per campo

Il lato import ha il problema speculare. XFDF scrive ogni selezione come proprio elemento <value>, e un importer che le passa a un setter scalare deve prima incollarle insieme, di solito con LF. Con l'opzione Deep/Blue di sopra, quella colla è indistinguibile dai dati. HotPDF quindi tiene gli scalari fuori dalla parte centrale del round trip: ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF e ExportLoadedFormToXFDF leggono l'array direttamente dal campo, e i metodi di import corrispondenti costruiscono un nuovo array senza mai passare per una stringa unita

Vecchio round trip multi-select di HotPDF in cui due opzioni scelte di una list box, una con un'interruzione di riga inclusa, vengono appiattite dal percorso scalare GetFormFieldValue nella singola stringa Deep, line feed, Blue, line feed, Red, che i reader a valle possono parsificare come due selezioni o come tre
Unire i valori multi-select in una stringa distrugge i confini tra i valori, e un valore di export che contiene per sé un'interruzione di riga rende ambigua la forma appiattita

Cosa contengono i file FDF e XFDF esportati

Su disco i confini restano visibili. In FDF un campo multi-select diventa un array tipizzato, e ogni voce conserva la grafia che aveva nel PDF sorgente: le stringhe esadecimali escono come hex, le stringhe letterali escono come literal con CR e LF con escape come \r e \n. Nel file scritto da ExportLoadedFormToFDF, un /RV rich-text o un /Ff locale diverso da zero sul campo viaggia dentro lo stesso field dictionary. In XFDF l'elemento root dichiara xml:space="preserve", che ISO 19444-1 richiede e che trasforma ogni carattere di whitespace dentro un elemento di testo in dato. È una trappola per il pretty-printing. HotPDF scrive il tag di apertura, il testo con escape e il tag di chiusura di ogni <value> su una sola riga, tiene l'indentazione fuori dall'elemento, e codifica CR, LF e TAB come character reference così che un parser XML che applica la normalizzazione dei fine riga non possa riscrivere una coppia CR LF in un LF solo

Forme di esportazione che HotPDF scrive per una list box multi-select dalla 2.755.0: FDF porta un array tipizzato per campo con /V [(Deep interruzione di riga Blue) (Red)] e un valore di regione hex, mentre XFDF porta un elemento value per selezione sotto xml:space preserve così il whitespace conta come dato
I confini restano visibili su disco: FDF tiene ogni selezione come propria voce dell'array e XFDF scrive ognuna in un elemento value separato, così nessun importer deve indovinare
<!-- FDF: one typed array per field -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: one <value> per selection -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

Due casi limite dell'export contano quando scrivi il codice chiamante. ExportLoadedFormToFDF assembla l'intero corpo FDF in memoria e solo dopo crea il file di destinazione, quindi un valore che non può essere esportato (per esempio un array che contiene qualcosa di diverso da stringhe) solleva un'eccezione mentre il precedente .fdf su disco resta intatto. Il secondo caso è un vuoto genuino dello stesso XFDF: una selezione vuota su una list box che offre anche un valore di export stringa vuota. <value/> potrebbe significare "niente selezionato" o "l'opzione vuota selezionata", e il formato non ha una terza grafia. ExportLoadedFormToXFDF solleva un'eccezione in quella situazione, prima che il file di destinazione venga aperto. FDF non ha questo problema, perché /V [] e /V [()] sono oggetti diversi. Tutti e tre gli exporter saltano i terminali widget-only che non hanno nome /T, poiché nessun importer potrebbe mai rimappare una voce simile a un campo

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // Multi-select list boxes are written as /V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // Empty selection plus an empty export option: XFDF cannot tell
          // them apart, and the existing .xfdf file is left untouched
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Come viene controllata una selezione importata contro /Opt?

HotPDF accetta un array in arrivo solo quando il target è un campo choice col flag MultiSelect impostato e ogni valore combacia con un valore di export nell'array /Opt del campo (per una coppia di opzioni a due elementi, il primo elemento). Ogni slot di opzione si può usare una volta sola. Una lista con due opzioni che esportano entrambe b accetta quindi [<62> <62>] come due selezioni distinte e rifiuta una terza b, perché non resta slot libero per lei. Il /I ricostruito segue l'ordine di /Opt a prescindere dall'ordine di arrivo dei valori, il che tiene gli indici crescenti come richiede §12.7.4.4. Il nuovo /V e il nuovo /I vengono costruiti come oggetti scollegati e assegnati solo dopo che ogni valore ha passato la validazione, così un valore rifiutato non può lasciare sul campo mezzo array o indici stanti

Dopo l'assegnamento il campo riceve un appearance stream fresco. È il passo più facile da dimenticare in un importer scritto a mano, e il sintomo confonde: i viewer disegnano lo /AP memorizzato, non il /V, così la vecchia selezione resta a schermo e in stampa anche se i dati sono cambiati. La list box entra anche nell'insieme dei campi cambiati, il che significa che una chiamata senza parametri a RecalculateLoadedFormFieldsIncremental rilancia esattamente i calcoli che la leggono. Le grafie hex che arrivano da FDF restano hex attraverso il salvataggio. Se ti serve impostare un solo valore scalare, impostare un valore di campo form in un PDF caricato copre quel percorso, che per progetto non gestisce selezioni multiple

Validazione all'import dei valori multi-select in HotPDF: il target deve essere un campo choice con MultiSelect impostato in /Ff, ogni valore in arrivo deve combaciare con un valore di export /Opt con ogni slot usato una volta, /I viene ricostruito crescente in ordine /Opt, e /V e /I scollegati vengono assegnati solo dopo che tutti i valori hanno passato
Ogni valore in arrivo viene controllato contro le opzioni del campo prima che qualcosa sia scritto, così un valore rifiutato non lascia mai dietro mezzo array o indici di selezione stanti

Un pattern di input merita un trattamento speciale. Un PDF può memorizzare un valore di export ASCII puro come stringa hex senza byte order mark, <416272> per Abr, e un exporter che copia il corpo della stringa PDF verbatim produce XFDF il cui testo dell'elemento sono le cifre grezze 416272 anziché il testo decodificato. Un XFDF che porta un valore di export codificato hex come cifre grezze non può combaciare con /Opt per grafia: un confronto rigoroso non trova nessuna opzione chiamata 416272 e l'import fallisce. Quando l'importer XFDF di HotPDF riceve un file del genere, riprova una volta il valore senza corrispondenza decodificandolo come payload hex. Il retry è deliberamente ristretto. Scatta solo per un valore senza alcuna corrispondenza esatta; ogni corrispondenza esatta viene riservata prima, così una grafia salvata non può mai occupare uno slot di opzione che un valore arrivato dopo, con grafia esatta, richiede; e la voce /V memorizzata è la stringa PDF dell'opzione stessa, quindi /V e /I descrivono la stessa selezione. È il giusto quantitativo di clemenza: scatta solo dove l'import avrebbe altrimenti sollevato un'eccezione, e un valore che già combaciava non viene mai reinterpretato. Il matching in sé passa per un unico decodificatore Unicode che capisce PDFDocEncoding, UTF-16 con byte order mark in entrambi gli ordini e UTF-8, così lo stesso testo logico combacia con la stessa opzione anche nei documenti che mescolano codifiche

Due trappole del parsing FDF: terminatori hex e numeri di oggetto estranei

La prima trappola è lessicale. In << /T (region) /V <416273>>> il primo > chiude la stringa hex e i due successivi chiudono il dictionary. Uno scanner che cerca solo coppie >> chiude il dictionary un carattere prima e perde il campo. L'importer FDF a livello file di HotPDF tiene traccia di essere dentro una stringa hex o un literal mentre ritaglia l'array /Fields, e il reader a stream dietro ImportLoadedInterchangeFromFDF tokenizza l'intero file come sintassi PDF, quindi entrambi vedono tre parentesi di chiusura dove una scansione ingenua ne vede due

La seconda trappola riguarda i riferimenti. Un file FDF è un piccolo documento in sintassi PDF con una numerazione di oggetti tutta sua (ISO 32000-1 §12.7.7), quindi /V [11 0 R] significa l'oggetto 11 del file FDF, mai l'oggetto 11 del PDF che si sta riempiendo. Risolverlo contro il documento di destinazione tirerebbe in silenzio dentro qualunque oggetto porti per caso quel numero lì. Nelle build da Delphi 2009 in poi, il reader di interchange decodifica l'FDF come grafo di oggetti completo, segue il riferimento dentro quel grafo, valida le stringhe risultanti come qualunque array inline, e solleva un'eccezione se l'FDF non contiene l'oggetto 11. L'ImportLoadedFormFromFDF a livello file legge ogni field dictionary da sé senza la tabella degli oggetti dell'FDF; un array di riferimenti fallisce il suo controllo solo-stringhe, e quel campo viene saltato

Scegliere una rotta di import: saltare, sollevare, o ripristinare

Tutte e tre le rotte di import validano i valori multi-select allo stesso modo. Distinguono per cosa succede quando un valore fallisce. ImportLoadedFormFromFDF salta ogni campo che fallisce e restituisce il numero di campi che ha effettivamente applicato, così un conteggio più basso del numero di voci inviate è l'unico segnale di guaio. ImportLoadedFormFromXFDF e ImportLoadedInterchangeFromFDF sollevano un'eccezione al primo campo rifiutato, e ogni chiamata gira come una transazione sul documento caricato: quando l'eccezione esce dal metodo, ogni campo (e, per la rotta interchange, ogni annotazione) che la chiamata aveva già cambiato viene ripristinato. Anche un import interchange che restituisce False viene ripristinato, con Status e record informativo azzerati

Per dati che una persona ha revisionato e rimandato indietro, usa una delle rotte che sollevano eccezioni. Un form d'ordine applicato a metà è peggiore di uno rifiutato, perché nulla nel PDF salvato dice quali list box vengono dal file revisionato e quali hanno tenuto i vecchi valori. Il comportamento salta-e-conta di ImportLoadedFormFromFDF sta bene per un lavoro diverso: un prefill del meglio possibile da un FDF magari generato per una revisione della form leggermente diversa

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // Solo campi. Un valore fuori da /Opt, o più valori puntati su una
      // lista single-select, solleva e ripristina l'intero import
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
      begin
        // Rilancia solo i calcoli che leggono i campi importati
        Pdf.RecalculateLoadedFormFieldsIncremental;
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
      end;
    except
      on E: Exception do
        ShowMessage('Import rejected, document unchanged: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

Collegare il proprio archivio dati ai callback array di XFDF

L'unit HPDFXFDF, che converte tra XFDF e qualsiasi archivio di campi descrivibile con callback, porta il supporto array in un record separato, THPDFXFDFArrayAccess, e negli overload extra di HPDFXFDFExportFields e HPDFXFDFImportFields. Aggiungere nuovi slot in coda al record esistente THPDFXFDFAccess sarebbe stato più corto e sbagliato. I chiamanti spesso dichiarano quel record come variabile locale e riempiono solo gli slot che conoscono, quindi un nuovo puntatore a funzione in fondo conterrebbe spazzatura dallo stack che la libreria scambierebbe per un callback. Col record separato, il codice esistente conserva il suo layout e i suoi overload, e quegli overload passano internamente un record array tutto a nil

I due overload di import si comportano diversamente sugli elementi <value> ripetuti. L'overload scalare originale li unisce ancora con LF e chiama SetFormFieldValue, perché i chiamanti esistenti dipendono da quella convenzione. L'overload array-aware non unisce mai: passa la lista a SetFormFieldValueArray, ricade sul setter scalare solo per un singolo valore che il tuo callback rifiuta, e solleva quando il tuo callback rifiuta una lista di due o più. All'export, parti da Default(THPDFXFDFArrayAccess) così gli slot che non assegni (incluso l'opzionale SetFormFieldRV) restano nil. Restituisci True da GetFormFieldValueArray per qualsiasi campo a valore lista, incluso uno senza nulla di selezionato, e False per lasciare il campo al callback scalare. Un array vuoto esce come un solo <value/> vuoto, quindi un archivio che offre un'opzione stringa vuota dovrebbe rifiutare quel caso dentro il callback, allo stesso modo in cui fa ExportLoadedFormToXFDF

uses HPDFXFDF;

// Puntatore a funzione semplice, non "of object": Context porta il tuo archivio.
// TFormStore e i suoi metodi sono codice tuo, non parte di HotPDF
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // i tuoi binding scalari esistenti
  ArrayAccess := Default(THPDFXFDFArrayAccess); // ogni slot non usato è nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

Tutto quanto sopra presuppone list box che esistono già e hanno già il bit MultiSelect impostato in /Ff. La creazione dei campi choice e l'impostazione dei loro bit di flag è trattata in aggiungere campi ListBox e altri campi AcroForm a un PDF caricato, e il markup dei commenti che passa per la parte <annots> di XFDF è trattato in import ed export di annotazioni XFDF in HotPDF. Il riferimento ai metodi per ogni chiamata mostrata qui, più una build di prova da far girare sulle tue form, è sulla pagina del componente PDF Delphi HotPDF