Een PDF-formulierveldwaarde "valt terug" wanneer het veld als directe dictionary is opgeslagen: PDFium promoveert de kopie in de /Annots-array van de pagina naar een indirect object, terwijl /Fields een tweede directe kopie vasthoudt. PDFium Component for Delphi behandelt getypeerde veldschrijfacties door beide verwijzingen samen te voegen tot één indirecte eigenaar voordat er wordt geschreven
Het symptoom leest meestal als een spookverhaal. De schrijfactie geeft True terug en de allereerste volgende lezing geeft de oude waarde terug. Opslaan, heropenen, en de oude waarde staat er nog steeds. Er is niets gegooid en de PDF ziet er in elke viewer die je hebt volkomen normaal uit
Het symptoom: een geslaagde schrijfactie die niet blijft plakken
Het faalpatroon is smal maar heel herhaalbaar: het toont zich alleen op formulieren waarvan de veld-dictionaries inline zijn weggeschreven (als directe objecten) in plaats van als genummerde indirecte objecten. De meeste authoringtools emit indirecte velden, dus het probleem verbergt zich maandenlang en verschijnt dan op bestanden van één specifieke generator, vaak een handgemaakte exporter die de hele /AcroForm inline wegschrijft
Twee waarnemingen wijzen samen op de oorzaak: de schrijfactie meldt succes en een native lezing van het veld via de form-laag geeft de vorige waarde terug. Zie je beide, houd dan op met je stringcodering te verdenken en ga naar objecteigendom kijken
Waarom valt een PDF-formulierveldwaarde terug na een geslaagde schrijfactie?
Een PDF-formulierveldwaarde valt terug omdat twee aparte dictionaries allebei beweren hetzelfde veld te zijn, en de schrijfactie de kopie bijwerkte die de veldboom niet raadpleegt. De waarde van een veld leeft in zijn /V-entry, en ISO 32000-1 §12.7.3.1 definieert waar die entry leidend is: op de veld-dictionary die via /AcroForm /Fields wordt bereikt, dalend via /Kids naar het terminale veld, met overerfbare entries opgelost vanuit /Parent
Dezelfde clausule staat een gemak toe dat alle ellende veroorzaakt. Heeft een veld precies één widgetannotatie, dan mogen de veld-dictionary en de widgetannotatie-dictionary worden samengevoegd tot één dictionary. In een welgevormd bestand is die samengevoegde dictionary één indirect object, waarnaar vanaf twee plekken wordt verwezen: vanuit /Fields (of de /Kids van een ouder) en vanuit de /Annots-array van de pagina. Twee verwijzingen, één object, één /V
Twee kopieën van één dictionary
ISO 32000-1 §7.3.10 is expliciet: alleen een indirect object heeft een identiteit die kan worden gedeeld. Een directe dictionary heeft geen objectnummer, dus wanneer een generator de samengevoegde veld/widget-dictionary inline in zowel /Fields als /Annots wegschrijft, bevat het bestand niet één object dat tweemaal wordt verwezen. Het bevat twee identiek ogende dictionaries die toevallig dezelfde keys en waarden hebben
PDFium tolerert dit en maakt daarmee de splitsing permanent. Wanneer hij een handle naar de annotatie nodig heeft, kan PDFium de directe widget-dictionary in de /Annots-array van de pagina promoveren tot een indirect object, maar de equivalente directe kopie blijft staan waar hij was, binnen /AcroForm /Fields (of binnen de /Kids van de ouder). Vanaf dat moment houdt het document twee eigenaren voor één logisch veld vast. Een writer die /V op de pagina-annotatie bewerkt, krijgt een echte succes op dictionaryniveau. De veldboom — wat de form-laag en elke latere reader als bron van waarheid behandelen — ziet de wijziging nooit
Daarom overleeft de verouderde waarde een opslag en heropenen: de nieuwe waarde is trouw geserialiseerd naar een object dat de veldhiërarchie niet bezit
Hoe PDFium Component één enkele veldeigenaar herstelt
PDFium Component normaliseert direct veldeigendom vóór elke getypeerde schrijfactie: hij promoveert directe /AcroForm- en veld-dictionaries naar indirecte objecten, koppelt elke directe widget aan zijn matchende pagina-annotatie en herschrijft /Fields, /Kids, /Parent en /Annots zodat elke verwijzing naar hetzelfde indirecte object wijst. Pas daarna wijzigt hij /V, zodat de veldwaarde, de geselecteerde widget en het opgeslagen document één eigenaar delen
Het lastige is identiteit. Twee directe dictionaries met identieke inhoud bewijzen niet dat ze dezelfde widget zijn; een formulier kan legitiem twee widgets bevatten die er hetzelfde uitzien. De normalizer weigert daarom te gokken en eist een expliciete, unieke /NM-annotatienaam (ISO 32000-1 §12.5.2) op elke directe widget. De regels die het schrijfpad toepast zijn streng:
- Een directe widget heeft een niet-lege
/NMvan hooguit 1024 UTF-16-units nodig en precies één pagina-annotatie mag dezelfde naam dragen - De directe kopie en de pagina-annotatie moeten identiek serialiseren behalve
/Parenten de door PDFium opnieuw gegenereerde/AP; elk ander verschil weigert de schrijfactie - Beide ouderverbindingen worden apart gecontroleerd, dus een correcte
/Parentop de pagina-annotatie kan een conflicterende op de originele directe kopie niet maskeren - Een ontbrekende
/Parentwordt alleen aangeleverd wanneer uniek veldboom-eigendom het antwoord ondubbelzinnig maakt, en equivalente directe ouderkopieën worden opnieuw gebonden aan hun gepromoveerde eigenaar - Dubbele volledig gekwalificeerde veldnamen, cyclische of meervoudig bezeten veldbomen en handtekeningvelden worden geweigerd voordat er iets wordt gepubliceerd
- Ongerelateerde annotaties, zoals links op dezelfde pagina, en ongerelateerde
/AcroForm-entries blijven onaangetast behouden
De normalizer draait ook onder harde budgetten: 4096 veldboom-bezoeken, pagina's of arrayleden, nestingsdiepte 64, 256 nieuwe indirecte objecten, 1 MiB per gepromoveerde dictionary, in totaal 4 MiB aan gepromoveerde bytes, 128 MiB aan geïnspecteerde dictionarybytes en 4096 UTF-16-units per volledige veldnaam met 262144 over het formulier. Een vijandig of kapot bestand raakt een budget aan en faalt met een foutmelding in plaats van een formuliervulling tot een denial of service te maken
De hele operatie is transactioneel. TPdf.SetFormFieldValues legt een snapshot vast, bouwt een incrementele update met de genormaliseerde objecten en de nieuwe waarden, laadt die kandidaat en vervangt het levende document pas zodra de kandidaat verifieert. Weigert de normalisatie het bestand, of faalt een latere aanvraag in een batch, dan blijven het originele document, zijn waarden en zijn logische veldcursor precies zoals ze waren
Een directe veldwaarde schrijven en verifiëren in Delphi
Het veilige patroon is: schrijven, daarna teruglezen via de native form-laag en een mismatch als fout behandelen, ook als de schrijfactie True teruggaf. TPdf.SetFormFieldValue adresseert een veld op index op de huidige pagina, neemt een getypeerde TPdfFormValue en vult een TPdfFormValueWriteReport; TPdf.FormField[Index] leest de waarde terug via de form-laag van PDFium, die de veldboom resolvet in plaats van de widget te vertrouwen
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue richt zich op de huidige pagina
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]);
// Verifieer tegen de veldboom, niet tegen onze eigen variabele
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Op een bestand met onoplosbaar eigendom, bijvoorbeeld een directe widget zonder /NM, geeft SetFormFieldValue False terug, is Report.UpdatedWidgets nul en benoemt Report.ErrorMessage het probleem, zoals "Direct widgets require an explicit unique NM identity". Dat is het gedrag dat je wilt: een heldere weigering in plaats van een stille halve schrijfactie. Nog één voorwaarde is goed om te weten. De getypeerde schrijfactie is annotation-only en verwerft geen native focus, dus is er nog een native veldeditor met focus, dan weigert de hele transactie voordat de snapshot wordt genomen; roep bewust eerst Pdf.ClearFormFieldFocus aan in plaats van in een lus opnieuw te proberen
Gebruik voor meerdere velden, vooral over pagina's heen, de batchvorm. TPdf.SetFormFieldValues accepteert 1 tot 256 aanvragen, past ze allemaal toe op één kandidaat en publiceert alles of niets. TPdfFormValuesWriteReport.FailedRequest vertelt je welke aanvraag de batch brak
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 telt elke gesynchroniseerde widget
end;
Twee aanvragen die naar hetzelfde terminale veld wijzen worden geweigerd als dubbele eigenaar in plaats van de laatste schrijfactie te laten winnen, en het brondocument blijft behouden. Checkbox- en radio-waarden volgen dezelfde regels; de /V-tegenover-/AS-splitsing voor knoppen wordt behandeld in checkbox-veldwaarde versus widgetstatus in PDFium
Bewijzen dat de waarde opslag en heropenen overleeft
Een teruglezing in dezelfde sessie bewijst dat het levende document coherent is. Een opslag en heropenen bewijst dat het bestand dat is, en de controle is goedkoop genoeg om in je eigen integratietests te houden
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;
Nog twee valkuilen op byteniveau: Trim en de trailer-/ID
Directe velden normaliseren betekent rauwe dictionarybytes opnieuw lezen, wat twee kleinere valkuilen oproept die relevant zijn voor elke Delphi-code die PDF-syntaxis aanraakt
De eerste is SysUtils.Trim op rauwe PDF-bytes in een AnsiString. Een literal string zoals een keuzeoptie kan legitiem UTF-16BE-tekst bevatten met een FE FF-bytevolgordemarkering en ingesloten NUL-bytes. In Unicode Delphi neemt SysUtils.Trim een string, dus de aanroep converteert de AnsiString stilletjes naar UTF-16 via de actieve ANSI-codepage en converteert het resultaat terug. Afhankelijk van die codepage kan de roundtrip hoge bytes herschrijven, de BOM beschadigen en zelfs het scheidingsteken erna storen. De veilige aanpak houdt de lexer op bytes: sla PDF-witruimte op positie over (ISO 32000-1 §7.2.2 somt NUL, tab, LF, FF, CR en spatie op) en snij met Copy, en decodeer pas zodra een waarde bekend is als PDF-tekststring
// Riskant op rauwe PDF-bytes: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Byte-safe: sla PDF-witruimte op positie over en snij daarna
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
De tweede valkuil zit in de incrementele update zelf. ISO 32000-1 §7.5.5 (Table 15) en §14.4 eisen dat de trailer-/ID een array van twee bytestrings is. De interne helper die de sparse update-identifier samenstelt, geeft alleen de inhoud van de twee strings terug, zonder de /ID-key of de array-haakjes, en de trailer moet hem expliciet wikkelen als /ID [ ... ]. De toegeeflijke loader van PDFium en een latere volledige heropslag verhullen beide een ontbrekende wikkel, dus de betrouwbare controle is de kandidaat- en opgeslagen bestanden te parsen met een onafhankelijke strenge parser en naar twee strings van 16 bytes te zoeken. Schrijf je zelf incrementele updates, dan geldt dezelfde valkuil; het artikel over incrementele updates voor versleutelde PDFs gaat dieper in op de trailerketen
Wat moet je doen in je eigen Delphi-formuliercode?
- Lees de waarde na een schrijfactie altijd terug met
FormField[Index], en sla in tests ook op, heropen en lees opnieuw - Beschouw
FalseplusErrorMessageals informatie: een weigering dieNMnoemt, een ouderconflict of een dubbele naam betekent dat het eigendom van het bestand echt dubbelzinnig is - Genereer je PDFs, emit velden en widgets dan als indirecte objecten en geef elke widget een unieke
/NM; dat haalt de dubbelzinnigheid bij de bron weg - Gebruik
SetFormFieldValuesvoor multi-veld- of cross-page-vullingen zodat een late fout niet de helft van het formulier bijgewerkt kan achterlaten - Draai
Trimof anderestring-RTL-helpers nooit over rauwe PDF-bytes in eenAnsiString
Wees ook helder over de grenzen. Ondertekende documenten, XFA-formulieren, versleutelde snapshots en bestanden waarvan het widgeteigendom niet ondubbelzinnig is op te lossen, blijven buiten het opgegeven profiel en worden geweigerd in plaats van met dwang erdoorheen geduwd. Verplaatst je workflow velden tussen documenten, dan worden de eigendomsvragen nog lastiger; AcroForm-velden over PDF-documenten heen enten behandelt dat geval
De PDFium Component brengt dit getypeerde, transactionele formulier schrijven naar Delphi-, C++Builder- en Lazarus-applicaties, naast rendering, tekstextractie en documentbewerking. Versiegeschiedenis, licenties en downloads staan op de productpagina van de PDFium Component for Delphi