PDFium Component gemmer redigerede XFA-formværdier eksakt, gennem gemning og genåbning, når den kører Windows V8-runtime pdfium.v8.dll, der skibes i v3.125.2 eller senere. Ældre runtimes tilføjede linjeskift til feltværdier, skar emoji ned til et urelateret BMP-tegn, sprang enkelt-stream-XFA-gemninger stille over og kunne slugte et fejlet sidste skriv. Ét genåbningssymptom er slet ikke en biblioteksdefekt: en dynamisk form, hvis rodsubform mangler restoreState="auto", genopbygger sit layout fra skabelonen
Bug-rapporterne for dette lignede hinanden alle. En kunde udfylder en XFA-claim-form i en Delphi-viewer, gemmer, genåbner, og noget er en anelse skævt. En tom kommentarboks indeholder nu en tom linje, og efter en anden gemning to. Et navn tastet med en emoji kommer tilbage med et private-use-glyf. Ingen får en fejl, og det er det, der gør disse bugs dyre: driften viser sig uger senere i en andens eksport
Hvad går galt, når en XFA-form gemmes og genåbnes?
Fire separate defekter i den native XFA-gemmevej forårsagede værdidrift, og hver eneste af dem gemte sig bag en succeslignende gemning. To kom fra serialisering, én fra single-stream-lagringslayoutet og én fra selve PDF-skriveren. Tabellen mapper hvert symptom til sin årsag og til udgivelsen, hvor PDFium Component rettede det
| Symptom efter genåbning | Årsag | Fixet i |
|---|---|---|
| Tomt felt indeholder et linjeskift; værdier vokser ét linjeskift pr. gemning | Begge XFA-skrivere indsatte layout-linjeskift efter start-tags | v3.125.2, pdfium.v8.dll |
| U+1F642 kommer tilbage som U+F642, eller emoji'en forsvinder fra form-packet'en | 16-bit wchar_t-afkortning ved dekodning; surrogate-filtrering i form-serializeren | v3.125.2, pdfium.v8.dll |
| Redigeringer i et single-stream XFA-dokument er simpelthen væk | Nativ gemning afviste stream-layoutet, men returværdien blev ignoreret | v3.125.2; kommentarer og processing instructions bevaret siden v3.126.0 |
| Afkortet fil, selv om gemningen rapporterede succes | Det sidste bufferede skriv fejlede, efter skriveren allerede havde returneret succes | v3.125.2 V8-runtime; v3.125.3 almindelig pdfium.dll |
| Tresidet dynamisk form genåbner som to sider | Rodsubformen anmoder ikke om restoreState="auto" | Form-forfatterskab, ikke en biblioteksdefekt |
Tidligere gennemgange konkluderede, at XFA-feltredigeringer slet ikke kunne fastholdes med PDFium, hvilket var korrekt for datidens runtimes. Den nyere V8-runtime gemmer XFA-værdier nativt, så en redigering lavet i den levende form når det gemte datasets-packet uden packet-kirurgi på din side
Hvilken PDFium-runtime gemmer XFA-værdier?
XFA-gemmetrofasthed afhænger af den native DLL, ikke af Delphi-wrapperen, så det første tjek er, hvilken runtime din proces faktisk har indlæst. PDFium Component skiber to Windows-builds pr. arkitektur: den almindelige pdfium.dll, bygget uden V8 og XFA, og pdfium.v8.dll, som bærer JavaScript-motoren og XFA-form-runtime'en. Kun pdfium.v8.dll kan køre en XFA-form, så ethvert XFA-fix beskrevet her bor dér, med start i de genopbyggede Win32- og Win64 V8-biblioteker i v3.125.2
Det sidste-skriv-fix er generisk PDF-skriver-kode, så det tæller også for almindelige dokumenter. v3.125.3 genopbyggede de almindelige pdfium.dll-biblioteker til at bære samme reparation. Delt kildekode er intet bevis for delt opførsel: indtil binærfilen er genopbygget, beholder den gamle DLL den gamle bug
En anden fælde sad i loaderen. Før v3.125.2 fik det at sætte EnableV8Engine til True bindingen til at vælge default-navnet pdfium.v8.dll og ignorere en fuld sti i LibraryName. En applikation, der pegede på en nyudrullet runtime, kunne blive ved at indlæse en ældre kopi fra en anden mappe. Siden v3.125.2 vælger et LibraryName, der indeholder en mappe, præcis den fil i begge motor-tilstande, og en manglende sti fejler i stedet for at falde tilbage til et andet medfølgende bibliotek
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// En mappe i LibraryName fæstner netop denne fil (v3.125.2 og senere);
// mangler filen, udløser indlæsningen i stedet for at falde tilbage
{$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; // fejle ved startup, ikke ved den første gemning
end;
Efter åbning af et dokument fortæller TPdf.XFA dig, at filen indeholder XFA, og TPdf.XfaRuntimeAvailable fortæller dig, at den indlæste DLL faktisk kan eksekvere den. Behøver du også at skelne statiske og dynamiske former, returnerer TPdf.FormType ftXfaFull eller ftXfaForeground; artiklen om detektering af XFA-former og udtrækning af XFA-pakker i Delphi dækker den probing i detaljer
Hvorfor får gemte XFA-felter ekstra linjeskift?
Gemte XFA-felter fik linjeskift, fordi begge native XFA-skrivere, den generiske XML-element-skriver og form-packet-serializeren, pretty-printede deres output med et linjeskift efter start-tags. I det meste XML er det whitespace kosmetik. I XFA-data er det ikke: når datasets-packet'en parses igen, er teksten mellem <Comments> og </Comments> feltværdien, linjeskift inkluderet. Et tomt felt genåbnede derfor med et enkelt LF, og hver yderligere gem-og-genåbn-cyklus kunne tilføje endnu et
Den åbenlyse reparation, at trimme værdier ved indlæsning, ville være forkert. Brugere taster foranstillede mellemrum, efterstillede mellemrum og bevidst flerlinjetekst i XFA-felter, og en adresseblok eller en fast-bredde-kode skal overleve byte for byte. Fixet i v3.125.2 fjerner derfor kun det whitespace, serializeren selv syntetiserede omkring tags. Brugerværdier, eksisterende tekstknuder og CDATA-sektioner passeres uændret, så " indented" forbliver indrykket, og et bevidst tomt felt forbliver tomt
Hvorfor kommer en emoji tilbage som et andet tegn?
En emoji kom tilbage forkert, fordi Windows wchar_t er 16 bits bred, og to dekodeveje gemte en fuld Unicode-skalarværdi i en enkelt wchar_t. Både UTF-8-stream-dekoderen og parseren for numeriske tegnreferencer som 🙂 gjorde det. U+1F642, det let smilende ansigt, kan ikke være i 16 bits, så de høje bits faldt af, og U+F642 optrådte i stedet: et kodepunkt i Private Use Area, som de fleste fonts renderer som en boks eller ingenting
Form-serializeren havde det modsatte problem. Den filtrerede tegn én wchar_t ad gangen, så to surrogate-kodeenheder, der er ugyldige i isolation, og droppede begge, så emoji'en forsvandt fra form-packet'en helt. I v3.125.2 forbruger dekoderen hver skalarværdi komplet og udsender et ordentligt surrogate-par. Er der kun én outputplads tilbage, holder den den lave surrogate afventende og rapporterer ikke slut-på-stream, mens den enhed stadig er bufferet. En UTF-8-sekvens delt over read-blokke føres videre til næste læsning i stedet for at blive smidt væk. Form-eksportøren holder nu gyldige surrogate-par sammen, og numeriske tegnreferencer producerer også korrekte par
Latin-1-testdata viser intet af dette, så ethvert XFA-round-trip-test behøver mindst ét supplementary-plane-tegn
Single-stream XFA og gemmefejl ingen så
Et single-stream XFA-dokument mistede sine redigeringer, fordi det native gemme-hjælpefunktion afviste det lagringslayout, og dets kalder ignorerede fejlen. ISO 32000-1 §12.7.8 tillader, at /XFA-posten i den interaktive form-dictionary enten er et array af packet-navne og streams eller en enkelt stream, der holder hele XDP-dokumentet. Packet-arrays er det almindelige tilfælde, men enkelt-streams er helt lovlige, og PDF-gemningen fuldførte, som om intet var hændt, mens formdataene blev ved deres gamle værdier
Siden v3.125.2 håndterer V8-runtime'en den understøttede single-stream-delmængde. Den eksporterer først begge levende packets, datasets og form, til et staging-område og validerer dem, og erstatter først derefter de matchende packets i den oprindelige XDP. Andre packets og rod-namespace-deklarationerne bevares. Fejler staging, røres den persistente XFA-stream aldrig, og dokumentet beholder sin ændringsmarkering
XML-kommentarer og processing instructions behøvede ekstra omsorg, fordi den interne XML-DOM dropper dem. I v3.125.2 fik deres tilstedeværelse gemningen til at fejle på stedet frem for at miste indhold lydløst. v3.126.0 bevarer dem: før parsing byttes hver kommentar eller processing instruction med en markør bygget ud fra et præfiks, der ikke optræder noget sted i den oprindelige tekst. Efter de levende packets er erstattet, skal hver markør optræde præcis én gang, før den oprindelige token gendannes, og streamen skrives. Tokens uden for de erstattede packets beholder derfor deres tekst og orden, inklusive tokens i prologen, skabelonen og andre packets
Visse inputs nægtes stadig med vilje, og hver nægtelse er en eksplicit gemmefejl:
- Kommentarer eller processing instructions inde i de levende
datasets- ellerform-packets, da deres oprindelige positioner ikke kan mappes ind i frisk eksporteret indhold - DTD-deklarationer og XMLDSig-signaturer, da omskrivning af XDP'en ikke kan holde en XML-signatur gyldig
- Ugyldig UTF-8- eller UTF-16-enkodning, ufuldstændige tags, ugyldige tegnreferencer, ukendte entiteter og misdannede processing instructions, som afvises i stedet for at blive repareret lydløst
Single-stream-outputtet er UTF-8 og bevarer XML-indholdsmodellen, ikke det oprindelige byte-layout eller encoding-deklarationen
Den sidste defekt sad under XFA. Den native filskriver bufferer output i 32 KB-blokke og flushede den sidste delblok kun i sin destruktor, efter dokument-skriveren allerede havde rapporteret succes. En disk-full- eller I/O-fejl på den sidste blok var usynlig for kalderen. Siden v3.125.2 i V8-runtime'en og v3.125.3 i den almindelige runtime er den sidste flush en del af gemmeresultatet, og XFA-ændringsmarkeringen ryddes kun efter en ægte succes. På Delphi-siden skriver TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean til en midlertidig fil ved siden af målet og flytter den på plads, kun når gemningen returnerer True, så en fejlet gemning efterlader den forrige fil intakt
Hvorfor genåbner en dynamisk XFA-form med færre sider?
En dynamisk XFA-form genåbner med færre sider, når dens rodsubform ikke deklarerer restoreState="auto", og det er en beslutning i form-forfatterskabet frem for en PDFium Component-defekt. I XFA 3.3 default'er restoreState på rodsubformen til manual. Under manual gendanner XFA-processoren kun begrænset tilstand fra det gemte form-packet og overlader resten til forfatterens scripts. Gemte feltværdier og repeating-subform-instance-antal kommer stadig tilbage, men geometriske egenskaber sat ved run time gør ikke
Tilfældet, der afslørede det, var en tresidet form, hvis script voksede en subform til h="450pt". Det gemte form-packet holdt den nye højde, værdierne og instance-antallet. Ved genåbning blev layoutet dog genopbygget fra skabelon-højderne, og formen reflowede ind på to sider. Runtime'en havde ret: skabelonen havde aldrig bedt om automatisk genoprettelse. At deklarere den på rodsubformen retter genåbningen:
<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">
<!-- felter; scripts kan ændre h eller tilføje instanser ved run time -->
</subform>
</subform>
</template>
Ejer du ikke skabelonen, så lappel ikke rundt om den i vieweren: en form, der stoler på manual-tilstand, forventer sine egne scripts til at genopbygge tilstand. Levende repaginering, mens brugeren taster, er et separat emne, dækket i hvordan PDFium Component sporer dynamiske XFA-sideantal og flyttede felter
Hvordan verificerer du en XFA-gemning i Delphi?
Det eneste pålidelige XFA-gemme-tjek er at genåbne den gemte fil i en frisk TPdf-instans og læse de gemte data tilbage. TPdf.GetXfaDatasets returnerer datasets-packet'en, som den er gemt i dokumentet, ikke den levende XFA-datamodel, så et kald før gemning viser de gamle værdier. Efter genåbning viser den præcis, hvad der blev skrevet. Et single-stream-dokument har ingen separat navngivne packets: PDFium rapporterer hele XDP'en som én packet med et tomt navn, så GetXfaPacketByName('datasets') og GetXfaDatasets returnerer ingenting, og fallback'en læser den komplette stream gennem 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; // enkelt stream: én unavngiven 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); // gemt XDP-output er UTF-8
finally
Pdf.Free;
end;
end;
Gemme-rutinen fuldfører derefter den afventende redigering, tjekker SaveAs-resultatet og sammenligner den genåbnede værdi. TPdf.ClearFormFieldFocus dræber form-fokus, hvilket er øjeblikket, hvor PDFium fuldfører edit-bufferen af det fokuserede felt. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean udfylder det fokuserede felt programmatisk, men den stoler på et fokus, som wrapperen sporer gennem FocusFormField, der gennemgår widget-annotationer. En dynamisk XFA-side har som regel ingen, så dér ankommer teksten som regel gennem tastaturinput i TPdfView, og funktionen returnerer False, når intet sporet felt har fokus
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
// Valgfri scripted udfyldning; False betyder intet sporet felt har fokus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // fuldfør edit-bufferen
if not Pdf.SaveAs(FileName) then // inkluderer den sidste 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;
Behandl substring-testen som en smoke test. Et tomt element kan serialiseres som <Tag/>, attributter kan optræde på dataelementer, og escaping ud over & og < er et serializer-valg. Til produktionstjek, indlæs den genåbnede XML med en rigtig XML-parser og sammenlign tekstknuden af det bundne dataelement. Kør også tjekket to gange i træk, for newline-defekten viste først sin fulde form på anden generation
Hurtig reference: XFA-gemmetrofasthed-tjekliste
- Deployér
pdfium.v8.dllfra v3.125.2 eller senere til XFA-former, og v3.125.3 eller senere til den almindeligepdfium.dll, så det sidste-skriv-fix er i begge - Peg
LibraryNamepå en fuld sti og sætEnableV8Enginetil True; en manglende sti fejler i stedet for at indlæse en anden kopi - Bekræft
TPdf.XFAogTPdf.XfaRuntimeAvailableefter åbning af dokumentet - Kald
ClearFormFieldFocusførSaveAs, så det fokuserede felt fuldføres - Ignorér aldrig Boolean-resultatet af
SaveAs; et False-resultat efterlader den forrige fil på plads - Verificér ved at genåbne i en ny
TPdfog læseGetXfaDatasets, med fallback tilGetXfaFormPacketsfor single-stream XFA - Test med tomme værdier, foranstillede mellemrum, flerlinjetekst,
&og et supplementary-plane-tegn, over to gemme-generationer - Forvent eksplicitte gemmefejl for DTD'er, XMLDSig og kommentarer inde i de levende packets af single-stream XFA
- Mister en dynamisk form run-time-geometri ved genåbning, så tjek rodsubformen for
restoreState="auto", før du mistænker biblioteket
For callback-strukturen, som XFA-runtime'en forventer fra en værtsapplikation, se FPDF_FORMFILLINFO version 2 og XFA-ABI'en i Delphi. V8-runtime'en, Delphi- og C++Builder-wrapperen og viewer-kontrollen er alle en del af PDFium Component for Delphi og C++Builder, som inkluderer begge Windows-runtimes til Win32 og Win64