En PDF-formularfeltværdi "vender tilbage", når feltet er gemt som en direkte dictionary: PDFium promoverer kopien i sidens /Annots-array til et indirekte objekt, mens /Fields beholder en anden direkte kopi. PDFium Component til Delphi håndterer typede felt-skrivninger ved at forene begge referencer til én indirekte ejer, før der skrives
Symptomet ligner som regel et spøgelse. Skrivningen returnerer True, og den allernæste læsning giver den gamle værdi tilbage. Gem, genåbn, og den gamle værdi er der stadig. Intet kastede en fejl, og PDF'en ser helt almindelig ud i hver viewer, du ejer
Symptomet: en vellykket skrivning, der ikke hænger ved
Fejlmønsteret er snævert, men meget gentageligt: det viser sig kun på formularer, hvis felt-dictionaries er skrevet inline (som direkte objekter) frem for som nummererede indirekte objekter. De fleste authoring-værktøjer udsender indirekte felter, så problemet skjuler sig i måneder og optræder så på filer fra én bestemt generator, ofte en håndbygget eksportør, der skriver hele /AcroForm inline
To observationer peger sammen på årsagen: skrivningen rapporterer succes, og en native læsning af feltet gennem form-laget returnerer den forrige værdi. Ser du begge, så hold op med at mistænke din streng-encoding og begynd at kigge på objektejerskab
Hvorfor vender en PDF-formularfeltværdi tilbage efter en vellykket skrivning?
En PDF-formularfeltværdi vender tilbage, for to separate dictionaries påstår begge at være samme felt, og skrivningen opdaterede den kopi, felttræet ikke konsulterer. Et felts værdi bor i dets /V-post, og ISO 32000-1 §12.7.3.1 definerer, hvor den post er autoritativ: på felt-dictionary'en nået fra /AcroForm /Fields, ned gennem /Kids til terminalfeltet, med nedarvede poster opløst fra /Parent
Samme klausul tillader en bekvemmelighed, der forårsager alt besværet. Har et felt præcis én widget-annotation, må felt-dictionary'en og widget-annotation-dictionary'en flettes til én enkelt dictionary. I en velformet fil er den flettede dictionary ét indirekte objekt, refereret fra to steder: fra /Fields (eller en forælders /Kids) og fra sidens /Annots-array. To referencer, ét objekt, én /V
To kopier af én dictionary
ISO 32000-1 §7.3.10 er eksplicit om, at kun et indirekte objekt har en identitet, der kan deles. En direkte dictionary har intet objektnummer, så når en generator skriver den flettede felt/widget-dictionary inline i både /Fields og /Annots, indeholder filen ikke ét objekt refereret to gange. Den indeholder to ens udseende dictionaries, der tilfældigvis har de samme nøgler og værdier
PDFium tolererer det, og gør dermed delingen permanent. Når den behøver et handle til annotationen, kan PDFium promovere den direkte widget-dictionary i sidens /Annots-array til et indirekte objekt, men den ækvivalente direkte kopi bliver, hvor den var, inde i /AcroForm /Fields (eller inde i forælders /Kids). Fra det øjeblik holder dokumentet to ejere af ét logisk felt. En writer, der redigerer /V på sideannotationen, får en ægte succes på dictionary-niveau. Felttræet, som form-laget og enhver senere læser behandler som sandhedskilden, ser aldrig ændringen
Det er derfor, den forældede værdi overlever en gemning og genåbning: den nye værdi blev serialiseret trofast ind i et objekt, felthierarkiet ikke ejer
Hvordan PDFium Component gendanner én enkelt feltejer
PDFium Component normaliserer direkte feltejerskab før hver typet skrivning: den promoverer direkte /AcroForm- og felt-dictionaries til indirekte objekter, binder hver direkte widget til dens matchende sideannotation og omskriver /Fields, /Kids, /Parent og /Annots, så hver reference peger på samme indirekte objekt. Først derefter ændrer den /V, så feltværdien, den valgte widget og det gemte dokument deler én ejer
Den svære del er identitet. To direkte dictionaries med identisk indhold beviser ikke, at de er samme widget; en formular kan legitimt indeholde to widgets, der ligner hinanden. Normalisereren nægter derfor at gætte og kræver et eksplicit, unikt /NM-annotationsnavn (ISO 32000-1 §12.5.2) på hver direkte widget. Reglerne, skrivevejen anvender, er strenge:
- En direkte widget behøver en ikke-tom
/NMpå højst 1024 UTF-16-enheder, og præcis én sideannotation må bære samme navn - Den direkte kopi og sideannotationen skal serialiseres identisk ud over
/Parentog den af PDFium-regenererede/AP; enhver anden forskel afviser skrivningen - Begge forælderkæder tjekkes separat, så en korrekt
/Parentpå sideannotationen ikke kan maskere en modstridende på den oprindelige direkte kopi - En manglende
/Parentleveres kun, når unikt felttræ-ejerskab gør svaret entydigt, og ækvivalente direkte forælderkopier rebindes til deres promoverede ejer - Duplikerede fuldt kvalificerede feltnavne, cykliske eller multi-ejede felttræer og signaturfelter afvises, før noget publiceres
- Urelaterede annotationer, som links på samme side, og urelaterede
/AcroForm-poster beholdes urørte
Normalisereren kører også under hårde budgets: 4096 felttræ-besøg, sider eller array-medlemmer, nestingdybde 64, 256 nye indirekte objekter, 1 MiB pr. promoveret dictionary, 4 MiB promoverede byte i alt, 128 MiB inspicerede dictionary-byte og 4096 UTF-16-enheder pr. fuldt feltnavn med 262144 på tværs af formularen. En fjendtlig eller ødelagt fil rammer et budget og fejler med en fejlbesked i stedet for at gøre en formularudfyldning til et denial of service
Hele operationen er transactionel. TPdf.SetFormFieldValues fanger et snapshot, bygger en inkrementel opdatering indeholdende de normaliserede objekter og de nye værdier, loader den kandidat og erstatter først det live dokument, når kandidaten verificeres. Afviser normaliseringen filen, eller fejler en senere request i en batch, efterlades det oprindelige dokument, dets værdier og dets logiske felt-cursor præcis, som de var
At skrive og verificere en direkte feltværdi i Delphi
Det sikre mønster er at skrive, derefter læse tilbage gennem det native form-lag og behandle en mismatch som en fejl, selv når skrivningen returnerede True. TPdf.SetFormFieldValue adresserer et felt efter indeks på den aktuelle side, tager en typet TPdfFormValue og fylder en TPdfFormValueWriteReport; TPdf.FormField[Index] læser værdien tilbage gennem PDFiums form-lag, som opløser felttræet i stedet for at stole på widgeten
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue rammer den aktuelle side
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]);
// Verificér mod felttræet, ikke mod vores egen variabel
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
På en fil med uopløseligt ejerskab, for eksempel en direkte widget uden /NM, returnerer SetFormFieldValue False, Report.UpdatedWidgets er nul, og Report.ErrorMessage navngiver problemet, som "Direct widgets require an explicit unique NM identity". Det er adfærden, du vil have: en klar afvisning i stedet for en lydløs halvskrivning. Én forudsætning mere er værd at kende. Den typede skrivning er annotation-only og erhverver ikke native fokus, så er en native felteditor stadig fokuseret, afviser hele transaktionen, før snapshotet tages; kald Pdf.ClearFormFieldFocus bevidst først frem for at genprøve i en løkke
Til flere felter, især på tværs af sider, brug batchformen. TPdf.SetFormFieldValues accepterer 1 til 256 requests, anvender alle på én kandidat og publicerer alt eller intet. TPdfFormValuesWriteReport.FailedRequest fortæller dig, hvilken request der brød batchen
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 tæller hver synkroniseret widget
end;
To requests, der adresserer samme terminalfelt, afvises som en duplikeret ejer i stedet for at lade sidste skrivning vinde, og kildedokumentet bevares. Checkbox- og radioværdier følger de samme regler; opdelingen /V versus /AS for knapper er dækket i checkbox-feltværdi versus widget-tilstand i PDFium
At bevise, at værdien overlever gemning og genåbning
En tilbage læsning i samme session beviser, at det live dokument er sammenhængende. En gemning og genåbning beviser, at filen er det, og tjekket er billigt nok til at beholdes i dine egne integrationstests
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;
To fælder mere på byteniveau: Trim og trailerens /ID
At normalisere direkte felter betyder at genlæse rå dictionary-byte, hvilket bringer to mindre fælder op, relevante for al Delphi-kode, der rører PDF-syntaks
Den første er SysUtils.Trim på rå PDF-byte gemt i en AnsiString. En literal streng som et choice-valg kan legitimt indeholde UTF-16BE-tekst med et FE FF byte order mark og indlejrede NUL-byte. I Unicode Delphi tager SysUtils.Trim en string, så kaldet konverterer lydløst AnsiString til UTF-16 med den aktive ANSI-kodepage og konverterer resultatet tilbage. Afhængigt af den kodepage kan round trip'en omskrive høje byte, beskadige BOM'en og endda forstyrre afgrænsningen bagefter. Den sikre tilgang holder lexeren på byte: spring PDF-whitespace over efter position (ISO 32000-1 §7.2.2 lister NUL, tab, LF, FF, CR og mellemrum) og udskær med Copy, og afkod først, når en værdi vides at være en PDF-tekststreng
// Riskant på rå PDF-byte: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Byte-sikker: spring PDF-whitespace over efter position, og udskær så
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
Den anden fælde sidder i selve den inkrementelle opdatering. ISO 32000-1 §7.5.5 (Table 15) og §14.4 kræver, at trailerens /ID er et array af to bytestrenge. Den interne hjælper, der komponerer den sparse opdaterings-identifikator, returnerer kun indholdet af de to strenge, uden /ID-nøglen eller arrayklammerne, og traileren skal pakke den eksplicit som /ID [ ... ]. PDFiums eftergivende loader og en senere fuld resave skjuler begge en manglende wrapper, så det pålidelige tjek er at parse kandidat- og gemte filer med en uafhængig striks parser og kigge efter to 16-byte-strenge. Skriver du selv inkrementelle opdateringer, gælder samme fælde; artiklen om inkrementelle opdateringer for krypterede PDF'er går dybere ned i trailerkæden
Hvad bør du gøre i din egen Delphi-formularkode?
- Læs altid værdien tilbage med
FormField[Index]efter en skrivning, og gem, genåbn og læs igen i tests - Behandl
FalseplusErrorMessagesom information: en afvisning, der navngiverNM, en forælderkonflikt eller et duplikeret navn betyder, at filens ejerskab reelt er tvetydigt - Genererer du PDF'er, så udsend felter og widgets som indirekte objekter og giv hver widget et unikt
/NM; det fjerner tvetydigheden ved kilden - Brug
SetFormFieldValuestil multi-felt- eller cross-page-udfyldninger, så et sent fejltilfælde ikke kan efterlade halvdelen af formularen opdateret - Kør aldrig
Trimeller andrestring-RTL-hjælpere over rå PDF-byte gemt iAnsiString
Vær også klar over grænserne. Signerede dokumenter, XFA-formularer, krypterede snapshots og filer, hvis widget-ejerskab ikke kan opløses entydigt, forbliver uden for den deklarerede profil og afvises i stedet for at blive tvunget igennem. Flytter dit workflow felter mellem dokumenter, bliver ejerskabsspørgsmålene endnu sværere; grafting af AcroForm-felter på tværs af PDF-dokumenter handler om det tilfælde
PDFium Component bringer denne typede, transactionelle formularskrivning til Delphi-, C++Builder- og Lazarus-programmer ved siden af rendering, tekstekstraktion og dokumentredigering. Versionshistorik, licensering og downloads findes på PDFium Component til Delphi-productsiden