Teknisk artikel

Varför återgår PDF-formulärvärden: direkta fält i Delphi

Ett PDF-formulärfältvärde ”återgår” när fältet lagras som en direkt dictionary: PDFium befordrar kopian i sidans /Annots-array till ett indirekt objekt, medan /Fields behåller en andra direkt kopia. PDFium Component for Delphi hanterar typade fältskrivningar genom att förena båda referenserna till en indirekt ägare innan skrivningen

Symptomet låter oftast som ett spökeri. Skrivningen returnerar True, och den allra nästa läsningen räcker tillbaka gamla värdet. Spara, öppna på nytt, och gamla värdet står kvar. Ingenting kastade, och PDF:en ser helt vanlig ut i varje viewer du äger

Symptomet: en lyckad skrivning som inte fastnar

Felmönstret är smalt men mycket repeterbart: det visar sig bara på formulär vars fältdictionaryer skrivs inline (som direkta objekt) snarare än som numrerade indirekta objekt. De flesta författarverktyg emitterar indirekta fält, så problemet gömmer sig i månader och dyker sedan upp på filer från en viss generator, oftast en handbyggd exportör som skriver hela /AcroForm inline

Två observationer tillsammans pekar på orsaken: skrivningen rapporterar framgång, och en nativ läsning av fältet genom formulärlagern returnerar föregående värde. Ser du båda, sluta misstänka din strängkodning och börja titta på objektägande

Varför återgår ett PDF-formulärvärde efter en lyckad skrivning?

Ett PDF-formulärvärde återgår för två separata dictionaryer gör båda anspråk på att vara samma fält, och skrivningen uppdaterade kopian som fältträdet inte rådfrågar. Ett fältvärde bor i dess /V-post, och ISO 32000-1 §12.7.3.1 definierar var den posten är auktoritativ: på fältdictionaryn nådd från /AcroForm /Fields, vandrande ner genom /Kids till terminalfältet, med ärvbara poster lösta från /Parent

Samma klausul tillåter en bekvämlighet som orsakar allt trubbel. När ett fält har exakt en widget-annotation får fältdictionaryn och widget-annotationdictionaryn slås ihop till en enda dictionary. I en välformad fil är den sammanslagna dictionaryn ett indirekt objekt, refererat från två ställen: från /Fields (eller en förälders /Kids) och från sidans /Annots-array. Två referenser, ett objekt, en /V

Två kopior av en dictionary

ISO 32000-1 §7.3.10 är explicit med att bara ett indirekt objekt har en identitet som kan delas. En direkt dictionary har inget objektnummer, så när en generator skriver den sammanslagna fält-/widget-dictionaryn inline i både /Fields och /Annots innehåller filen inte ett objekt refererat två gånger. Den innehåller två lika-seende dictionaryer som råkar ha samma nycklar och värden

PDFium tolererar det, och gör därmed splittringen permanent. När den behöver ett handle till annotationen kan PDFium befordra den direkta widgetdictionaryn i sidans /Annots-array till ett indirekt objekt, men den motsvarande direkta kopian stannar där den var inuti /AcroForm /Fields (eller inuti förälderns /Kids). Från det ögonblicket håller dokumentet två ägare för ett logiskt fält. En skrivare som redigerar /V på sidannotationen får en genuin framgång på dictionarynivå. Fältträdet, vilket är vad formulärlagern och varje senare läsare behandlar som sanningens källa, ser aldrig ändringen

Det är varför det inaktuella värdet överlever en sparning och återöppning: det nya värdet serialiserades troget in i ett objekt fälthierarkin inte äger

PDFium Component i Delphi: två direkta dictionaryer gör anspråk på ett fält, skrivningen träffar den befordrade annotationen och fältträdet läser fortfarande den gamla /Fields-kopian
skrivningen serialiserades troget in i ett objekt fälthierarkin inte äger, så det inaktuella värdet överlever sparning och återöppning

Hur återställer PDFium Component en enda fältägare?

PDFium Component normaliserar direkta fältäganden före varje typad skrivning: den befordrar direkta /AcroForm- och fältdictionaryer till indirekta objekt, binder varje direkt widget till sin matchande sidannotation, och skriver om /Fields, /Kids, /Parent och /Annots så att varje referens pekar på samma indirekta objekt. Först därefter ändrar den /V, så att fältvärdet, den valda widgeten och det sparade dokumentet delar en ägare

Det svåra är identiteten. Två direkta dictionaryer med identiskt innehåll bevisar inte att de är samma widget; ett formulär kan legitimt innehålla två widgets som ser likadana ut. Normaliseraren vägrar därför att gissa, och kräver ett explicit, unikt /NM-annotationsnamn (ISO 32000-1 §12.5.2) på varje direkt widget. Reglerna som skrivvägen tillämpar är strikta:

  • En direkt widget behöver ett icke-tomt /NM på högst 1024 UTF-16-enheter, och exakt en sidannotation får bära samma namn
  • Den direkta kopian och sidannotationen måste serialiseras identiskt utöver /Parent och PDFium-regenererade /AP; vilken annan skillnad som helst avvisar skrivningen
  • Båda föräldralänkarna kontrolleras separat, så en korrekt /Parent på sidannotationen inte kan maskera en motsägelsefull på den ursprungliga direkta kopian
  • En saknad /Parent levereras bara när unikt fältträd-ägande gör svaret entydigt, och likvärdiga direkta föräldrakopior återbinds till sin befordrade ägare
  • Dubbletter av fullständigt kvalificerade fältnamn, cykliska eller mångägte fältträd och signaturfält avvisas innan något publiceras
  • Orelaterade annotationer, som länkar på samma sida, och orelaterade /AcroForm-poster behålls orörda

Normaliseraren kör också under hårda budgeter: 4096 fältträdsbesök, sidor eller arraymedlemmar, nästningsdjup 64, 256 nya indirekta objekt, 1 MiB per befordrad dictionary, 4 MiB befordrade byte totalt, 128 MiB inspekterade dictionary-byte, och 4096 UTF-16-enheter per fullständigt fältnamn med 262144 över formuläret. En fientlig eller trasig fil träffar en budget och fallerar med ett felmeddelande i stället för att göra en formulärfyllning till en denial of service

Hela operationen är transaktionell. TPdf.SetFormFieldValues infångar en snapshot, bygger en inkrementell uppdatering innehållande de normaliserade objekten och de nya värdena, laddar den kandidaten, och ersätter först det levande dokumentet när kandidaten verifierar. Avvisar normaliseringen filen, eller fallerar en senare request i en batch, lämnas originaldokumentet, dess värden och dess logiska fältcursor exakt som de var

Hur PDFium Component för Delphi förenar direkt AcroForm-fältägande: snapshot, NM-identitetskontroll, referensförening, värdeskrivning och verifierad publicering med explicita avvisningsvägar
direkta fältdictionaryer befordras och återbinds till en indirekt ägare innan /V ändras, och tvetydigt ägande avvisas rent

Skriva och verifiera ett direkt fältvärde i Delphi

Det säkra mönstret är att skriva, sedan läsa tillbaka genom det nativa formulärlagret, och behandla en avvikelse som ett fel även när skrivningen returnerade True. TPdf.SetFormFieldValue adresserar ett fält per index på den aktuella sidan, tar en typad TPdfFormValue och fyller en TPdfFormValueWriteReport; TPdf.FormField[Index] läser värdet tillbaka genom PDFiums formulärlager, som löser fältträdet i stället för att lita på widgeten

Skriv-och-verifiera-mönstret för PDF-formulärfält i Delphi med PDFium Component: typad SetFormFieldValue, läs tillbaka via FormField, sedan spara-och-återöppna-kontroller
ett True-svar ensamt bevisar att dictionaryn ändrades — läs tillbaka genom fältträdet och testa en sparning och återöppning
uses
  System.SysUtils, PDFium;

procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
  const NewText: WString);
var
  Value: TPdfFormValue;
  Report: TPdfFormValueWriteReport;
begin
  Pdf.PageNumber := PageNo;          // SetFormFieldValue riktar sig mot den aktuella sidan
  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]);

  // Verifiera mot fältträdet, inte mot vår 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 olösbart ägande, till exempel en direkt widget utan /NM, returnerar SetFormFieldValue False, Report.UpdatedWidgets är noll och Report.ErrorMessage namnger problemet, som ”Direct widgets require an explicit unique NM identity”. Det är beteendet du vill ha: en tydlig avvisning i stället för en tyst halvskrivning. Ett förhandsvillkor till är värt att känna till. Den typade skrivningen är endast-annotation och införskaffar inte nativt fokus, så om en nativ fältredigerare fortfarande har fokus avvisas hela transaktionen innan snapshoten tas; anropa Pdf.ClearFormFieldFocus med flit först i stället för att försöka om och om igen i en loop

För flera fält, särskilt över sidor, använd batchformen. TPdf.SetFormFieldValues accepterar 1 till 256 requests, tillämpar alla på en kandidat, och publicerar allt eller inget. TPdfFormValuesWriteReport.FailedRequest talar om vilken request som bröt 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 räknar varje synkad widget
end;

Två requests som adresserar samma terminalfält avvisas som en dubblettägare i stället för att låta sista skrivningen vinna, och källdokumentet bevaras. Checkbox- och radiovärden följer samma regler; /V-mot-/AS-uppdelningen för knappar tas upp i checkbox-fältvärde mot widget-tillstånd i PDFium

Bevisa att värdet överlever sparning och återöppning

En tillbakaläsning i samma session bevisar att det levande dokumentet är sammanhängande. En sparning och återöppning bevisar att filen är det, och kontrollen är billig nog att behålla i dina egna integrationstester

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;

Två byte-nivåfällor till: Trim och trailer-/ID:et

Att normalisera direkta fält betyder att läsa råa dictionary-byte igen, vilket väcker två mindre fällor relevanta för varje Delphi-kod som rör PDF-syntax

Den första är SysUtils.Trim på råa PDF-byte i en AnsiString. En literal sträng som ett choice-alternativ kan legitimt innehålla UTF-16BE-text med en FE FF byte order mark och inbäddade NUL-byte. I Unicode Delphi tar SysUtils.Trim en string, så anropet konverterar tyst AnsiString:en till UTF-16 med den aktiva ANSI-kodsidan och konverterar resultatet tillbaka. Beroende på den kodsidan kan roundtrifen skriva om höga byte, skada BOM:en och till och med störa avgränsaren som följer. Det säkra upplägget håller lexern på byte: hoppa över PDF-whitespace per position (ISO 32000-1 §7.2.2 listar NUL, tab, LF, FF, CR och mellanslag) och skär med Copy, avkoda först när ett värde är känt vara en PDF-textsträng

// Riskabelt på råa PDF-byte: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);

// Bytesäkert: hoppa över PDF-whitespace per position, skär sedan
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
  Inc(P);
Token := Copy(Token, P, MaxInt);

Den andra fällan sitter i den inkrementella uppdateringen själv. ISO 32000-1 §7.5.5 (Table 15) och §14.4 kräver att trailer-/ID:en är en array av två byte-strängar. Den interna hjälparen som komponerar sparse-uppdateringsidentifieraren returnerar bara innehållet i de två strängarna, utan /ID-nyckeln eller arrayhakarna, och trailern måste svepa in den explicit som /ID [ ... ]. PDFiums tillmötesgående laddare, och en senare fullständig omsparning, gömmer båda en saknad wrapper, så den pålitliga kontrollen är att parsa kandidat- och sparade filerna med en oberoende strikt parser och leta efter två 16-byte-strängar. Skriver du inkrementella uppdateringar själv gäller samma fälla; artikeln om inkrementell uppdatering för krypterade PDF:er täcker trailerkedjan mer i detalj

Vad bör du göra i din egen Delphi-formulärkod?

  • Läs alltid värdet tillbaka med FormField[Index] efter en skrivning, och i tester även spara, öppna på nytt och läsa igen
  • Behandla False plus ErrorMessage som information: en avvisning som namnger NM, en föräldrakonflikt eller ett dubblettnamn betyder att filens ägande är genuint tvetydigt
  • Genererar du PDF:er, emittera fält och widgets som indirekta objekt och ge varje widget ett unikt /NM; det tar bort tvetydigheten vid källan
  • Använd SetFormFieldValues för flerfälts- eller över-sidor-ifyllningar så att ett sent fel inte kan lämna halva formuläret uppdaterat
  • Kör aldrig Trim eller andra string-RTL-hjälpare över råa PDF-byte i AnsiString

Var tydlig med gränserna också. Signerade dokument, XFA-formulär, krypterade snapshots och filer vars widget-ägande inte kan lösas unikt stannar utanför den deklarerade profilen och avvisas i stället för att tvingas igenom. Flyttar ditt arbetsflöde fält mellan dokument blir ägandefrågorna ännu svårare; att transplantera AcroForm-fält mellan PDF-dokument tar det fallet

PDFium Component ger den här typade, transaktionella formulärskrivningen till Delphi-, C++Builder- och Lazarus-applikationer bredvid rendering, textextrahering och dokumentredigering. Versionshistorik, licensiering och nedladdningar finns på produktsidan för PDFium Component for Delphi