Ein PDF-Formularfeldwert "fällt zurück", wenn das Feld als direktes Dictionary gespeichert ist: PDFium befördert die Kopie im /Annots-Array der Seite zu einem indirekten Objekt, während /Fields eine zweite direkte Kopie behält. PDFium Component for Delphi handhabt typisierte Feldschreibungen, indem es beide Referenzen vor dem Schreiben zu einem indirekten Besitzer vereint
Das Symptom liest sich meist wie ein Spuk. Die Schreibung liefert True, und die allererste Lesung reicht den alten Wert zurück. Sichere, öffne wieder, und der alte Wert ist weiterhin da. Nichts hat geworfen, und das PDF sieht in jedem Viewer, den du besitzt, vollkommen ordentlich aus
Das Symptom: eine erfolgreiche Schreibung, die nicht haften bleibt
Das Fehlermuster ist schmal, aber sehr wiederholbar: Es zeigt sich nur auf Formularen, deren Feld-Dictionarys inline geschrieben sind (als direkte Objekte) statt als nummerierte indirekte Objekte. Die meisten Authoring-Werkzeuge emittieren indirekte Felder, also versteckt sich das Problem monatelang und erscheint dann auf Dateien eines bestimmten Generators, oft ein handgestrickter Exporteur, der das ganze /AcroForm inline schreibt
Zwei Beobachtungen zusammen zeigen auf die Ursache: Die Schreibung meldet Erfolg, und eine native Lesung des Felds durch die Formularschicht liefert den vorherigen Wert. Wenn du beide siehst, hör auf, deine String-Kodierung zu verdächtigen, und schau dir die Objektbesitzverhältnisse an
Warum fällt ein PDF-Formularfeldwert nach erfolgreicher Schreibung zurück?
Ein PDF-Formularfeldwert fällt zurück, weil zwei getrennte Dictionarys beide beanspruchen, dasselbe Feld zu sein, und die Schreibung die Kopie aktualisierte, die der Feldbaum nicht befragt. Der Wert eines Felds lebt in seinem /V-Eintrag, und ISO 32000-1 §12.7.3.1 definiert, wo jener Eintrag maßgeblich ist: auf dem Feld-Dictionary, erreicht von /AcroForm /Fields, hinab durch /Kids zum Terminal-Feld, mit vererbbaren Einträgen aufgelöst aus /Parent
Dieselbe Klausel erlaubt eine Bequemlichkeit, die den ganzen Ärger verursacht. Hat ein Feld exakt eine Widget-Annotation, dürfen Feld-Dictionary und Widget-Annotations-Dictionary zu einem einzigen Dictionary verschmolzen werden. In einer wohlgeformten Datei ist jenes verschmolzene Dictionary ein einziges indirektes Objekt, referenziert von zwei Stellen: von /Fields (oder den /Kids eines Eltern) und vom /Annots-Array der Seite. Zwei Referenzen, ein Objekt, ein /V
Zwei Kopien eines Dictionarys
ISO 32000-1 §7.3.10 ist explizit, dass nur ein indirektes Objekt eine teilbare Identität hat. Ein direktes Dictionary hat keine Objektnummer, also enthält die Datei, wenn ein Generator das verschmolzene Feld-/Widget-Dictionary inline in sowohl /Fields als auch /Annots schreibt, nicht ein zweimal referenziertes Objekt. Sie enthält zwei gleich aussehende Dictionarys, die zufällig dieselben Schlüssel und Werte haben
PDFium toleriert das und macht dabei die Aufspaltung permanent. Wenn es ein Handle zur Annotation braucht, kann PDFium das direkte Widget-Dictionary im /Annots-Array der Seite zu einem indirekten Objekt befördern, doch die äquivalente direkte Kopie bleibt, wo sie war, innerhalb von /AcroForm /Fields (oder innerhalb der /Kids des Eltern). Von diesem Moment an hält das Dokument zwei Besitzer für ein logisches Feld. Ein Writer, der /V auf der Seiten-Annotation editiert, bekommt einen echten Erfolg auf Dictionary-Ebene. Der Feldbaum, den die Formularschicht und jeder spätere Reader als Quelle der Wahrheit behandeln, sieht die Änderung nie
Deshalb überlebt der veraltete Wert ein Sichern und Wiederöffnen: Der neue Wert wurde treu serialisiert in ein Objekt, das die Feldhierarchie nicht besitzt
Wie PDFium Component einen einzigen Feldbesitzer wiederherstellt
PDFium Component normalisiert den direkten Feldbesitz vor jeder typisierten Schreibung: Es befördert direkte /AcroForm- und Feld-Dictionarys zu indirekten Objekten, bindet jedes direkte Widget an seine passende Seiten-Annotation und schreibt /Fields, /Kids, /Parent und /Annots um, sodass jede Referenz auf dasselbe indirekte Objekt zeigt. Erst danach ändert es /V, also teilen sich Feldwert, gewähltes Widget und gesichertes Dokument einen Besitzer
Der harte Teil ist die Identität. Zwei direkte Dictionarys mit identischem Inhalt beweisen nicht, dass sie dasselbe Widget sind; ein Formular kann legitim zwei Widgets enthalten, die gleich aussehen. Der Normalisierer weigert sich deshalb zu raten und verlangt einen expliziten, eindeutigen /NM-Annotationsnamen (ISO 32000-1 §12.5.2) an jedem direkten Widget. Die Regeln, die der Schreibpfad anwendet, sind streng:
- Ein direktes Widget braucht ein nichtleeres
/NMvon höchstens 1024 UTF-16-Einheiten, und exakt eine Seiten-Annotation darf denselben Namen tragen - Die direkte Kopie und die Seiten-Annotation müssen identisch serialisieren, abgesehen von
/Parentund dem PDFium-regenerierten/AP; jede andere Differenz weist die Schreibung ab - Beide Eltern-Links werden getrennt geprüft, also kann ein korrektes
/Parentauf der Seiten-Annotation ein widersprüchliches auf der ursprünglichen direkten Kopie nicht verdecken - Ein fehlendes
/Parentwird nur ergänzt, wenn eindeutiger Feldbaum-Besitz die Antwort unzweideutig macht, und äquivalente direkte Eltern-Kopien werden an ihren beförderten Besitzer zurückgebunden - Doppelte voll qualifizierte Feldnamen, zyklische oder mehrfach besessene Feldbäume und Signaturfelder weisen ab, bevor irgendetwas veröffentlicht wird
- Fremde Annotationen, etwa Links auf derselben Seite, und fremde
/AcroForm-Einträge bleiben unangetastet
Der Normalisierer läuft außerdem unter harten Budgets: 4096 Feldbaum-Besuche, Seiten oder Array-Mitglieder, Verschachtelungstiefe 64, 256 neue indirekte Objekte, 1 MiB je befördertem Dictionary, 4 MiB beförderte Bytes insgesamt, 128 MiB inspizierter Dictionary-Bytes und 4096 UTF-16-Einheiten je vollem Feldnamen bei 262144 über das Formular. Eine feindliche oder kaputte Datei stößt an ein Budget und scheitert mit einer Fehlermeldung, statt aus einem Formularfüllen einen Denial of Service zu machen
Die ganze Operation ist transaktional. TPdf.SetFormFieldValues erfasst einen Snapshot, baut ein inkrementelles Update, das die normalisierten Objekte und die neuen Werte enthält, lädt jenen Kandidaten und ersetzt das lebende Dokument erst, sobald der Kandidat verifiziert. Weist die Normalisierung die Datei ab oder scheitert ein späterer Request in einem Batch, bleiben das Originaldokument, seine Werte und sein logischer Feld-Cursor exakt, wie sie waren
Einen direkten Feldwert in Delphi schreiben und verifizieren
Das sichere Muster ist: schreiben, dann über die native Formularschicht zurücklesen und eine Abweichung als Fehler behandeln, selbst wenn die Schreibung True lieferte. TPdf.SetFormFieldValue adressiert ein Feld per Index auf der aktuellen Seite, nimmt einen typisierten TPdfFormValue entgegen und füllt einen TPdfFormValueWriteReport; TPdf.FormField[Index] liest den Wert über PDFiums Formularschicht zurück, die den Feldbaum auflöst, statt dem Widget zu vertrauen
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue zielt auf die aktuelle Seite
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]);
// Verifiziere gegen den Feldbaum, nicht gegen unsere eigene Variable
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Auf einer Datei, deren Besitzverhältnis sich nicht auflösen lässt, etwa einem direkten Widget ohne /NM, liefert SetFormFieldValue False, Report.UpdatedWidgets ist null und Report.ErrorMessage benennt das Problem, etwa "Direct widgets require an explicit unique NM identity". Das ist das Verhalten, das du willst: eine klare Verweigerung statt einer stillen Halbschreibung. Noch eine Vorbedingung ist gut zu wissen. Die typisierte Schreibung ist reine Annotation und erwirbt keinen nativen Fokus, ist also noch ein nativer Feldeditor fokussiert, weist die ganze Transaktion ab, bevor der Snapshot erfasst wird; rufe Pdf.ClearFormFieldFocus bewusst zuerst auf, statt in einer Schleife zu wiederholen
Für mehrere Felder, besonders seitenübergreifend, nimm die Batch-Form. TPdf.SetFormFieldValues akzeptiert 1 bis 256 Requests, wendet alle auf einen Kandidaten an und veröffentlicht alles oder nichts. TPdfFormValuesWriteReport.FailedRequest sagt dir, welcher Request den Batch gebrochen hat
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 zählt jedes synchronisierte Widget
end;
Zwei Requests, die dasselbe Terminal-Feld adressieren, werden als doppelter Besitzer abgewiesen, statt den letzten Schreiber gewinnen zu lassen, und das Quelldokument bleibt erhalten. Checkbox- und Radiowerte folgen denselben Regeln; die /V-gegen-/AS-Aufteilung für Buttons behandelt Checkbox-Feldwert gegen Widget-Zustand in PDFium
Beweisen, dass der Wert Sichern und Wiederöffnen übersteht
Eine Rücklesung in derselben Session beweist, dass das lebende Dokument kohärent ist. Ein Sichern und Wiederöffnen beweist es für die Datei, und die Prüfung ist billig genug, um in deinen eigenen Integrationstests zu bleiben
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;
Zwei weitere Fallen auf Byte-Ebene: Trim und das Trailer-/ID
Direkte Felder zu normalisieren heißt, rohe Dictionary-Bytes neu zu lesen, was zwei kleinere Fallen aufbringt, relevant für jeden Delphi-Code, der PDF-Syntax anfasst
Die erste ist SysUtils.Trim auf rohen PDF-Bytes in einem AnsiString. Ein Literal-String, etwa eine Choice-Option, kann legitim UTF-16BE-Text mit einer FE FF-Byte-Reihenfolge-Markierung und eingebetteten NUL-Bytes enthalten. Im Unicode-Delphi nimmt SysUtils.Trim einen string, der Aufruf konvertiert also den AnsiString stillschweigend nach UTF-16 über die aktive ANSI-Codepage und konvertiert das Ergebnis zurück. Je nach jener Codepage kann der Round-Trip hohe Bytes umschreiben, die BOM beschädigen und sogar das folgende Trennzeichen stören. Der sichere Weg hält den Lexer auf Bytes: Überspringe PDF-Whitespace nach Position (ISO 32000-1 §7.2.2 listet NUL, Tab, LF, FF, CR und Leerzeichen) und schneide mit Copy, dekodiere erst, wenn ein Wert als PDF-Text-String bekannt ist
// Riskant auf rohen PDF-Bytes: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Byte-sicher: PDF-Whitespace nach Position überspringen, dann schneiden
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
Die zweite Falle sitzt im inkrementellen Update selbst. ISO 32000-1 §7.5.5 (Table 15) und §14.4 verlangen, dass das Trailer-/ID ein Array aus zwei Byte-Strings ist. Der interne Helfer, der die dünn besetzte Update-Kennung komponiert, liefert nur die Inhalte der beiden Strings, ohne den /ID-Schlüssel oder die Array-Klammern, und das Trailer muss ihn explizit als /ID [ ... ] wickeln. PDFiums kulanter Loader und ein späteres volles Wiedersichern verstecken beide einen fehlenden Wrapper, die verlässliche Prüfung ist also, die Kandidaten- und gesicherten Dateien mit einem unabhängigen strengen Parser zu parsen und nach zwei 16-Byte-Strings zu suchen. Schreibst du inkrementelle Updates selbst, gilt dieselbe Falle; der Artikel zu inkrementellen Updates für verschlüsselte PDFs behandelt die Trailer-Kette tiefer
Was solltest du in deinem eigenen Delphi-Formular-Code tun?
- Lies den Wert nach einer Schreibung immer mit
FormField[Index]zurück, und in Tests auch sichern, wiederöffnen und erneut lesen - Behandle
FalseplusErrorMessageals Information: Eine Verweigerung, dieNMnennt, einen Elternkonflikt oder einen doppelten Namen, heißt, die Besitzlage der Datei ist echt mehrdeutig - Erzeugst du PDFs, gib Felder und Widgets als indirekte Objekte aus und gib jedem Widget ein eindeutiges
/NM; das beseitigt die Mehrdeutigkeit an der Quelle - Nimm
SetFormFieldValuesfür Mehrfeld- oder seitenübergreifende Füllungen, damit ein später Fehler nicht die halbe Form aktualisiert zurücklassen kann - Lass
Trimoder anderestring-RTL-Helfer nie über rohe PDF-Bytes in einemAnsiStringlaufen
Sei dir auch über die Grenzen im Klaren. Signierte Dokumente, XFA-Formulare, verschlüsselte Snapshots und Dateien, deren Widget-Besitz sich nicht eindeutig auflösen lässt, bleiben außerhalb des deklarierten Profils und weisen ab, statt durchgezwungen zu werden. Verlagert dein Workflow Felder zwischen Dokumenten, werden die Besitzfragen noch härter; AcroForm-Felder über PDF-Dokumente hinweg verpflanzen behandelt jenen Fall
Das PDFium Component bringt diese typisierte, transaktionale Formularschreibung in Delphi-, C++Builder- und Lazarus-Anwendungen, neben Rendering, Textextraktion und Dokumentediting. Versionshistorie, Lizenzierung und Downloads liegen auf der Produktseite des PDFium Component for Delphi