PDFium Component sichert editierte XFA-Formularwerte exakt, über Sichern und Wiederöffnen hinweg, wenn es die Windows-V8-Runtime pdfium.v8.dll ab v3.125.2 fährt. Ältere Runtimes fügten Feldwerten Linefeeds hinzu, kappten Emoji auf ein unverwandtes BMP-Zeichen, übersprungen stillschweigend Single-Stream-XFA-Saves und konnten einen fehlgeschlagenen letzten Schreibvorgang verschlucken. Ein Wiederöffnen-Symptom ist gar kein Bibliotheksdefekt: Ein dynamisches Formular, dessen Root-Subform restoreState="auto" fehlt, baut sein Layout aus der Vorlage neu auf
Die Bug-Reports dazu sahen alle gleich aus. Ein Kunde füllt ein XFA-Formular in einem Delphi-Viewer aus, sichert, öffnet wieder, und irgendwas sitzt leicht schief. Eine leere Kommentare-Box hält jetzt eine Leerzeile, nach einem zweiten Sichern zwei. Ein Name mit einem Emoji kommt mit einem Private-Use-Glyph zurück. Niemand kriegt einen Fehler, und genau das macht diese Bugs teuer: Das Driften taucht Wochen später im Export von jemand anderem auf
Was geht schief, wenn ein XFA-Formular gesichert und wieder geöffnet wird?
Vier separate Defekte im nativen XFA-Save-Pfad verursachten das Wertedriften, und jeder versteckte sich hinter einem erfolgreich aussehenden Sichern. Zwei kamen aus der Serialisierung, einer aus dem Single-Stream-Speicherlayout, und einer aus dem PDF-Writer selbst. Die Tabelle mappt jedes Symptom auf seine Ursache und auf das Release, in dem PDFium Component es gefixt hat
| Symptom nach dem Wiederöffnen | Ursache | Gefixt in |
|---|---|---|
| Ein leeres Feld hält einen Linefeed; Werte wachsen pro Sichern um einen Zeilenumbruch | Beide XFA-Writer fügten Layout-Zeilenumbrüche nach Start-Tags ein | v3.125.2, pdfium.v8.dll |
| U+1F642 kommt als U+F642 zurück, oder das Emoji verschwindet aus dem Form-Packet | 16-Bit-wchar_t-Abschneiden beim Dekodieren; Surrogate-Filterung im Form-Serializer | v3.125.2, pdfium.v8.dll |
| Edits in einem Single-Stream-XFA-Dokument sind schlicht weg | Das native Sichern wies das Stream-Layout zurück, aber der Rückgabewert wurde ignoriert | v3.125.2; Kommentare und Processing Instructions bleiben seit v3.126.0 erhalten |
| Abgeschnittene Datei, obwohl das Sichern Erfolg meldete | Der letzte gepufferte Schreibvorgang scheiterte, nachdem der Writer bereits Erfolg gemeldet hatte | v3.125.2 V8-Runtime; v3.125.3 gewöhnliche pdfium.dll |
| Ein dreiseitiges dynamisches Formular öffnet als zweiseitiges wieder | Die Root-Subform verlangt kein restoreState="auto" | Formular-Authoring, kein Bibliotheksdefekt |
Frühere Abhandlungen schlossen, XFA-Feldedits ließen sich mit PDFium überhaupt nicht persistieren, was für die Runtimes der damaligen Zeit stimmte. Die neuere V8-Runtime sichert XFA-Werte nativ, ein im Live-Formular gemachter Edit erreicht also das gespeicherte Datasets-Packet, ohne dass Sie selbst am Packet operieren müssen
Welche PDFium-Runtime sichert XFA-Werte?
Die XFA-Save-Fidelity hängt an der nativen DLL, nicht am Delphi-Wrapper, die erste Prüfung lautet also, welche Runtime Ihr Prozess tatsächlich geladen hat. PDFium Component liefert pro Architektur zwei Windows-Builds: die gewöhnliche pdfium.dll, gebaut ohne V8 und XFA, und pdfium.v8.dll, die die JavaScript-Engine und die XFA-Formular-Runtime trägt. Nur pdfium.v8.dll kann ein XFA-Formular fahren, jeder hier beschriebene XFA-Fix wohnt also dort, beginnend mit den neu gebauten Win32- und Win64-V8-Bibliotheken in v3.125.2
Der Final-Write-Fix ist generischer PDF-Writer-Code, er zählt also auch für gewöhnliche Dokumente. v3.125.3 baute die gewöhnlichen pdfium.dll-Bibliotheken neu, um dieselbe Reparatur zu tragen. Geteilter Quellcode ist kein Beweis für geteiltes Verhalten: Bis die Binärdatei neu gebaut ist, behält die alte DLL den alten Bug
Eine zweite Falle saß im Loader. Vor v3.125.2 brachte das Setzen von EnableV8Engine auf True die Bindung dazu, den Default-Namen pdfium.v8.dll zu nehmen und einen vollständigen Pfad in LibraryName zu ignorieren. Eine Anwendung, die auf eine frisch ausgelieferte Runtime zeigte, konnte also weiter eine ältere Kopie aus einem anderen Ordner laden. Seit v3.125.2 wählt ein LibraryName mit Verzeichnis in beiden Engine-Modi exakt diese Datei, und ein fehlender Pfad schlägt fehl, statt auf eine andere gebündelte Bibliothek zurückzufallen
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Ein Verzeichnis in LibraryName nagelt genau diese Datei fest (v3.125.2 und später);
// fehlt die Datei, wirft das Laden eine Exception, statt zurückzufallen
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // beim Start scheitern, nicht beim ersten Sichern
end;
Nach dem Öffnen eines Dokuments sagt Ihnen TPdf.XFA, dass die Datei XFA enthält, und TPdf.XfaRuntimeAvailable, dass die geladene DLL sie tatsächlich ausführen kann. Müssen Sie außerdem statische und dynamische Formulare auseinanderhalten, liefert TPdf.FormType ftXfaFull oder ftXfaForeground; der Artikel zu dem Erkennen von XFA-Formularen und dem Extrahieren von XFA-Packets in Delphi behandelt dieses Sondieren im Detail
Warum kommen gesicherte XFA-Felder mit zusätzlichen Linefeeds zurück?
Gesicherte XFA-Felder gewannen Linefeeds, weil beide nativen XFA-Writer, der generische XML-Element-Writer und der Form-Packet-Serializer, ihre Ausgabe hübsch druckten, mit einem Zeilenumbruch nach Start-Tags. In den meisten XML-Dateien ist dieser Whitespace kosmetisch. In XFA-Daten ist er es nicht: Wird das Datasets-Packet erneut geparst, ist der Text zwischen <Comments> und </Comments> der Feldwert, Linefeed eingeschlossen. Ein leeres Feld öffnete sich also wieder mit einem einzelnen LF, und jeder weitere Save-and-Reopen-Zyklus konnte einen weiteren ergänzen
Die naheliegende Reparatur, Werte beim Laden zu trimmen, wäre falsch. Nutzer tippen führende Leerzeichen, abschließende Leerzeichen und absichtlichen Mehrzeilen-Text in XFA-Felder, und ein Adressblock oder ein Festbreiten-Code muss Byte für Byte überleben. Der v3.125.2-Fix entfernt deshalb nur den Whitespace, den der Serializer selbst um Tags herum synthetisiert hat. Nutzwerden, bestehende Textknoten und CDATA-Sektionen laufen unangetastet durch, " indented" bleibt also eingerückt, und ein absichtlich leeres Feld bleibt leer
Warum kommt ein Emoji als anderes Zeichen zurück?
Ein Emoji kam falsch zurück, weil Windows-wchar_t 16 Bits breit ist und zwei Dekodierpfade einen vollständigen Unicode-Skalarwert in einem einzelnen wchar_t speicherten. Der UTF-8-Stream-Decoder und der Parser für numerische Zeichenreferenzen wie 🙂 taten das beide. U+1F642, das leicht lächelnde Gesicht, passt nicht in 16 Bits, die hohen Bits fielen also ab, und U+F642 erschien stattdessen: ein Codepunkt in der Private Use Area, den die meisten Fonts als Kasten oder gar nichts rendern
Der Form-Serializer hatte das umgekehrte Problem. Er filterte Zeichen je ein wchar_t auf einmal, sah zwei Surrogate-Code-Einheiten, die für sich genommen ungültig sind, und warf beide weg, das Emoji verschwand also komplett aus dem Form-Packet. In v3.125.2 konsumiert der Decoder jeden Skalarwert vollständig und emittiert ein ordentliches Surrogate-Paar. Bleibt nur noch ein Ausgabe-Slot, hält er die Low-Surrogate zurück und meldet kein Stream-Ende, solange diese Einheit noch gepuffert ist. Eine UTF-8-Sequenz, die über Leseblöcke hinweg gespalten ist, wird in den nächsten Lesevorgang übertragen statt weggeworfen. Der Form-Exporter hält gültige Surrogate-Paare jetzt zusammen, und numerische Zeichenreferenzen produzieren ebenfalls korrekte Paare
Latein-1-Testdaten zeigen davon nie etwas, jeder XFA-Roundtrip-Test braucht also mindestens ein Supplemental-Plane-Zeichen
Single-Stream-XFA und Save-Fehler, die niemand sah
Ein Single-Stream-XFA-Dokument verlor seine Edits, weil der native Save-Helfer dieses Speicherlayout zurückwies und sein Aufrufer den Fehlschlag ignorierte. ISO 32000-1 §12.7.8 erlaubt dem /XFA-Eintrag des interaktiven Formular-Dictionarys entweder ein Array aus Packet-Namen und Streams oder einen einzelnen Stream, der das ganze XDP-Dokument hält. Packet-Arrays sind der gewöhnliche Fall, aber Single Streams sind völlig legal, und das PDF-Sichern schloss ab, als wäre nichts gewesen, während die Formulardaten auf ihren alten Werten blieben
Seit v3.125.2 handhabt die V8-Runtime die unterstützte Single-Stream-Teilmenge. Sie exportiert zuerst beide Live-Packets, datasets und form, in einen Staging-Bereich und validiert sie, und erst dann ersetzt sie die passenden Packets im ursprünglichen XDP. Andere Packets und die Root-Namespace-Deklarationen bleiben erhalten. Scheitert das Staging, wird der persistente XFA-Stream nie angefasst, und das Dokument behält seine Änderungsmarkierung
XML-Kommentare und Processing Instructions brauchten Extra-Pflege, weil das interne XML-DOM sie fallen lässt. In v3.125.2 ließ ihre Anwesenheit das Sichern rundherum scheitern, statt stillschweigend Inhalt zu verlieren. v3.126.0 bewahrt sie: Vor dem Parsen wird jeder Kommentar oder jede Processing Instruction gegen einen Marker getauscht, gebaut aus einem Präfix, das nirgendwo im Originaltext vorkommt. Nachdem die Live-Packets ersetzt sind, muss jeder Marker exakt einmal auftauchen, bevor das Original-Token wiederhergestellt und der Stream geschrieben wird. Tokens außerhalb der ersetzten Packets behalten also ihren Text und ihre Reihenfolge, Tokens im Prolog, im Template und in anderen Packets eingeschlossen
Manche Eingaben werden weiterhin mit Absicht abgewiesen, und jede Ablehnung ist ein expliziter Save-Fehler:
- Kommentare oder Processing Instructions innerhalb der Live-Packets
datasetsoderform, denn ihre ursprünglichen Positionen lassen sich nicht in frisch exportierten Inhalt übertragen - DTD-Deklarationen und XMLDSig-Signaturen, denn das Umschreiben des XDP kann eine XML-Signatur nicht gültig halten
- Ungültiges UTF-8 oder UTF-16, unvollständige Tags, ungültige Zeichenreferenzen, unbekannte Entities und malformed Processing Instructions, die abgewiesen statt stillschweigend repariert werden
Die Single-Stream-Ausgabe ist UTF-8 und bewahrt das XML-Inhaltsmodell, nicht das ursprüngliche Byte-Layout oder die Encoding-Deklaration
Der letzte Defekt saß unterhalb von XFA. Der native Datei-Writer puffert die Ausgabe in 32-KB-Blöcken und flushte den letzten Teilblock erst in seinem Destruktor, nachdem der Dokument-Writer bereits Erfolg gemeldet hatte. Ein Disk-full- oder I/O-Fehler auf diesem letzten Block blieb für den Aufrufer unsichtbar. Seit v3.125.2 in der V8-Runtime und v3.125.3 in der gewöhnlichen Runtime gehört dieser finale Flush zum Save-Ergebnis, und die XFA-Änderungsmarkierung wird erst nach einem echten Erfolg geräumt. Auf Delphi-Seite schreibt TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean in eine temporäre Datei neben dem Ziel und verschiebt sie nur dann an ihren Platz, wenn das Sichern True liefert, ein gescheitertes Sichern lässt also die vorherige Datei intakt
Warum öffnet ein dynamisches XFA-Formular mit weniger Seiten wieder?
Ein dynamisches XFA-Formular öffnet mit weniger Seiten wieder, wenn seine Root-Subform kein restoreState="auto" deklariert, und das ist eine Formular-Authoring-Entscheidung, kein PDFium-Component-Defekt. In XFA 3.3 defaultet restoreState auf der Root-Subform zu manual. Unter manual stellt der XFA-Prozessor nur begrenzten Zustand aus dem gespeicherten Form-Packet wieder her und überlässt den Rest den Skripten des Autors. Gespeicherte Feldwerte und Instanzzahlen wiederholter Subforms kommen weiterhin zurück, aber geometrische Properties, die zur Laufzeit gesetzt wurden, nicht
Der Fall, der das enthüllte, war ein dreiseitiges Formular, dessen Skript eine Subform auf h="450pt" aufblähte. Das gespeicherte Form-Packet hielt die neue Höhe, die Werte und die Instanzzahlen. Beim Wiederöffnen aber wurde das Layout aus den Vorlagen-Höhen neu gebaut, und das Formular floss auf zwei Seiten um. Die Runtime hatte recht: Die Vorlage hatte nie um automatische Wiederherstellung gebeten. Sie auf der Root-Subform zu deklarieren fixt das Wiederöffnen:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- Felder; Skripte dürfen h ändern oder Instanzen zur Laufzeit ergänzen -->
</subform>
</subform>
</template>
Gehört Ihnen die Vorlage nicht, patchen Sie nicht im Viewer drumherum: Ein Formular, das auf den manual-Modus baut, erwartet, dass seine eigenen Skripte den Zustand neu aufbauen. Live-Repagination während der Nutzer tippt ist ein eigenes Thema, behandelt in wie PDFium Component dynamische XFA-Seitenzahlen und umgezogene Felder verfolgt
Wie verifiziert man ein XFA-Sichern in Delphi?
Die einzige verlässliche XFA-Save-Prüfung ist, die gesicherte Datei in einer frischen TPdf-Instanz wieder zu öffnen und die gespeicherten Daten zurückzulesen. TPdf.GetXfaDatasets liefert das Datasets-Packet so, wie es im Dokument gespeichert ist, nicht das Live-XFA-Datenmodell, ein Aufruf vor dem Sichern zeigt also die alten Werte. Nach dem Wiederöffnen zeigt es exakt das Geschriebene. Ein Single-Stream-Dokument hat keine separat benannten Packets: PDFium meldet das ganze XDP als ein Packet mit leerem Namen, GetXfaPacketByName('datasets') und GetXfaDatasets liefern also nichts, und der Fallback liest den kompletten Stream über GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // Packet-Array-Layout
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // Single Stream: ein unbenanntes Packet
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // Die gespeicherte XDP-Ausgabe ist UTF-8
finally
Pdf.Free;
end;
end;
Die Save-Routine committet dann den ausstehenden Edit, prüft das SaveAs-Ergebnis und vergleicht den wieder geöffneten Wert. TPdf.ClearFormFieldFocus killt den Formularfokus, das ist der Moment, in dem PDFium den Edit-Buffer des fokussierten Felds committet. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean füllt das fokussierte Feld programmatisch, beruht aber auf einem Fokus, den der Wrapper über FocusFormField verfolgt, das Widget-Annotationen abläuft. Eine dynamische XFA-Seite hat normalerweise keine, der Text kommt dort meist über Tastatureingabe in TPdfView, und die Funktion liefert False, wenn kein verfolgtes Feld den Fokus hat
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Optionales Skript-Fill; False heißt, kein verfolgtes Feld hat Fokus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // den Edit-Buffer committen
if not Pdf.SaveAs(FileName) then // einschließlich des finalen Flushs (v3.125.2+)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
Behandeln Sie den Teilstring-Test als Smoke-Test. Ein leeres Element kann als <Tag/> serialisiert werden, Attribute können an Datenelementen auftauchen, und Escaping über & und < hinaus ist eine Serializer-Entscheidung. Für Produktions-Prüfungen laden Sie das wieder geöffnete XML mit einem echten XML-Parser und vergleichen den Textknoten des gebundenen Datenelements. Fahren Sie die Prüfung außerdem zweimal hintereinander, denn der Linefeed-Defekt zeigte seine volle Gestalt erst in der zweiten Generation
Kurzreferenz: XFA-Save-Fidelity-Checkliste
pdfium.v8.dllab v3.125.2 für XFA-Formulare ausliefern, und ab v3.125.3 für die gewöhnlichepdfium.dll, damit der Final-Write-Fix in beiden stecktLibraryNameauf einen vollständigen Pfad zeigen lassen undEnableV8Engineauf True setzen; ein fehlender Pfad schlägt fehl, statt eine andere Kopie zu ladenTPdf.XFAundTPdf.XfaRuntimeAvailablenach dem Öffnen des Dokuments bestätigenClearFormFieldFocusvorSaveAsaufrufen, damit das fokussierte Feld committet wird- Das Boolean-Ergebnis von
SaveAsnie ignorieren; ein False-Ergebnis lässt die vorherige Datei an ihrem Platz - Verifizieren, indem Sie in einem neuen
TPdfwieder öffnen undGetXfaDatasetslesen, mit Fallback aufGetXfaFormPacketsfür Single-Stream-XFA - Testen mit leeren Werten, führenden Leerzeichen, Mehrzeilen-Text,
&und einem Supplemental-Plane-Zeichen, über zwei Save-Generationen hinweg - Mit expliziten Save-Fehlern rechnen bei DTDs, XMLDSig und Kommentaren innerhalb der Live-Packets von Single-Stream-XFA
- Verliert ein dynamisches Formular Laufzeit-Geometrie beim Wiederöffnen, die Root-Subform auf
restoreState="auto"prüfen, bevor man der Bibliothek misstraut
Für die Callback-Struktur, die die XFA-Runtime von einer Host-Anwendung erwartet, siehe FPDF_FORMFILLINFO Version 2 und das XFA-ABI in Delphi. Die V8-Runtime, der Delphi- und C++Builder-Wrapper und das Viewer-Control sind allesamt Teil von PDFium Component for Delphi and C++Builder, das beide Windows-Runtimes für Win32 und Win64 einschließt