Teknisk artikel

PDFium Component XFA Save: Linjeskift, emoji og restoreState

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ÅrsagFixet i
Tomt felt indeholder et linjeskift; værdier vokser ét linjeskift pr. gemningBegge XFA-skrivere indsatte layout-linjeskift efter start-tagsv3.125.2, pdfium.v8.dll
U+1F642 kommer tilbage som U+F642, eller emoji'en forsvinder fra form-packet'en16-bit wchar_t-afkortning ved dekodning; surrogate-filtrering i form-serializerenv3.125.2, pdfium.v8.dll
Redigeringer i et single-stream XFA-dokument er simpelthen vækNativ gemning afviste stream-layoutet, men returværdien blev ignoreretv3.125.2; kommentarer og processing instructions bevaret siden v3.126.0
Afkortet fil, selv om gemningen rapporterede succesDet sidste bufferede skriv fejlede, efter skriveren allerede havde returneret succesv3.125.2 V8-runtime; v3.125.3 almindelig pdfium.dll
Tresidet dynamisk form genåbner som to siderRodsubformen 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

PDFium Component XFA-gemme-cyklus-diagram, hvor skriveren tilføjer et linjeskift efter start-tags, den genåbnende parser læser LF'en mellem Comments-tags som feltværdien, og hver yderligere gemning tilføjer endnu et linjeskift, indtil v3.125.2 fjerner kun serializer-syntetiseret whitespace
Én gem-genåbn-cyklus planter det første linjeskift, og hver yderligere runde tilføjer endnu et, hvilket er grunden til, at driften først viste sin fulde form på anden generation

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

PDFium Component surrogate-håndterings-diagram, hvor U+1F642 ankommer som UTF-16-paret D83D DE42, og to defekte veje korrupterer det: 16-bit wchar_t-dekodere afkorter skalaren til U+F642 i private use area, mens form-serializeren filtrerer løse surrogater og dropper emoji'en helt
Windows wchar_t er 16 bits bred, så en skalar, der behøver et surrogate-par, mistede enten sin høje halvdel eller forsvandt fra packet'en, indtil begge veje lærte at holde par sammen

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- eller form-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
PDFium Component single-stream XFA-gemme-pipeline, hvor levende datasets- og form-packets eksporteres til staging, valideres og derefter erstattes inde i den oprindelige XDP med kommentarer bevaret gennem markører, mens staging-fejl og inputs som DTD'er eller XMLDSig nægter gemningen eksplicit
Det stagede eksport valideres, før noget erstattes, så en fejlet gemning efterlader den persistente XFA-stream urørt, og dokumentet beholder sin ændringsmarkering

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, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [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.dll fra v3.125.2 eller senere til XFA-former, og v3.125.3 eller senere til den almindelige pdfium.dll, så det sidste-skriv-fix er i begge
  • Peg LibraryName på en fuld sti og sæt EnableV8Engine til True; en manglende sti fejler i stedet for at indlæse en anden kopi
  • Bekræft TPdf.XFA og TPdf.XfaRuntimeAvailable efter åbning af dokumentet
  • Kald ClearFormFieldFocus før SaveAs, 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 TPdf og læse GetXfaDatasets, med fallback til GetXfaFormPackets for 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