PDFium Component slaat bewerkte XFA-formulierwaarden exact op, over opslaan en heropenen heen, wanneer hij de Windows V8-runtime pdfium.v8.dll draait die met v3.125.2 of later wordt meegeleverd. Oudere runtimes voegden line feeds toe aan veldwaarden, kaptten emoji af tot een ongerelateerd BMP-teken, sloegen single-stream XFA-saves stilletjes over en konden een mislukte slotwrite doorslikken. Eén heropen-symptoom is helemaal geen librarydefect: een dynamisch formulier wiens root-subform geen restoreState="auto" heeft, bouwt zijn layout weer op vanuit de template
De bugmeldingen hierover zagen er allemaal hetzelfde uit. Een klant vult een XFA-claimformulier in een Delphi-viewer, slaat op, heropent, en er klopt iets net niet. Een leeg opmerkingenvak bevat nu een blanco regel, en na een tweede opslag bevat hij er twee. Een naam getypt met een emoji komt terug met een private-use-glyph. Niemand krijgt een fout, en dat maakt deze bugs duur: de drift komt weken later boven water in de export van iemand anders
Wat gaat er mis wanneer een XFA-formulier wordt opgeslagen en heropend?
Vier aparte defecten in het native XFA-savepad veroorzaakten waardedrift, en elk verstopte zich achter een succesvol ogende opslag. Twee kwamen uit serialisatie, één uit de single-stream-opslaglayout en één uit de PDF-writer zelf. De tabel mapt elk symptoom op zijn oorzaak en op de release waarin PDFium Component hem heeft gefixt
| Symptoom na heropenen | Oorzaak | Gefixt in |
|---|---|---|
| Leeg veld bevat een line feed; waarden groeien per opslag een newline | Beide XFA-writers plaatsten layout-newlines na start-tags | v3.125.2, pdfium.v8.dll |
| U+1F642 komt terug als U+F642, of de emoji verdwijnt uit het form-packet | 16-bit wchar_t-afkapping bij decodering; surrogate-filtering in de form-serializer | v3.125.2, pdfium.v8.dll |
| Bewerkingen in een single-stream XFA-document zijn simpelweg weg | De native save wees de streamlayout af, maar de retourwaarde werd genegeerd | v3.125.2; comments en processing instructions behouden sinds v3.126.0 |
| Afgekapt bestand hoewel de opslag succes meldde | De laatste gebufferde write faalde nadat de writer al succes had teruggegeven | v3.125.2 V8-runtime; v3.125.3 gewone pdfium.dll |
| Dynamisch formulier van drie pagina's heropent als twee pagina's | Root-subform vraagt geen restoreState="auto" | Formulierontwerp, geen librarydefect |
Eerdere stukken concludeerden dat XFA-veldbewerkingen met PDFium helemaal niet konden worden bewaard, wat voor de runtimes van destijds klopte. De nieuwere V8-runtime slaat XFA-waarden native op, dus een bewerking in het live formulier komt het opgeslagen datasets-packet binnen zonder packetchirurgie aan uw kant
Welke PDFium-runtime slaat XFA-waarden op?
XFA-save-fidelity hangt af van de native DLL, niet van de Delphi-wrapper, dus de eerste controle is welke runtime uw proces werkelijk heeft geladen. PDFium Component levert per architectuur twee Windows-builds: de gewone pdfium.dll, gebouwd zonder V8 en XFA, en pdfium.v8.dll, die de JavaScript-engine en de XFA-formulieruntime meedraagt. Alleen pdfium.v8.dll kan een XFA-formulier draaien, dus elke hier beschreven XFA-fix woont daar, te beginnen met de herbouwde Win32- en Win64-V8-bibliotheken in v3.125.2
De slot-write-fix is generieke PDF-writercode, dus hij doet er ook voor gewone documenten toe. v3.125.3 bouwde de gewone pdfium.dll-bibliotheken opnieuw om diezelfde reparatie te dragen. Gedeelde source is geen bewijs van gedeeld gedrag: tot de binary opnieuw is gebouwd houdt de oude DLL de oude bug vast
Een tweede val zat in de loader. Vóór v3.125.2 liet het zetten van EnableV8Engine op True de binding de defaultnaam pdfium.v8.dll kiezen en een volledig pad in LibraryName negeren. Een applicatie die naar een vers gedeployde runtime wees kon zo een oudere kopie uit een andere map blijven laden. Sinds v3.125.2 selecteert een LibraryName die een directory bevat exact dat bestand in beide engine-modi, en een ontbrekend pad faalt in plaats van terug te vallen op een andere meegeleverde bibliotheek
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Een directory in LibraryName legt precies dit bestand vast (v3.125.2 en later);
// ontbreekt het bestand, dan werpt het laden in plaats van terug te vallen
{$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; // faal bij de opstart, niet bij de eerste opslag
end;
Na het openen van een document vertelt TPdf.XFA u dat het bestand XFA bevat, en TPdf.XfaRuntimeAvailable dat de geladen DLL hem ook werkelijk kan uitvoeren. Moet u ook statische en dynamische formulieren uit elkaar houden, dan geeft TPdf.FormType ftXfaFull of ftXfaForeground terug; het artikel XFA-formulieren detecteren en XFA-packets extraheren in Delphi behandelt dat aftasten in detail
Waarom krijgen opgeslagen XFA-velden extra line feeds?
Opgeslagen XFA-velden kregen line feeds omdat beide native XFA-writers, de generieke XML-element-writer en de form-packet-serializer, hun output pretty-printten met een newline na start-tags. In de meeste XML is die whitespace cosmetisch. In XFA-data is dat niet zo: wanneer het datasets-packet opnieuw wordt geparsed is de tekst tussen <Comments> en </Comments> de veldwaarde, newline inbegrepen. Een leeg veld heropende daardoor met één enkele LF, en elke verdere save-en-heropen-cyclus kon er nog een toevoegen
De voor de hand liggende reparatie, waarden bij het laden trimmen, zou verkeerd zijn. Gebruikers typen voorloopspaties, volgspaties en bewuste meerregelige tekst in XFA-velden, en een adresblok of een code met vaste breedte moet byte voor byte overleven. De fix in v3.125.2 verwijdert daarom alléén de whitespace die de serializer zelf rond tags heeft gesynthetiseerd. Gebruikerswaarden, bestaande tekstnodes en CDATA-secties gaan onaangeroerd door, dus " indented" blijft ingesprongen en een met opzet leeg veld blijft leeg
Waarom komt een emoji terug als een ander teken?
Een emoji kwam verkeerd terug omdat Windows wchar_t 16 bits breed is, en twee decodeerpaden een volledige Unicode-scalarwaarde in één enkele wchar_t bewaarden. De UTF-8-streamdecoder en de parser voor numerieke character references zoals 🙂 deden dat beide. U+1F642, het licht glimlachende gezicht, past niet in 16 bits, dus vielen de hoge bits weg en verscheen U+F642 in plaats daarvan: een code point in de Private Use Area dat de meeste fonts als een kadertje of niets renderen
De form-serializer had het tegenovergestelde probleem. Ze filterde tekens één wchar_t per keer, zag twee surrogaat-code units die op zichzelf ongeldig zijn, en liet beide vallen, dus verdween de emoji volledig uit het form-packet. In v3.125.2 consumeert de decoder elke scalarwaarde volledig en emitteert hij een net surrogaatpaar. Wanneer er nog maar één outputslot over is, houdt hij de lage surrogaat in de wacht en meldt hij geen end-of-stream zolang die unit nog gebufferd is. Een UTF-8-reeks die over leesblokken heen is gesplitst wordt meegenomen naar de volgende lees in plaats van te worden weggegooid. De form-exporteur houdt geldige surrogaatparen nu bij elkaar, en numerieke character references leveren eveneens correcte paren op
Latin-1-testdata laten hiervan nooit iets zien, dus elke XFA-round-trip-test heeft minstens één teken uit een supplementair vlak nodig
Single-stream XFA en save-falingen die niemand zag
Een single-stream XFA-document verloor zijn bewerkingen omdat de native save-helper die opslaglayout weigerde en zijn aanroeper de faling negeerde. ISO 32000-1 §12.7.8 staat toe dat de entry /XFA van de interactive form-dictionary ofwel een array van packetnamen en streams is ofwel één enkele stream die het hele XDP-document bevat. Packetarrays zijn het gebruikelijke geval, maar single streams zijn volkomen legaal, en de PDF-opslag klaarde alsof er niets was gebeurd terwijl de formulierdata op zijn oude waarden bleef
Sinds v3.125.2 behandelt de V8-runtime de ondersteunde single-stream-subset. Ze exporteert eerst beide live packets, datasets en form, naar een staging-gebied en valideert ze, en pas daarna vervangt ze de overeenkomende packets in de originele XDP. Andere packets en de root-namespace-declaraties blijven behouden. Faalt de staging, dan wordt de persistente XFA-stream nooit aangeraakt en houdt het document zijn modificatiemarkering
XML-comments en processing instructions vroegen extra zorg omdat de interne XML-DOM ze laat vallen. In v3.125.2 liet hun aanwezigheid de opslag ronduit falen in plaats van stilletjes inhoud te verliezen. v3.126.0 behoudt ze: vóór het parsen wordt elk comment of elke processing instruction verwisseld voor een marker gebouwd vanuit een prefix die nergens in de originele tekst voorkomt. Nadat de live packets zijn vervangen, moet elke marker exact één keer voorkomen voordat het originele token wordt hersteld en de stream wordt weggeschreven. Tokens buiten de vervangen packets houden daardoor hun tekst en volgorde, ook tokens in de prolog, de template en andere packets
Sommige inputs worden nog steeds met opzet geweigerd, en elke weigering is een expliciete save-faling:
- Comments of processing instructions binnen de live packets
datasetsofform, want hun originele posities zijn niet te mappen op vers geëxporteerde inhoud - DTD-declaraties en XMLDSig-handtekeningen, want het herschrijven van de XDP kan een XML-handtekening niet geldig houden
- Ongeldige UTF-8- of UTF-16-codering, onvolledige tags, ongeldige character references, onbekende entities en misvormde processing instructions, die worden geweigerd in plaats van stilletjes gerepareerd
De single-stream-output is UTF-8 en behoudt het XML-inhoudsmodel, niet de originele bytelayout of coderingsdeclaratie
Het laatste defect zat onder XFA. De native file writer buffert output in blokken van 32 KB en spoelde het laatste partiële blok pas door in zijn destructor, nadat de documentwriter al succes had gemeld. Een disk-full- of I/O-fout op dat laatste blok was onzichtbaar voor de aanroeper. Sinds v3.125.2 in de V8-runtime en v3.125.3 in de gewone runtime hoort die laatste flush bij het saveresultaat, en wordt de XFA-modificatiemarkering pas gewist na echt succes. Aan de Delphi-kant schrijft TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean naar een tijdelijk bestand naast het doel en verplaatst het pas op zijn plek wanneer de opslag True teruggeeft, dus laat een mislukte opslag het vorige bestand intact
Waarom heropent een dynamisch XFA-formulier met minder pagina's?
Een dynamisch XFA-formulier heropent met minder pagina's wanneer zijn root-subform geen restoreState="auto" declareert, en dat is een formulierontwerpbeslissing in plaats van een defect van PDFium Component. In XFA 3.3 staat restoreState op het root-subform by default op manual. Onder manual herstelt de XFA-processor slechts beperkte toestand uit het opgeslagen form-packet en laat de rest over aan de scripts van de auteur. Opgeslagen veldwaarden en instance-aantallen van herhalende subforms komen nog wel terug, maar geometrische eigenschappen die tijdens run time zijn gezet niet
Het geval dat dit aan het licht bracht was een formulier van drie pagina's waarvan het script een subform liet groeien naar h="450pt". Het opgeslagen form-packet bevatte de nieuwe hoogte, de waarden en de instance-aantallen. Bij het heropenen werd de layout echter weer opgebouwd vanuit de templatehoogten en vloeide het formulier over twee pagina's. De runtime had gelijk: de template had nooit om automatische restauratie gevraagd. Het op het root-subform declareren fixt het heropenen:
<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">
<!-- velden; scripts kunnen h wijzigen of instances toevoegen tijdens run time -->
</subform>
</subform>
</template>
Is de template niet van u, plak er dan niet in de viewer omheen: een formulier dat op manual-modus vertrouwt verwacht dat zijn eigen scripts de toestand herbouwen. Live herpaginering terwijl de gebruiker typt is een apart onderwerp, behandeld in hoe PDFium Component dynamische XFA-pagina-aantallen en verplaatste velden volgt
Hoe verifieert u een XFA-opslag in Delphi?
De enige betrouwbare XFA-save-controle is het opgeslagen bestand in een verse TPdf-instantie heropenen en de opgeslagen data teruglezen. TPdf.GetXfaDatasets geeft het datasets-packet terug zoals het in het document is opgeslagen, niet het live XFA-datamodel, dus een aanroep vóór het opslaan toont de oude waarden. Na het heropenen toont hij exact wat is weggeschreven. Een single-stream document heeft geen afzonderlijk benoemde packets: PDFium meldt de hele XDP als één packet met een lege naam, dus GetXfaPacketByName('datasets') en GetXfaDatasets geven niets terug, en de fallback leest de volledige stream via 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: één naamloos 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); // opgeslagen XDP-output is UTF-8
finally
Pdf.Free;
end;
end;
De saveroutine commit daarna de wachtende bewerking, controleert het resultaat van SaveAs en vergelijkt de heropende waarde. TPdf.ClearFormFieldFocus doodt de formulierfocus, en dat is het moment waarop PDFium de editbuffer van het gefocuste veld commit. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean vult het gefocuste veld programmatisch, maar hij leunt op een focus die de wrapper volgt via FocusFormField, dat langs widget-annotaties loopt. Een dynamische XFA-pagina heeft die normaal niet, dus daar komt de tekst meestal binnen via toetsenbordinvoer in TPdfView, en geeft de functie False terug wanneer geen gevolgd veld focus heeft
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
// Optionele scripted invulling; False betekent dat geen gevolgd veld focus heeft
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // commit de editbuffer
if not Pdf.SaveAs(FileName) then // omvat de laatste flush (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;
Behandel de substringtest als een rooktest. Een leeg element kan worden geserialiseerd als <Tag/>, attributen kunnen op data-elementen voorkomen, en escapen voorbij & en < is een keuze van de serializer. Voor productiecontroles laadt u de heropende XML met een echte XML-parser en vergelijkt u de tekstnode van het gebonden data-element. Draai de controle ook twee keer achter elkaar, want het newline-defect liet pas in de tweede generatie zijn volle vorm zien
Snelnaslag: de XFA save fidelity-checklist
- Deploy
pdfium.v8.dllvan v3.125.2 of later voor XFA-formulieren, en v3.125.3 of later voor de gewonepdfium.dll, zodat de slot-write-fix in beide zit - Wijs
LibraryNamenaar een volledig pad en zetEnableV8Engineop True; een ontbrekend pad faalt in plaats van een andere kopie te laden - Bevestig
TPdf.XFAenTPdf.XfaRuntimeAvailablena het openen van het document - Roep
ClearFormFieldFocusaan vóórSaveAszodat het gefocuste veld wordt gecommit - Negeer nooit het Boolean-resultaat van
SaveAs; een False-resultaat laat het vorige bestand op zijn plek - Verifieer door te heropenen in een nieuwe
TPdfenGetXfaDatasetste lezen, met terugval opGetXfaFormPacketsvoor single-stream XFA - Test met lege waarden, voorloopspaties, meerregelige tekst,
&en een teken uit een supplementair vlak, over twee opslaggeneraties heen - Verwacht expliciete save-falingen voor DTD's, XMLDSig en comments binnen de live packets van single-stream XFA
- Verliest een dynamisch formulier zijn run-time-geometrie bij het heropenen, controleer dan het root-subform op
restoreState="auto"voordat u de library verdenkt
Voor de callbackstructuur die de XFA-runtime van een hostapplicatie verwacht, zie FPDF_FORMFILLINFO versie 2 en de XFA-ABI in Delphi. De V8-runtime, de Delphi- en C++Builder-wrapper en de viewer-control maken allemaal deel uit van PDFium Component for Delphi and C++Builder, dat beide Windows-runtimes voor Win32 en Win64 omvat