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
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
/NMpå 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
/Parentoch PDFium-regenererade/AP; vilken annan skillnad som helst avvisar skrivningen - Båda föräldralänkarna kontrolleras separat, så en korrekt
/Parentpå sidannotationen inte kan maskera en motsägelsefull på den ursprungliga direkta kopian - En saknad
/Parentlevereras 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
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
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
FalseplusErrorMessagesom information: en avvisning som namngerNM, 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
SetFormFieldValuesför flerfälts- eller över-sidor-ifyllningar så att ett sent fel inte kan lämna halva formuläret uppdaterat - Kör aldrig
Trimeller andrastring-RTL-hjälpare över råa PDF-byte iAnsiString
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