Teknisk artikel

PDFium Component XFA: nyrader, emoji och restoreState

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öppningOrsakFixat i
Tomt fält håller en radmatning; värden växer en nyrad per sparningBåda XFA-skrivarna infogade layoutnyrader efter starttaggarv3.125.2, pdfium.v8.dll
U+1F642 kommer tillbaka som U+F642, eller emojin försvinner från formulärpaketet16-bitars wchar_t-avhuggning i avkodningen; surrogatfiltrering i formulärserialiserarenv3.125.2, pdfium.v8.dll
Redigeringar i ett ensam-ström-XFA-dokument är helt enkelt bortaNativa sparandet avvisade ström-layouten, men returvärdet ignoreradesv3.125.2; kommentarer och bearbetningsinstruktioner bevarade sedan v3.126.0
Avhuggen fil fastän sparandet rapporterade framgångSlutlig buffrad skrivning misslyckades efter att skrivaren redan returnerat framgångv3.125.2 V8-körning; v3.125.3 vanliga pdfium.dll
Tresidigt dynamiskt formulär öppnas igen som två sidorRotsubformulä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

PDFium Component-diagram över XFA-sparcykeln där skrivaren lägger till en nyrad efter starttaggar, den återöppnade parsaren läser LF mellan Comments-taggarna som fältvärdet, och varje ytterligare sparning lägger till ytterligare en radmatning tills v3.125.2 tar bort bara serialisatorsyntetiserat mellanrum
En spara-öppna-igen-cykel planterar den första radmatningen och varje ytterligare omgång lägger till en till, vilket är varför driften visade sin fulla form först på den andra generationen

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 &#x1F642; 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å

PDFium Component-diagram över surrogathantering där U+1F642 anländer som UTF-16-paret D83D DE42 och två defekta vägar korrumperar det: 16-bitars wchar_t-avkodare hugger av skalaren till U+F642 i det privata användningsområdet, medan formulärserialiseraren filtrerar ensamma surrogat och släpper emojin helt
Windows wchar_t är 16 bitar brett, så en skalär som behöver ett surrogatpar tappade antingen sin högra halva eller försvann från paketet tills båda vägarna lärde sig hålla par ihop

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- eller form-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
PDFium Component-pipeline för ensam-ström-XFA-sparande där levande datasets- och form-paket exporteras till ställning, valideras, sedan ersätts inuti den ursprungliga XDP:en med kommentarer bevarade via markörer, medan ställningsmisslyckanden och indata som DTD:er eller XMLDSig avvisar sparandet explicit
Den ställda exporten valideras innan något ersätts, så ett misslyckat sparande lämnar den beständiga XFA-strömmen orörd och dokumentet behåller sin ändringsmarkering

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, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [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.dll från v3.125.2 eller senare för XFA-formulär, och v3.125.3 eller senare för det vanliga pdfium.dll, så slutskrivningsfixen finns i båda
  • Pekta LibraryName på en fullständig sökväg och sätt EnableV8Engine till True; en saknad sökväg misslyckas i stället för att ladda en annan kopia
  • Bekräfta TPdf.XFA och TPdf.XfaRuntimeAvailable efter att ha öppnat dokumentet
  • Anropa ClearFormFieldFocus före SaveAs så 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 TPdf och läsa GetXfaDatasets, med återgång till GetXfaFormPackets fö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