PDFium Component sparar redigerade XFA-formulärvärden exakt, genom sparning och återöppning, när den kör Windows V8-körningen pdfium.v8.dll levererad i v3.125.2 eller senare. Äldre körningar lade till radmatningar i fältvärden, högg emoji ner till ett orelaterat BMP-tecken, hoppade tyst över ensam-ström-XFA-sparanden och kunde svälja en misslyckad slutskrivning. Ett återöppningssymptom är ingen biblioteksdefekt alls: ett dynamiskt formulär vars rotsubformulär saknar restoreState="auto" bygger om sin layout från mallen
Buggrapporterna för det här såg alla likadana ut. En kund fyller i ett XFA-skadeformulär i en Delphi-visare, sparar, öppnar igen, och något är svagt fel. En tom kommentarsruta innehåller nu en tom rad, och efter ett andra sparande innehåller den två. Ett namn inskrivet med en emoji kommer tillbaka med ett privatanvändningsglyf. Ingen får något fel, vilket är det som gör de här buggarna dyra: driften dyker upp veckor senare i någon annans export
Vad går fel när ett XFA-formulär sparas och öppnas igen?
Fyra separata defekter i den nativa XFA-sparvägen orsakade värdedrift, och var och en gömde sig bakom en sparning som såg framgångsrik ut. Två kom från serialisering, en från ensam-ström-lagringslayouten, och en från PDF-skrivaren själv. Tabellen mappar varje symptom till dess orsak och till utgåvan där PDFium Component fixade det
| Symptom efter återöppning | Orsak | Fixat i |
|---|---|---|
| Tomt fält håller en radmatning; värden växer en nyrad per sparning | Båda XFA-skrivarna infogade layoutnyrader efter starttaggar | v3.125.2, pdfium.v8.dll |
| U+1F642 kommer tillbaka som U+F642, eller emojin försvinner från formulärpaketet | 16-bitars wchar_t-avhuggning i avkodningen; surrogatfiltrering i formulärserialiseraren | v3.125.2, pdfium.v8.dll |
| Redigeringar i ett ensam-ström-XFA-dokument är helt enkelt borta | Nativa sparandet avvisade ström-layouten, men returvärdet ignorerades | v3.125.2; kommentarer och bearbetningsinstruktioner bevarade sedan v3.126.0 |
| Avhuggen fil fastän sparandet rapporterade framgång | Slutlig buffrad skrivning misslyckades efter att skrivaren redan returnerat framgång | v3.125.2 V8-körning; v3.125.3 vanliga pdfium.dll |
| Tresidigt dynamiskt formulär öppnas igen som två sidor | Rotsubformuläret begär inte restoreState="auto" | Formulärskapande, inte en biblioteksdefekt |
Tidigare utlägg drog slutsatsen att XFA-fältredigeringar inte alls kunde bevaras med PDFium, vilket stämde för den tidens körningar. Den nyare V8-körningen sparar XFA-värden nativt, så en redigering gjord i det levande formuläret når det sparade datasets-paketet utan paketkirurgi på din sida
Vilken PDFium-körning sparar XFA-värden?
XFA-sparningstrohet beror på den nativa DLL:en, inte på Delphi-wrappern, så den första kontrollen är vilken körning din process faktiskt laddat. PDFium Component levererar två Windows-byggen per arkitektur: det vanliga pdfium.dll, byggt utan V8 och XFA, och pdfium.v8.dll, som bär JavaScript-motorn och XFA-formulärkörningen. Bara pdfium.v8.dll kan köra ett XFA-formulär, så varje XFA-fix som beskrivs här bor där, med början i de återbyggda Win32- och Win64 V8-biblioteken i v3.125.2
Slutskrivningsfixen är generisk PDF-skrivarkod, så den spelar roll för vanliga dokument också. v3.125.3 byggde om de vanliga pdfium.dll-biblioteken för att bära samma reparation. Delad källkod är inget bevis för delat beteende: tills binären byggs om behåller den gamla DLL:en den gamla buggen
En andra fälla satt i laddaren. Före v3.125.2 fick att sätta EnableV8Engine till True bindningen att välja standardnamnet pdfium.v8.dll och ignorera en fullständig sökväg i LibraryName. En applikation som pekade på en färsk distribuerad körning kunde fortsätta ladda en äldre kopia från en annan mapp. Sedan v3.125.2 väljer en LibraryName som innehåller en katalog exakt den filen i båda moterlägena, och en saknad sökväg misslyckas i stället för att falla tillbaka på ett annat medföljande bibliotek
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// En katalog i LibraryName låser just denna fil (v3.125.2 och senare);
// saknas filen kastar inläsningen i stället för att falla tillbaka
{$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; // misslyckas vid uppstart, inte vid första sparandet
end;
Efter att ha öppnat ett dokument talar TPdf.XFA om att filen innehåller XFA och TPdf.XfaRuntimeAvailable talar om att den laddade DLL:en faktiskt kan exekvera den. Behöver du också skilja statiska och dynamiska formulär åt returnerar TPdf.FormType ftXfaFull eller ftXfaForeground; artikeln om att upptäcka XFA-formulär och extrahera XFA-paket i Delphi täcker det sonderandet i detalj
Varför får sparade XFA-fält extra radmatningar?
Sparade XFA-fält fick radmatningar för att båda nativa XFA-skrivarna, den generiska XML-elementskrivaren och formulärpaketserialiseraren, pretty-printade sin utdata med en nyrad efter starttaggar. I de flesta XML är det mellanrummet kosmetiskt. I XFA-data är det inte det: när datasets-paketet parsas igen är texten mellan <Comments> och </Comments> fältvärdet, nyrad inkluderad. Ett tomt fält öppnades därför igen hållande en enda LF, och varje ytterligare spara-och-öppna-cykel kunde lägga till en till
Den uppenbara reparationen, att trimma värden vid inläsning, skulle vara fel. Användare skriver inledande mellanslag, avslutande mellanslag och medveten fleradig text i XFA-fält, och ett adressblock eller en kod med fast bredd måste överleva byte för byte. Fixen i v3.125.2 tar därför bara bort det mellanrum som serialisatorn själv syntetiserade runt taggar. Användarvärden, befintliga textnoder och CDATA-sektioner passerar orörda, så " indented" förblir indenterat och ett medvetet tomt fält förblir tomt
Varför kommer en emoji tillbaka som ett annat tecken?
En emoji kom tillbaka fel för att Windows wchar_t är 16 bitar brett, och två avkodningsvägar lagrade ett fullständigt Unicode-skalarvärde i en enda wchar_t. UTF-8-strömavkodaren och parsaren för numeriska teckenreferenser som 🙂 gjorde båda detta. U+1F642, det svagt leende ansiktet, ryms inte i 16 bitar, så de höga bitarna föll av och U+F642 dök upp i stället: en kodpunkt i Private Use Area som de flesta typsnitt renderar som en ruta eller ingenting
Formulärserialiseraren hade det motsatta problemet. Den filtrerade tecken en wchar_t i taget, såg två surrogatkodenheter som är ogiltiga i isolering, och släppte båda, så emojin försvann från formulärpaketet helt. I v3.125.2 konsumerar avkodaren varje skalarvärde fullständigt och avger ett riktigt surrogatpar. När bara en utdataplat är kvar håller den det låga surrogatet väntande och rapporterar inte slut på ström medan den enheten fortfarande är buffrad. En UTF-8-sekvens delad över läsblock förs vidare till nästa läsning i stället för att kasseras. Formulärexportören håller nu giltiga surrogatpar ihop, och numeriska teckenreferenser producerar riktiga par också
Latin-1-testdata visar aldrig något av detta, så varje XFA-rundturntest behöver minst ett tecken på det kompletmentära planet
Ensam-ström-XFA och sparningsfel ingen såg
Ett ensam-ström-XFA-dokument förlorade sina redigeringar för att den nativa sparningshjälparen avvisade den lagringslayouten och dess anropare ignorerade misslyckandet. ISO 32000-1 §12.7.8 tillåter att /XFA-posten i den interaktiva formulärsordlistan antingen är en matris av paketnamn och strömmar eller en enda ström som håller hela XDP-dokumentet. Paketmatriser är det vanliga fallet, men ensamma strömmar är helt lagliga, och PDF-sparandet fullbordades som om ingenting hänt medan formulärdatan stannade på sina gamla värden
Sedan v3.125.2 hanterar V8-körningen den stödda delen av ensam-ström-mängden. Den exporterar först båda levande paket, datasets och form, till ett ställningsområde och validerar dem, och först sedan ersätts de matchande paketen i den ursprungliga XDP:en. Andra paket och rotens namnrymdsdeklarationer bevaras. Misslyckas ställningen rör den beständiga XFA-strömmen aldrig och dokumentet behåller sin ändringsmarkering
XML-kommentarer och bearbetningsinstruktioner behövde extra omsorg för att den interna XML-DOM:en släpper dem. I v3.125.2 fick deras närvaro sparandet att misslyckas rakt av i stället för att tyst förlora innehåll. v3.126.0 bevarar dem: före parsningen byts varje kommentar eller bearbetningsinstruktion mot en markör byggd från ett prefix som inte förekommer någonstans i den ursprungliga texten. Efter att de levande paketen ersatts måste varje markör dyka upp exakt en gång innan den ursprungliga tokenen återställs och strömmen skrivs. Tokener utanför de ersatta paketen behåller därför sin text och ordning, inklusive tokener i prologen, mallen och andra paket
Vissa indata avvisas fortfarande med flit, och varje avslag är ett explicit sparningsfel:
- Kommentarer eller bearbetningsinstruktioner inuti de levande
datasets- ellerform-paketen, eftersom deras ursprungliga positioner inte kan mappas in i färskt exporterat innehåll - DTD-deklarationer och XMLDSig-signaturer, eftersom att skriva om XDP:en inte kan hålla en XML-signatur giltig
- Ogiltig UTF-8- eller UTF-16-kodning, ofullständiga taggar, ogiltiga teckenreferenser, okända entiteter och felformade bearbetningsinstruktioner, vilka avvisas i stället för att tyst repareras
Ensam-ström-utdata är UTF-8 och bevarar XML-innehållsmodellen, inte den ursprungliga byte-layouten eller kodningsdeklarationen
Den sista defekten satt under XFA. Den nativa filskrivaren buffrar utdata i 32 KB-block och spolade det sista ofullständiga blocket först i sin destruktor, efter att dokumentskrivaren redan rapporterat framgång. Ett disk-full- eller I/O-fel på det sista blocket var osynligt för anroparen. Sedan v3.125.2 i V8-körningen och v3.125.3 i den vanliga körningen är den slutliga spolningen en del av sparningsresultatet, och XFA-ändringsmarkeringen rensas först efter en verklig framgång. På Delphi-sidan skriver TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean till en temporär fil bredvid målet och flyttar den på plats först när sparandet returnerar True, så ett misslyckat sparande lämnar den tidigare filen intakt
Varför öppnas ett dynamiskt XFA-formulär igen med färre sidor?
Ett dynamiskt XFA-formulär öppnas igen med färre sidor när dess rotsubformulär inte deklarerar restoreState="auto", och det är ett formulärskapandebeslut snarare än en PDFium Component-defekt. I XFA 3.3 har restoreState på rotsubformuläret standardvärdet manual. Under manual återställer XFA-processorn bara begränsat tillstånd från det sparade formulärpaketet och lämnar resten till upphovsmannens skript. Sparade fältvärden och instansantal för upprepande subformulär kommer fortfarande tillbaka, men geometriska egenskaper satta vid körning gör det inte
Fallet som avslöjade detta var ett tresidigt formulär vars skript växte ett subformulär till h="450pt". Det sparade formulärpaketet höll den nya höjden, värdena och instansantalen. Vid återöppningen byggdes dock layouten om från mallhöjderna och formuläret flöt om till två sidor. Körningen hade rätt: mallen hade aldrig bett om automatisk återställning. Att deklarera den på rotsubformuläret fixar återöppningen:
<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">
<!-- fält; skript kan ändra h eller lägga till instanser vid körning -->
</subform>
</subform>
</template>
Äger du inte mallen, lappa inte runt den i visaren: ett formulär som förlitar sig på manual-läget förväntar sig att sina egna skript bygger upp tillståndet. Levande omsidläggning medan användaren skriver är ett separat ämne, som tas upp i hur PDFium Component spårar dynamiska XFA-sidantal och flyttade fält
Hur verifierar du ett XFA-sparande i Delphi?
Den enda tillförlitliga XFA-sparningskontrollen är att öppna den sparade filen igen i en färsk TPdf-instans och läsa tillbaka den lagrade datan. TPdf.GetXfaDatasets returnerar datasets-paketet som det är lagrat i dokumentet, inte den levande XFA-datamodellen, så att anropa det före sparandet visar de gamla värdena. Efter återöppningen visar det exakt vad som skrevs. Ett ensam-ström-dokument har inga separat namngivna paket: PDFium rapporterar hela XDP:en som ett paket med ett tomt namn, så GetXfaPacketByName('datasets') och GetXfaDatasets returnerar ingenting, och återgången läser den kompletta strömmen genom 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; // paketmatris-layout
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // ensam ström: ett namnlöst paket
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); // sparad XDP-utdata är UTF-8
finally
Pdf.Free;
end;
end;
Sparningsrutinen begår sedan den väntande redigeringen, kontrollerar SaveAs-resultatet och jämför det återöppnade värdet. TPdf.ClearFormFieldFocus dödar formulärfokuset, vilket är stunden PDFium begår redigeringsbufferten för det fokuserade fältet. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean fyller det fokuserade fältet programmatiskt, men den förlitar sig på ett fokus som wrappern spårar genom FocusFormField, vilken vandrar genom widgetannoteringar. En dynamisk XFA-sida har normalt inga, så där anländer texten oftast via tangentbordsinput i TPdfView, och funktionen returnerar False när inget spårat fält 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
// Valfri skriptifyllning; False betyder att inget spårat fält har fokus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // begår redigeringsbufferten
if not Pdf.SaveAs(FileName) then // inkluderar slutspolningen (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;
Behandla delsträngstestet som ett röktest. Ett tomt element kan serialiseras som <Tag/>, attribut kan dyka upp på dataelement, och escaping utöver & och < är serialisatorns val. För produktionskontroller, ladda den återöppnade XML:en med en riktig XML-parser och jämför textnoden för det bundna dataelementet. Kör kontrollen två gånger i rad också, för nyradsdefekten visade sin fulla form först på den andra generationen
Snabbreferens: checklista för XFA-sparningstrohet
- Distribuera
pdfium.v8.dllfrån v3.125.2 eller senare för XFA-formulär, och v3.125.3 eller senare för det vanligapdfium.dll, så slutskrivningsfixen finns i båda - Pekta
LibraryNamepå en fullständig sökväg och sättEnableV8Enginetill True; en saknad sökväg misslyckas i stället för att ladda en annan kopia - Bekräfta
TPdf.XFAochTPdf.XfaRuntimeAvailableefter att ha öppnat dokumentet - Anropa
ClearFormFieldFocusföreSaveAsså att det fokuserade fältet begås - Ignorera aldrig det booleska resultatet av
SaveAs; ett False-resultat lämnar den tidigare filen kvar - Verifiera genom att öppna igen i en ny
TPdfoch läsaGetXfaDatasets, med återgång tillGetXfaFormPacketsför ensam-ström-XFA - Testa med tomma värden, inledande mellanslag, fleradig text,
&och ett tecken på det kompletmentära planet, över två sparningsgenerationer - Räkna med explicita sparningsfel för DTD:er, XMLDSig och kommentarer inuti de levande paketen i ensam-ström-XFA
- Förlorar ett dynamiskt formulär körningsgeometri vid återöppning, kontrollera rotsubformuläret efter
restoreState="auto"innan du misstänker biblioteket
För återanropsstrukturen XFA-körningen förväntar sig från en värdapplikation, se FPDF_FORMFILLINFO version 2 och XFA-ABI i Delphi. V8-körningen, Delphi- och C++Builder-wrappern och visarkontrollen är alla del av PDFium Component for Delphi and C++Builder, som omfattar båda Windows-körningarna för Win32 och Win64