Teknisk artikel

Når PDF-formularfeltværdier vender tilbage i Delphi

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

PDFium Component i Delphi: to direkte dictionaries påstår samme felt, skrivningen rammer den promoverede annotation, og felttræet læser stadig den gamle /Fields-kopi
skrivningen serialiserede trofast ind i et objekt, felthierarkiet ikke ejer, så den forældede værdi overlever gemning og genåbning

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 /NM på 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 /Parent og den af PDFium-regenererede /AP; enhver anden forskel afviser skrivningen
  • Begge forælderkæder tjekkes separat, så en korrekt /Parent på sideannotationen ikke kan maskere en modstridende på den oprindelige direkte kopi
  • En manglende /Parent leveres 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

Hvordan PDFium Component til Delphi forener direkte AcroForm-feltejerskab: snapshot, NM-identitetstjek, referensforening, værdiskrivning og verificeret publicering med eksplicitte afvisningsveje
direkte felt-dictionaries promoveres og rebindes til én indirekte ejer, før /V ændres, og tvetydigt ejerskab afvises rent

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

Skriv-og-verificér-mønster for PDF-formularfelter i Delphi med PDFium Component: typet SetFormFieldValue, læsning tilbage gennem FormField, derefter gem- og genåbn-tjek
en True-returværdi alene beviser, at dictionary'en ændrede sig — læs tilbage gennem felttræet og test en gemning og genåbning
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 False plus ErrorMessage som information: en afvisning, der navngiver NM, 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 SetFormFieldValues til multi-felt- eller cross-page-udfyldninger, så et sent fejltilfælde ikke kan efterlade halvdelen af formularen opdateret
  • Kør aldrig Trim eller andre string-RTL-hjælpere over rå PDF-byte gemt i AnsiString

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