Le checkbox e i pulsanti radio si appiattiscono come deselezionati perché lo stato di aspetto /AS non era mai stato sincronizzato con il valore del campo /V. PDFium Component, il componente VCL e LCL basato su PDFium per Delphi, C++Builder, e Lazarus, ora legge quel valore con FPDFAnnot_GetFormFieldValue, che risolve il dizionario del campo genitore anziché la widget annotation
La segnalazione di bug che ha portato qui è del tipo di cui diffidi in principio. Un cliente appiattisce un modulo di consenso firmato, apre il risultato, e ogni checkbox è vuota. Apri il file sorgente in Acrobat e le caselle sono visibilmente spuntate. Rileggi il file sorgente tramite lo stesso componente e i valori dei campi sono corretti. Solo l'output appiattito li perde, e solo per checkbox e pulsanti radio: i campi di testo sulla stessa pagina escono bene
Perché le checkbox sono deselezionate dopo l'appiattimento?
Perché l'appiattimento non guarda mai /V. FPDFPage_Flatten incorpora lo stream di aspetto della widget nel contenuto della pagina, e l'aspetto che sceglie è quello nominato da /AS. Se /AS dice ancora /Off mentre il valore del campo dice che la casella è attiva, l'appiattimento incorpora fedelmente l'aspetto off. Il valore non è mai stato perso; non è mai stato consultato
ISO 32000-1 §12.5.5 definisce il dizionario di aspetto /AP con tre possibili voci, /N, /R, e /D. Per una check box o un pulsante radio la voce /N non è uno stream ma un sotto-dizionario le cui chiavi sono nomi di stato di aspetto, e §12.5.2 rende /AS il selettore richiesto quando /N è un sotto-dizionario. Quindi una checkbox porta due aspetti prefabbricati e un puntatore. Sbaglia il puntatore e il rendering è sbagliato in un modo che nessuna quantità di /V corretto riparerà. Questo è anche il motivo per cui la modalità di fallimento differisce dai campi di testo, che non hanno affatto un aspetto prefabbricato da selezionare: un /N di campo di testo è un singolo stream che deve essere rigenerato da zero dopo che il valore cambia, quindi GenerateFormAppearances gestisce i due casi tramite percorsi di codice completamente separati e solo il percorso dei pulsanti era rotto
Dove vive realmente il valore della checkbox?
Sul dizionario del campo, non sulla widget. ISO 32000-1 §12.7.5.2 descrive le check box e i pulsanti radio come campi bottone il cui /V è un oggetto nome che nomina lo stato di aspetto corrente, e §12.7.3.1 colloca /V tra le voci comuni a tutti i dizionari di campo. La widget annotation definita in §12.5.6.19 contribuisce /AS e /AP. Nulla nella specifica obbliga una widget a portare /V
// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off
{ What the two objects look like when the field has several widgets:
12 0 obj % field dictionary (the parent)
<< /FT /Btn /T (Consent) /V /On
/Kids [ 13 0 R 14 0 R ] >>
endobj
13 0 obj % widget annotation (a kid)
<< /Type /Annot /Subtype /Widget /Parent 12 0 R
/AS /Off
/AP << /N << /On 20 0 R /Off 21 0 R >> >> >>
endobj }
FPDFAnnot_GetStringValue non è difettosa. Il suo contratto è esattamente ciò che dice il suo nome: recuperare una voce stringa dal dizionario di annotazione che le hai passato. Chiederle /V sull'oggetto 13 non restituisce nulla perché l'oggetto 13 genuinamente non ha /V. Il difetto era nel chiamante, che assumeva un modello di oggetti piatto che ISO 32000-1 non ha mai promesso
Quando campo e widget condividono un unico dizionario?
Ogni volta che un campo ha esattamente una widget. §12.5.6.19 permette che il dizionario del campo e la sua unica widget annotation vengano uniti in un unico oggetto, e la maggior parte degli strumenti di authoring prende questa scorciatoia. In un oggetto unito /FT, /T, /V, /AS, e /AP stanno tutti fianco a fianco, quindi una lettura di /V a livello widget riesce e l'intero bug resta invisibile
Nel momento in cui un campo possiede due o più widget la fusione è impossibile, e §12.7.3.1 richiede che le widget diventino /Kids di un dizionario di campo separato. Ogni gruppo radio è in questa forma per costruzione. Lo sono anche le checkbox di consenso ripetute in un'intestazione e un piè di pagina, e qualunque campo che uno strumento di authoring ha copiato in una seconda pagina. Questa è l'intera spiegazione del perché il difetto sia sopravvissuto a una suite di regressione: il corpus di test era pieno di moduli a widget singola e i file dei clienti no. Se percorri le widget tu stesso anziché affidarti al componente, la stessa asimmetria si presenta nell'ordine di enumerazione, e le note su navigazione dei campi form PDF con PDFium Component trattano come una percorrenza di annotazioni a livello pagina si relaziona all'albero dei campi a livello documento
Leggere il valore nel modo previsto da PDFium
FPDFAnnot_GetFormFieldValue è la API corretta, ed era stata collegata nel componente da tempo senza che il percorso checkbox la usasse. Prende l'handle del form oltre all'annotazione, il che è il segnale che conta: con l'ambiente form-fill disponibile, PDFium risolve l'annotazione al suo controllo form e legge il valore dall'oggetto campo, quindi restituisce la risposta giusta sia per layout uniti sia per layout separati
FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
begin
// /AP is prebuilt per state; only /AS has to be synchronised with /V.
// FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
// which is where ISO 32000-1 12.7.5.2 keeps the value.
buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
if buflen >= 4 then
begin
SetLength(OrigVal, buflen div 2 - 1);
FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
end;
end;
Due dettagli in quello snippet sono facili da sbagliare. La lunghezza restituita è un conteggio di byte per testo UTF-16 incluso il terminatore, quindi il conteggio di caratteri è buflen div 2 - 1 e un valore di 2 significa una stringa vuota. La guardia buflen >= 4 significa quindi almeno un carattere reale, il che è ciò che impedisce che un campo senza alcun /V veda il proprio /AS sovrascritto con un nome vuoto
Su cosa concordano realmente /AS e /AP /N
Concordano su un nome, e il nome viene scelto da chi ha prodotto il file. §12.7.5.2 richiede che lo stato off sia chiamato /Off, e lascia lo stato on interamente al produttore. /Yes è una convenzione, non una regola. Acrobat scrive /Yes, ma molti generatori scrivono /On, /1, /Choice1, o una parola localizzata, e un gruppo radio normalmente dà a ogni kid un nome di stato on distinto così il gruppo può esprimere quale pulsante è selezionato. Questo è precisamente il motivo per cui copiare /V verbatim in /AS è l'operazione giusta anziché un trucco: per un controllo selezionato PDFium riporta il nome di stato on che il file stesso definisce, e per uno non selezionato riporta Off, così il valore che scrivi in /AS è garantito essere una chiave che esiste in quel sotto-dizionario /AP /N della widget. Hard-codare /Yes funzionerebbe sull'output di Acrobat e si romperebbe silenziosamente ovunque altrove
Ordine delle operazioni, e dove serve ancora attenzione
La sequenza è fissa e non perdona: abilita il form fill, assegna i valori, rigenera gli aspetti, appiattisci, poi salva. Salta il passo di rigenerazione e FPDFPage_Flatten trova stream di aspetto vuoti o obsoleti e li incorpora senza lamentarsi, il che è una perdita di dati silenziosa anziché un ritorno di errore
Pdf.FileName := FormPath;
Pdf.FormFill := True; // required: FormHandle must exist
Pdf.Active := True;
Pdf.FormField[0] := 'On'; // writes /V only
Pdf.GenerateFormAppearances; // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
Pdf.SaveAs('consent-flat.pdf');
Restano due limiti onesti. Primo, la sincronizzazione scrive il valore del campo nell'/AS di ogni widget di quel campo, il che è corretto per le checkbox ma approssimativo per i gruppi radio i cui kid definiscono ciascuno il proprio nome di stato on; un kid il cui /AP /N non ha alcuna voce corrispondente all'/AS scritto non ha alcun aspetto da selezionare secondo §12.5.5, così un pulsante non selezionato può appiattirsi nel nulla invece che in un cerchio vuoto. Verificare un gruppo radio con FPDFAnnot_GetFormControlIndex prima dell'appiattimento vale le poche righe. Secondo, nulla di tutto ciò si applica a XFA, dove il valore vive in un pacchetto dati XML anziché nei dizionari AcroForm, una separazione trattata nelle note su modifiche ai campi XFA che non vengono persistite. La lezione generale vale la pena mantenerla oltre questa singola correzione: ogni volta che una API prende l'handle del form oltre all'annotazione, ti sta dicendo che risolverà la gerarchia dei campi per te, e ogni volta che prende solo l'annotazione leggerà esattamente l'oggetto che le hai passato. Quella distinzione governa anche lo scambio dati, poiché esportare e importare dati form XFDF funziona in nomi di campo pienamente qualificati, mai in posizioni widget
L'appiattimento dei form è una di quelle funzionalità che sembra una singola chiamata API e si rivela essere un contratto tra tre dizionari. Se preferisci lavorare con un componente che codifica già quel contratto, il PDFium Component per Delphi e C++Builder fornisce la rigenerazione degli aspetti, l'appiattimento, e l'accesso ai campi form descritti qui come proprietà e metodi ordinari