Un valore di campo form PDF "torna indietro" quando il campo è memorizzato come dizionario diretto: PDFium promuove la copia nell'array /Annots della pagina a oggetto indiretto, mentre /Fields conserva una seconda copia diretta. PDFium Component for Delphi gestisce le scritture tipizzate dei campi unificando entrambi i riferimenti in un solo proprietario indiretto prima di scrivere
Il sintomo di solito si legge come un'infestazione. La scrittura restituisce True, e la lettura immediatamente successiva riconsegna il valore vecchio. Salvate, riaprite, e il valore vecchio è ancora lì. Nulla ha sollevato, e il PDF sembra perfettamente ordinario in ogni viewer che possedete
Il sintomo: una scrittura riuscita che non atterra
Il pattern di fallimento è stretto ma molto ripetibile: compare solo sulle form i cui dizionari dei campi sono scritti inline (come oggetti diretti) invece che come oggetti indiretti numerati. La maggior parte degli strumenti di authoring emette campi indiretti, quindi il problema si nasconde per mesi e poi compare sui file di un generatore particolare, spesso un exporter artigianale che scrive l'intero /AcroForm inline
Due osservazioni insieme puntano alla causa: la scrittura riporta successo, e una lettura nativa del campo attraverso il layer form restituisce il valore precedente. Quando vedete entrambe, smettete di sospettare la vostra codifica delle stringhe e iniziate a guardare la proprietà degli oggetti
Perché un valore di campo form PDF torna indietro dopo una scrittura riuscita?
Un valore di campo form PDF torna indietro perché due dizionari separati rivendicano entrambi di essere lo stesso campo, e la scrittura ha aggiornato la copia che l'albero dei campi non consulta. Il valore di un campo vive nella sua voce /V, e ISO 32000-1 §12.7.3.1 definisce dove quella voce fa autorità: sul dizionario campo raggiunto da /AcroForm /Fields, scendendo per /Kids fino al campo terminale, con le voci ereditabili risolte dal /Parent
La stessa clausola consente una comodità che causa tutto il guaio. Quando un campo ha esattamente una annotation widget, il dizionario campo e il dizionario dell'annotation widget possono essere fusi in un unico dizionario. In un file ben formato quel dizionario fuso è un solo oggetto indiretto, riferito da due posti: da /Fields (o dai /Kids di un padre) e dall'array /Annots della pagina. Due riferimenti, un oggetto, un /V
Due copie di uno stesso dizionario
ISO 32000-1 §7.3.10 è esplicito sul fatto che solo un oggetto indiretto ha un'identità condivisibile. Un dizionario diretto non ha numero di oggetto, quindi quando un generatore scrive il dizionario fuso campo/widget inline sia in /Fields sia in /Annots, il file non contiene un oggetto riferito due volte. Contiene due dizionari dall'aspetto uguale che per caso hanno le stesse chiavi e gli stessi valori
PDFium lo tollera, e così facendo rende permanente la spaccatura. Quando gli serve un handle all'annotation, PDFium può promuovere a oggetto indiretto il dizionario widget diretto nell'array /Annots della pagina, però la copia diretta equivalente sta dove era dentro /AcroForm /Fields (o dentro i /Kids del padre). Da quel momento il documento tiene due proprietari per un campo logico. Uno writer che edita /V sull'annotation di pagina ottiene un successo autentico a livello di dizionario. L'albero dei campi, che è ciò che il layer form e ogni reader successivo trattano come fonte di verità, non vede mai la modifica
Ecco perché il valore vecchio sopravvive a un salvataggio e riapertura: il valore nuovo è stato serializzato fedelmente dentro un oggetto che la gerarchia dei campi non possiede
Come PDFium Component ripristina un unico proprietario del campo
PDFium Component normalizza la proprietà diretta dei campi prima di ogni scrittura tipizzata: promuove a oggetti indiretti i dizionari /AcroForm e campo diretti, lega ogni widget diretto alla sua annotation di pagina corrispondente, e riscrive /Fields, /Kids, /Parent e /Annots così che ogni riferimento punti allo stesso oggetto indiretto. Solo allora cambia /V, così valore del campo, widget selezionato e documento salvato condividono un proprietario
La parte difficile è l'identità. Due dizionari diretti dal contenuto identico non provano di essere lo stesso widget; una form può legittimamente contenere due widget che sembrano uguali. Il normalizzatore quindi rifiuta di indovinare, ed esige un nome annotation /NM esplicito e unico (ISO 32000-1 §12.5.2) su ogni widget diretto. Le regole applicate dal percorso di scrittura sono severe:
- un widget diretto vuole un
/NMnon vuoto di al massimo 1024 unità UTF-16, ed esattamente una annotation di pagina può portare lo stesso nome - la copia diretta e l'annotation di pagina devono serializzarsi identiche a parte
/Parente il/APrigenerato da PDFium; qualunque altra differenza rifiuta la scrittura - entrambi i link padre vengono controllati separatamente, così un
/Parentcorretto sull'annotation di pagina non può mascherarne uno conflittuale sulla copia diretta originale - un
/Parentmancante viene fornito solo quando una proprietà unica dell'albero dei campi rende la risposta non ambigua, e le copie dirette padri equivalenti vengono riagganciate al loro proprietario promosso - nomi di campo completi duplicati, alberi dei campi ciclici o a proprietà multipla e campi firma rifiutano prima che qualcosa venga pubblicato
- le annotation non correlate, come i link sulla stessa pagina, e le voci
/AcroFormnon correlate vengono conservate intatte
Il normalizzatore gira anche sotto budget rigidi: 4096 visite dell'albero dei campi, pagine o membri di array, profondità di annidamento 64, 256 nuovi oggetti indiretti, 1 MiB per dizionario promosso, 4 MiB di byte promossi in totale, 128 MiB di byte di dizionario ispezionati, e 4096 unità UTF-16 per nome campo completo con 262144 sull'intera form. Un file ostile o rotto becca un budget e fallisce con un messaggio di errore invece di trasformare un riempimento form in un denial of service
L'intera operazione è transazionale. TPdf.SetFormFieldValues cattura un'istantanea, costruisce un incremental update contenente gli oggetti normalizzati e i valori nuovi, carica quel candidate, e sostituisce il documento vivo solo dopo che il candidate verifica. Se la normalizzazione rifiuta il file, o una richiesta successiva in un batch fallisce, il documento originale, i suoi valori e il suo cursore logico dei campi vengono lasciati esattamente com'erano
Scrivere e verificare un valore di campo diretto in Delphi
Il pattern sicuro è scrivere, poi rileggere attraverso il layer form nativo, e trattare una discrepanza come fallimento anche quando la scrittura ha restituito True. TPdf.SetFormFieldValue indirizza un campo per indice sulla pagina corrente, prende un TPdfFormValue tipizzato e riempie un TPdfFormValueWriteReport; TPdf.FormField[Index] rilegge il valore attraverso il layer form di PDFium, che risolve l'albero dei campi invece di fidarsi del widget
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue punta alla pagina corrente
Value := Default(TPdfFormValue);
Value.Kind := fvkText;
Value.Text := NewText;
if not Pdf.SetFormFieldValue(FieldIndex, Value, Report) then
raise Exception.CreateFmt('Field %d on page %d rejected: %s',
[FieldIndex, PageNo, Report.ErrorMessage]);
// Verificate contro l'albero dei campi, non contro la nostra variabile
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Su un file con proprietà non risolvibile, per esempio un widget diretto senza /NM, SetFormFieldValue restituisce False, Report.UpdatedWidgets è zero e Report.ErrorMessage nomina il problema, tipo "Direct widgets require an explicit unique NM identity". È il comportamento che volete: un rifiuto chiaro invece di una mezza scrittura silenziosa. Un'altra precondizione merita di essere saputa. La scrittura tipizzata è annotation-only e non acquisisce il focus nativo, quindi se un editor di campo nativo è ancora focalizzato l'intera transazione rifiuta prima che l'istantanea venga catturata; chiamate Pdf.ClearFormFieldFocus deliberatamente prima invece di ritentare in un ciclo
Per diversi campi, soprattutto tra pagine, usate la forma batch. TPdf.SetFormFieldValues accetta da 1 a 256 richieste, le applica tutte a un solo candidate, e pubblica tutto o niente. TPdfFormValuesWriteReport.FailedRequest dice quale richiesta ha rotto il batch
procedure FillInvoiceHeader(Pdf: TPdf);
var
Requests: TPdfFormValueRequests;
Batch: TPdfFormValuesWriteReport;
begin
SetLength(Requests, 2);
Requests[0] := Default(TPdfFormValueRequest);
Requests[0].Page := 1;
Requests[0].FieldIndex := 0;
Requests[0].Value.Kind := fvkText;
Requests[0].Value.Text := 'ACME Tooling Inc.';
Requests[1] := Default(TPdfFormValueRequest);
Requests[1].Page := 2;
Requests[1].FieldIndex := 0;
Requests[1].Value.Kind := fvkText;
Requests[1].Value.Text := '2026-10-27';
if not Pdf.SetFormFieldValues(Requests, Batch) then
raise Exception.CreateFmt('Request %d failed: %s',
[Batch.FailedRequest, Batch.ErrorMessage]);
// Batch.UpdatedFields = 2; Batch.UpdatedWidgets conta ogni widget sincronizzato
end;
Due richieste che indirizzano lo stesso campo terminale vengono rifiutate come proprietario duplicato invece di lasciar vincere l'ultima scrittura, e il documento sorgente viene preservato. I valori checkbox e radio seguono le stesse regole; la ripartizione /V contro /AS per i pulsanti è trattata in valore del campo checkbox contro stato del widget in PDFium
Dimostrare che il valore sopravvive a salvataggio e riapertura
Una rilettura nella stessa sessione prova che il documento vivo è coerente. Un salvataggio e riapertura prova che lo è il file, e il controllo costa abbastanza poco da tenere nei vostri test di integrazione
procedure SaveAndReopenCheck(Pdf: TPdf; const Target: string;
PageNo, FieldIndex: Integer; const Expected: WString);
var
Reopened: TPdf;
begin
if not Pdf.SaveAs(Target, saNoIncremental, pvUnknown) then
raise Exception.Create('Save failed');
Reopened := TPdf.Create(nil);
try
Reopened.FileName := Target;
Reopened.Active := True;
Reopened.PageNumber := PageNo;
if Reopened.FormField[FieldIndex] <> Expected then
raise Exception.Create('Saved file lost the field value');
finally
Reopened.Free;
end;
end;
Due trappole ulteriori a livello di byte: Trim e il trailer /ID
Normalizzare i campi diretti significa rileggere byte grezzi dei dizionari, il che porta in scena due trappole minori rilevanti per qualunque codice Delphi che tocchi la sintassi PDF
La prima è SysUtils.Trim su byte PDF grezzi tenuti in un AnsiString. Una stringa letterale come un'opzione choice può legittimamente contenere testo UTF-16BE con un byte order mark FE FF e byte NUL incorporati. Nel Delphi Unicode, SysUtils.Trim prende uno string, quindi la chiamata converte in silenzio l'AnsiString in UTF-16 usando la code page ANSI attiva e riconverte il risultato indietro. A seconda di quella code page, il viaggio di andata e ritorno può riscrivere i byte alti, danneggiare il BOM e perfino disturbare il delimitatore che segue. L'approccio sicuro tiene il lexer sui byte: saltate il whitespace PDF per posizione (ISO 32000-1 §7.2.2 elenca NUL, tab, LF, FF, CR e spazio) e affettate con Copy, decodificando solo una volta che un valore è noto essere una stringa testo PDF
// Rischioso su byte PDF grezzi: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Sicuro sui byte: saltate il whitespace PDF per posizione, poi affettate
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
La seconda trappola sta nell'incremental update stesso. ISO 32000-1 §7.5.5 (Table 15) e §14.4 richiedono che il trailer /ID sia un array di due stringhe di byte. L'helper interno che compone l'identificatore dell'update sparso restituisce solo il contenuto delle due stringhe, senza la chiave /ID né le parentesi dell'array, e il trailer deve avvolgerlo esplicitamente come /ID [ ... ]. Il loader permissivo di PDFium, e un successivo salvataggio completo, nascondono entrambi un wrapper mancante, quindi il controllo affidabile è fare il parsing del candidate e dei file salvati con un parser severo indipendente e cercare due stringhe da 16 byte. Se scrivete incremental update da soli, la stessa trappola si applica; l'articolo sull'incremental update per PDF cifrati copre la catena del trailer più a fondo
Cosa dovreste fare nel vostro codice form Delphi?
- Rileggete sempre il valore con
FormField[Index]dopo una scrittura, e nei test salvate, riaprite e rileggete anche - Trattate
FalsepiùErrorMessagecome informazione: un rifiuto che nominaNM, un conflitto di padre o un nome duplicato significa che la proprietà del file è davvero ambigua - Se generate PDF, emettete campi e widget come oggetti indiretti e date a ogni widget un
/NMunico; questo toglie l'ambiguità alla fonte - Usate
SetFormFieldValuesper i riempimenti multi-campo o tra pagine così un fallimento tardivo non può lasciare mezza form aggiornata - Non fate mai girare
Trimo altri helper RTLstringsu byte PDF grezzi tenuti inAnsiString
Siate chiari anche sui confini. Documenti firmati, form XFA, istantanee cifrate e file la cui proprietà dei widget non è risolvibile in modo unico restano fuori dal profilo dichiarato e rifiutano invece di essere forzati attraverso. Se il vostro flusso sposta campi tra documenti, le domande di proprietà si fanno ancora più difficili; innesto di campi AcroForm tra documenti PDF tratta quel caso
Il PDFium Component porta questa scrittura form tipizzata e transazionale alle applicazioni Delphi, C++Builder e Lazarus accanto a rendering, estrazione testo e editing di documenti. Storia delle versioni, licenze e download sono sulla pagina del prodotto PDFium Component for Delphi