Articolo tecnico

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

HotPDF fa il round trip dei valori multi-select delle list box attraverso FDF e XFDF mantenendo il valore del campo come array da una parte all'altra. Dalla versione 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF e ExportLoadedFormToXFDF scrivono ogni opzione selezionata come propria stringa FDF o proprio elemento <value> XFDF, e i metodi di import corrispondenti controllano ogni valore contro le opzioni del campo e ricostruiscono gli indici di selezione /I prima di cambiare qualsiasi cosa. Nulla viene incollato in un'unica stringa lungo la strada

Il fallimento che questo risolve è facile da riprodurre. Prendi un form d'ordine con una list box multi-select di opzioni di prodotto, fai scegliere due opzioni a un utente, esporta i dati del form per un sistema di back-office, poi reimporta il file modificato nel PDF. Prima di questo cambiamento la list box tornava vuota o sbagliata. Il motivo è che uno dei valori di export conteneva un'interruzione di riga, e il vecchio percorso aveva appiattito le selezioni in un'unica stringa separata da righe. Rimettere fuori selezioni multiple da quella stringa non è mai stato affidabile, e con un valore di export che contiene per sé un'interruzione di riga non può funzionare affatto

Perché unire i valori multi-select con interruzioni di riga rompe il round trip?

Unire le selezioni in una stringa butta via i confini tra i valori, e un valore può contenere il separatore, quindi nessun importer può rispiezzare correttamente la stringa. ISO 32000-1 §12.7.4.4 consente che la voce /V di un campo choice sia o una singola stringa di testo o un array di stringhe di testo, e 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 come un array di indici di opzione 0-based in ordine crescente, che i viewer usano per distinguere due opzioni che per caso condividono un valore di export. In HotPDF il getter scalare GetFormFieldValue legge solo la forma stringa, quindi farci passare un array degradava l'export a una stringa vuota, e il vecchio import XFDF univa gli elementi <value> ripetuti con LF. Immagina una opzione esportata come Deep, line feed, Blue: dopo l'unione, Deep\nBlue\nRed poteva essere due selezioni o tre, e il file non dà modo di sapere quale. La correzione è stata smettere del tutto di usare uno scalare in mezzo al round trip

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

Che cosa contengono i file FDF e XFDF esportati?

HotPDF scrive un valore multi-select come array tipizzato in FDF e come un elemento <value> per selezione in XFDF, così i confini restano visibili su disco. In FDF ogni voce conserva la grafia che aveva nel PDF sorgente: le stringhe esadecimali escono come hex, e le stringhe letterali vengono corrette da un unico helper che trasforma CR e LF in \r e \n. In XFDF il root porta xml:space="preserve" come richiede ISO 19444-1, il che significa che qualunque whitespace dentro un elemento di testo conta come dato. HotPDF scrive quindi il tag di apertura, il testo corretto e il tag di chiusura di ogni <value> in un pezzo solo, tiene l'indentazione fuori dall'elemento, e codifica CR, LF e TAB come character references così un parser XML che applica la normalizzazione dei fine riga non può cambiare i byte originali

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 valgono la pena conoscere prima di scrivere il codice chiamante. Primo, ExportLoadedFormToFDF costruisce il body FDF completo in memoria prima di creare il file di destinazione (sistemato in 2.755.1), così un valore che non può essere esportato, come un array che contiene altro che stringhe, alza un'eccezione senza troncare un file esistente. Secondo, una selezione vuota su una list box che offre anche un valore di export stringa vuota è ambigua in XFDF, perché <value/> potrebbe significare che non è selezionato nulla o che l'opzione vuota è selezionata. ExportLoadedFormToXFDF alza un'eccezione in quel caso anziché indovinare, e alza prima che il file di destinazione venga aperto. FDF non ha ambiguità del genere, dato che /V [] e /V [()] sono distinti. Entrambi gli exporter FDF saltano anche i terminali solo-widget che non hanno nome /T, in linea con l'exporter XFDF, perché nessun importer potrebbe mai riabbincare quelle voci a un campo

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // Le list box multi-select vengono scritte come /V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // Selezione vuota più una opzione di export vuota: XFDF non può
          // distinguerle, e il file .xfdf esistente resta intatto
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Come valida HotPDF un valore multi-select all'import?

HotPDF accetta un array importato solo quando il target è un campo choice col flag MultiSelect impostato e ogni valore dell'array combacia con un valore di export nell'array /Opt del campo. Ogni slot di opzione può essere usato una volta, quindi una lista con due opzioni che condividono il valore di export b accetta [<62> <62>] come due selezioni distinte e rifiuta una terza b. Il /I ricostruito segue l'ordine di /Opt e non l'ordine dei valori in arrivo, perché §12.7.4.4 richiede indici crescenti. HotPDF costruisce il nuovo /V e il nuovo /I come oggetti scollegati e li assegna solo dopo che ogni valore ha passato la validazione, quindi un valore rifiutato non lascia mai dietro mezzo array o indici stanti. La copia viene scritta nel campo in fase di import anziché in un array di un antenato condiviso, le grafie esadecimali in arrivo da FDF restano hex attraverso il save, e i campi i cui calcoli dipendono dalla list box vengono marcati per il ricalcolo. Se ti serve solo impostare un valore singolo, impostare il valore di un campo form in un PDF caricato passa dal percorso scalare, 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

Qualche altro strumento scrive valori di export ASCII semplici come stringhe hex senza byte order mark, per esempio <416272>, e poi esporta XFDF scrivendo quelle cifre esadecimali come testo. Un confronto letterale severo al ritorno fallisce, e l'import si interrompe. La versione 2.755.1 aggiunge un retry: quando un valore non combacia con nessuna opzione, HPDFHexSpellingText decodifica il testo come payload hex e confronta di nuovo il risultato. Il retry si applica solo a input che altrimenti avrebbero alzato eccezione, quindi non cambia mai un valore che aveva già combaciato. La stessa release ha anche fatto sì che i percorsi scalare e array usino lo stesso decoder Unicode, che capisce PDFDocEncoding, UTF-16 con entrambi i byte order mark e UTF-8. Prima di allora, un valore logico poteva combaciare su un percorso e fallire sull'altro nei documenti che mescolavano codifiche

Perché un file FDF valido può ancora perdere campi durante il parsing?

Uno scanner FDF che non tiene traccia delle stringhe esadecimali può tagliare a metà un dizionario di campo quando un valore hex finisce proprio accanto al terminatore del dizionario. In << /T (region) /V <416273>>> il primo > chiude la stringa hex, ma uno scanner ingenuo lo legge insieme al > successivo come fine del dizionario e butta il campo in silenzio. L'importer FDF a livello di file teneva già conto di essere dentro una stringa hex, e in 2.755.1 gli scanner di array e dizionario dietro ImportLoadedInterchangeFromFDF fanno lo stesso. Un secondo tema riguarda i riferimenti indiretti. Un file FDF è un piccolo documento in sintassi PDF con numerazione di oggetti propria (ISO 32000-1 §12.7.7), quindi un valore come /V [11 0 R] si riferisce all'oggetto 11 del file FDF, non all'oggetto 11 del PDF che stai riempiendo. Il parser FDF semplificato di HotPDF non risolve i riferimenti dentro il file, quindi rifiuta un array simile anziché leggere qualunque sia l'oggetto 11 nel documento di destinazione

Gli import da file, stream e XFDF riportano gli errori in modo diverso

I tre percorsi di import validano allo stesso modo ma riportano i fallimenti diversamente, e vale la pena sceglierne uno di proposito. ImportLoadedFormFromFDF salta qualunque campo fallisca la validazione e restituisce il numero di campi che ha davvero applicato, quindi un conteggio più basso del previsto è l'unico segno di un problema. ImportLoadedInterchangeFromFDF e ImportLoadedFormFromXFDF alzano un'eccezione al primo campo rifiutato. Ogni campo viene commettuto per conto suo, quindi i campi elaborati prima dell'eccezione conservano i loro nuovi valori. Non trattare nessuno di questi come una transazione sull'intero file di scambio: se ti serve un comportamento tutto-o-nulla, scarta il documento caricato quando arriva un'eccezione invece di salvarlo

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 i campi; un valore fuori da /Opt o un target non multi-select alza eccezione
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

Estendere le callback XFDF senza rompere chi chiama già esistente

Il supporto degli array nella unit XFDF di livello più basso vive in un record separato, THPDFXFDFArrayAccess, e in nuovi overload di HPDFXFDFExportFields e HPDFXFDFImportFields, non in campi extra aggiunti in coda al record esistente THPDFXFDFAccess. Il motivo è la compatibilità binaria. Il codice che riempie THPDFXFDFAccess come variabile locale spesso imposta solo gli slot che conosce e non pulisce mai il resto, quindi un nuovo puntatore a funzione aggiunto a quel record conterrebbe spazzatura dallo stack, e la libreria lo prenderebbe per una callback vera. Con un record separato, i vecchi chiamanti conservano il vecchio layout e i vecchi overload, e quei overload passano internamente un record array tutto-nil. Il vecchio overload di import scalare originale unisce ancora i valori ripetuti con LF per compatibilità, e solo l'overload consapevole degli array li tiene separati. Quando colleghi il tuo store di dati, parti da Default(THPDFXFDFArrayAccess). Restituisci True da GetFormFieldValueArray per qualunque campo con valore lista, incluso uno senza nulla di selezionato, e False per ripiegare sulla callback scalare

uses HPDFXFDF;

// Puntatore a funzione semplice, non "of object": Context porta il tuo store
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);             // le tue binding scalari esistenti
  ArrayAccess := Default(THPDFXFDFArrayAccess); // ogni slot inutilizzato è nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

Lo scambio multi-select funziona su list box che esistono già e hanno il bit MultiSelect impostato in /Ff. Per come i campi choice e i loro bit di flag vengono creati all'origine, vedi aggiungere ListBox e altri campi AcroForm a un PDF caricato. Per il markup dei commenti che passa per l'albero <annots> di XFDF, vedi import ed export di annotazioni XFDF in HotPDF. Il riferimento API completo e il download di prova sono sulla pagina del PDF component HotPDF Delphi