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
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
<!-- 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
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
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