Teknisk artikkel

PDFium XFA-lagring: Linjeskift, emoji og restoreState

PDFium Component lagrer redigerte XFA-skjemaverdier eksakt, gjennom lagring og gjenåpning, når den kjører Windows V8-kjøretiden pdfium.v8.dll levert i v3.125.2 eller senere. Eldre kjøretider la til linjeskift i feltverdier, kuttet emoji ned til et urelatert BMP-tegn, hoppet stille over enkeltstrøm XFA-lagringer og kunne svelge en feilet siste skriving. Ett gjenåpningssymptom er overhodet ikke en biblioteksdefekt: et dynamisk skjema hvis rot-subform mangler restoreState="auto", bygger layouten sin opp fra malen på nytt

Feilrapportene for dette så alle like ut. En kunde fyller ut et XFA-skjema i en Delphi-viser, lagrer, åpner på nytt, og noe er litt av. En tom kommentarboks inneholder nå en blank linje, og etter en ny lagring inneholder den to. Et navn skrevet med en emoji kommer tilbake med et private-use-tegn. Ingen får en feil, og det er det som gjør disse buggene dyre: avdriften dukker opp uker senere i noen andre sin eksport

Hva går galt når et XFA-skjema lagres og åpnes på nytt?

Fire separate defekter i den native XFA-lagringsstien forårsaket verdidrift, og hver skjulte seg bak en lagring som så vellykket ut. To kom fra serialisering, én fra lagringsoppsettet med enkeltstrøm, og én fra PDF-skriveren selv. Tabellen mapper hvert symptom til sin årsak og til utgivelsen der PDFium Component rettet det

Symptom etter gjenåpningÅrsakRettet i
Tomt felt holder et linjeskift; verdier vokser ett linjeskift per lagringBegge XFA-skriverne satt inn layout-linjeskift etter start-taggerv3.125.2, pdfium.v8.dll
U+1F642 kommer tilbake som U+F642, eller emoji-en forsvinner fra skjemapakken16-bit wchar_t-avkorting i dekoding; surrogat-filtrering i skjemaserialiserenv3.125.2, pdfium.v8.dll
Redigeringer i et enkeltstrøm XFA-dokument er rett og slett borteNative lagring avviste strømoppsettet, men returverdien ble ignorertv3.125.2; kommentarer og prosessinstruksjoner beholdt siden v3.126.0
Avkortet fil selv om lagringen rapporterte suksessDen siste bufrede skrivingen feilet etter at skriveren allerede hadde returnert suksessv3.125.2 V8-kjøretid; v3.125.3 vanlig pdfium.dll
Tretagers dynamisk skjema åpner på nytt som to siderRot-subformen ber ikke om restoreState="auto"Skjemaforfatterskap, ikke en biblioteksdefekt

Tidligere omtaler konkluderte med at XFA-feltredigeringer ikke kunne lagres med PDFium i det hele tatt, noe som var riktig for den tids kjøretider. Den nyere V8-kjøretiden lagrer XFA-verdier nativt, så en redigering gjort i det levende skjemaet når den lagrede datasets-pakken uten pakke-kirurgi på din side

Hvilken PDFium-kjøretid lagrer XFA-verdier?

XFA-lagrings-troskap avhenger av den native DLL-en, ikke av Delphi-wrapperen, så den første sjekken er hvilken kjøretid prosessen din faktisk lastet. PDFium Component leverer to Windows-bygg per arkitektur: den vanlige pdfium.dll, bygget uten V8 og XFA, og pdfium.v8.dll, som bærer JavaScript-motoren og XFA-skjemakjøretiden. Bare pdfium.v8.dll kan kjøre et XFA-skjema, så hver XFA-fiks beskrevet her bor der, med start i de gjenoppbygde Win32- og Win64 V8-bibliotekene i v3.125.2

Sisteskriving-fiksen er generisk PDF-skriver-kode, så den betyr noe for vanlige dokumenter også. v3.125.3 gjenoppbygget de vanlige pdfium.dll-bibliotekene for å bære samme reparasjon. Delt kildekode er ikke bevis på delt atferd: til binærfilen er gjenoppbygget, beholder den gamle DLL-en den gamle buggen

En andre felle satt i loaderen. Før v3.125.2 fikk det å sette EnableV8Engine til True bindingen til å velge standard pdfium.v8.dll-navnet og ignorere en full sti i LibraryName. En applikasjon som pekte på en fersk utrullet kjøretid, kunne fortsette å laste en eldre kopi fra en annen mappe. Siden v3.125.2 velger en LibraryName som inneholder en katalog, nøyaktig den filen i begge motormodusene, og en manglende sti feiler i stedet for å falle tilbake til et annet medfølgende bibliotek

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // En katalog i LibraryName fester denne nøyaktige filen (v3.125.2 og senere);
  // mangler filen, reiser lastinget et unntak i stedet for å falle tilbake
{$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;  // feil ved oppstart, ikke ved den første lagringen
end;

Etter åpning av et dokument forteller TPdf.XFA deg at filen inneholder XFA, og TPdf.XfaRuntimeAvailable forteller deg at den lastede DLL-en faktisk kan kjøre den. Trenger du også å skille statiske og dynamiske skjemaer, returnerer TPdf.FormType ftXfaFull eller ftXfaForeground; artikkelen om deteksjon av XFA-skjemaer og uttrekk av XFA-pakker i Delphi dekker den probingen i detalj

Hvorfor får lagrete XFA-felter ekstra linjeskift?

Lagrete XFA-felter fikk linjeskift fordi begge native XFA-skriverne, den generiske XML-element-skriveren og skjemapakk-serialisatoren, pretty-printet utdataene sine med et linjeskift etter start-tagger. I de fleste XML er det mellomrommet kosmetisk. I XFA-data er det ikke det: når datasets-pakken parses igjen, er teksten mellom <Comments> og </Comments> feltverdien, linjeskift inkludert. Et tomt felt åpnet seg derfor igjen med ett enkelt LF, og hver videre lag-og-gjenåpning-syklus kunne legge til enda et

PDFium Component XFA-lagringssyklus-diagram der skriveren legger til et linjeskift etter start-tagger, den gjenåpnede parseren leser LF-en mellom Comments-taggene som feltverdien, og hver videre lagring legger til enda et linjeskift til v3.125.2 fjerner bare serializer-syntetisert mellomrom
Én lagring-gjenåpning-syklus planter det første linjeskiftet, og hver videre runde legger til enda et, noe som er grunnen til at avdriften viste sin fulle form først i andre generasjon

Den åpenbare reparasjonen, å trimme verdier ved lasting, ville være gal. Brukere taster ledende mellomrom, avsluttende mellomrom og bevisst flerlinjet tekst inn i XFA-felt, og en adresseblokk eller en fastbreddet kode må overleve byte for byte. v3.125.2-fiksen fjerner derfor bare mellomrommet serializeren selv syntetiserte rundt tagger. Brukerverdier, eksisterende tekstnoder og CDATA-seksjoner passerer uantastet, så " indented" forblir innrykket og et bevisst tomt felt forblir tomt

Hvorfor kommer en emoji tilbake som et annet tegn?

En emoji kom tilbake feil fordi Windows wchar_t er 16 biter bredt, og to dekodingsstier lagret en full Unicode skalarverdi i ett enkelt wchar_t. UTF-8-strøm-dekoderen og parseren for numeriske tegnreferanser som &#x1F642; gjorde begge dette. U+1F642, det lett smilende ansiktet, får ikke plass i 16 biter, så de høye bitene falt av og U+F642 dukket opp i stedet: et kodepunkt i Private Use Area som de fleste fonter renderer som en boks eller ingenting

Skjemaserialisatoren hadde det motsatte problemet. Den filtrerte tegn ett wchar_t om gangen, så to surrogat-kodeenheter som er ugyldige isolert, og droppet begge, så emoji-en forsvant fra skjemapakken fullstendig. I v3.125.2 konsumerer dekoderen hver skalarverdi fullstendig og sender ut et ordentlig surrogatpar. Når bare én utdataplass er igjen, holder den det lave surrogatet ventende og rapporterer ikke slutt-på-strøm mens den enheten fortsatt er bufret. En UTF-8-sekvens splittet over leseblokker, føres over til neste lesing i stedet for å kastes. Skjemaeksportøren beholder nå gyldige surrogatpar sammen, og numeriske tegnreferanser produserer korrekte par også

PDFium Component surrogathåndtering-diagram der U+1F642 ankommer som UTF-16-paret D83D DE42 og to defektstier korruperer det: 16-bit wchar_t-dekodere avkorter skalaren til U+F642 i private use-området, mens skjemaserialiseren filtrerer enslige surrogater og dropper emoji-en fullstendig
Windows wchar_t er 16 biter bredt, så en skalar som trenger et surrogatpar, mistet enten sin høye halvdel eller forsvant fra pakken til begge stiene lærte å holde par sammen

Latin-1 testdata viser aldri noe av dette, så hver XFA-rundtur-test trenger minst ett supplementært plan-tegn

Enkeltstrøm XFA og lagringsfeil ingen så

Et enkeltstrøm XFA-dokument mistet redigeringene sine fordi den native lagringshjelperen avviste det lagringsoppsettet og kalleren ignorerte feilen. ISO 32000-1 §12.7.8 tillater at /XFA-oppføringen i den interaktive skjemaordboken er enten en matrise av pakkenavn og strømmer, eller én enkelt strøm som holder hele XDP-dokumentet. Pakkematriser er det vanlige tilfellet, men enkeltstrømmer er fullt lovlig, og PDF-lagringen ble fullført som om ingenting hadde skjedd mens skjemadataene sto på de gamle verdiene

Siden v3.125.2 håndterer V8-kjøretiden den støttede enkeltstrøm-delmengden. Den eksporterer først begge levende pakkene, datasets og form, inn i et staging-område og validerer dem, og erstatter først så de matchende pakkene i den opprinnelige XDP-en. Andre pakker og rotnavneromsdeklarasjonene beholdes. Feiler stagingen, røres aldri den vedvarende XFA-strømmen, og dokumentet beholder endringsmerket sitt

XML-kommentarer og prosessinstruksjoner trengte ekstra omhu fordi den interne XML-DOM-en dropper dem. I v3.125.2 fikk deres tilstedeværelse lagringen til å feile rett ut i stedet for å miste innhold i stillhet. v3.126.0 bevarer dem: før parsing byttes hver kommentar eller prosessinstruksjon mot en markør bygget fra et prefiks som ikke forekommer noe sted i den opprinnelige teksten. Etter at de levende pakkene er erstattet, må hver markør opptre nøyaktig én gang før det opprinnelige tokenet gjenopprettes og strømmen skrives. Tokener utenfor de erstattede pakkene beholder derfor teksten og rekkefølgen sin, inkludert tokenene i prologen, malen og andre pakker

Noen inndata avvises fortsatt med vilje, og hver avvisning er en eksplisitt lagringsfeil:

  • Kommentarer eller prosessinstruksjoner inne i de levende datasets- eller form-pakkene, siden deres opprinnelige posisjoner ikke kan kartlegges inn i ferskeksportert innhold
  • DTD-deklarasjoner og XMLDSig-signaturer, siden å omskrive XDP-en ikke kan holde en XML-signatur gyldig
  • Ugyldig UTF-8- eller UTF-16-enkoding, ufullstendige tagger, ugyldige tegnreferanser, ukjente enheter og feilformede prosessinstruksjoner, som avvises i stedet for å repareres i stillhet
PDFium Component enkeltstrøm XFA-lagringsrørledning der levende datasets- og form-pakker eksporteres til staging, valideres, og så erstattes inne i den opprinnelige XDP-en med kommentarer bevart gjennom markører, mens staging-feil og inndata som DTD-er eller XMLDSig avviser lagringen eksplisitt
Den stagede eksporten valideres før noe erstattes, så en feilet lagring etterlater den vedvarende XFA-strømmen urørt, og dokumentet beholder endringsmerket sitt

Enkeltstrøm-utdataet er UTF-8 og bevarer XML-innholdsmodellen, ikke det opprinnelige byteoppsettet eller enkodingsdeklarasjonen

Den siste defekten satt under XFA. Den native filskriveren buffer-er utdata i 32 KB-blokker og tømte den siste delvise blokken bare i destruktoren sin, etter at dokumentskriveren allerede hadde rapportert suksess. En disk-full- eller I/O-feil på den siste blokken var usynlig for kalleren. Siden v3.125.2 i V8-kjøretiden og v3.125.3 i den vanlige kjøretiden, er den siste tømningen del av lagringsresultatet, og XFA-endringsmerket tømmes først etter en ekte suksess. På Delphi-siden skriver TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean til en midlertidig fil ved siden av målet og flytter den på plass bare når lagringen returnerer True, så en feilet lagring etterlater den forrige filen intakt

Hvorfor åpner et dynamisk XFA-skjema på nytt med færre sider?

Et dynamisk XFA-skjema åpner på nytt med færre sider når rot-subformen ikke deklarerer restoreState="auto", og det er en skjemaforfatter-beslutning snarere enn en PDFium Component-defekt. I XFA 3.3 har restoreState på rot-subformen manual som standard. Under manual gjenoppretter XFA-prosessoren bare begrenset tilstand fra den lagrete skjemapakken og overlater resten til forfatterens skript. Lagrete feltverdier og instansantall for repeterende subforms kommer fortsatt tilbake, men geometriske egenskaper satt under kjøring gjør det ikke

Tilfellet som avslørte dette, var et tretagers skjema hvis skript vokste en subform til h="450pt". Den lagrete skjemapakken holdt den nye høyden, verdiene og instansantallene. Ved gjenåpning ble layouten derimot bygget opp fra malhøydene, og skjemaet fløt om til to sider. Kjøretiden hadde rett: malen hadde aldri bedt om automatisk gjenoppretting. Å deklarere det på rot-subformen fikser gjenåpningen:

<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">
      <!-- felt; skript kan endre h eller legge til instanser under kjøring -->
    </subform>
  </subform>
</template>

Eier du ikke malen, lapp ikke rundt den i viseren: et skjema som er avhengig av manual-modus forventer egne skript til å bygge opp tilstanden på nytt. Levende repaginering mens brukeren skriver, er et separat tema, dekket i hvordan PDFium Component sporer dynamiske XFA-sideantall og flyttede felt

Hvordan verifiserer du en XFA-lagring i Delphi?

Den eneste pålitelige XFA-lagringssjekken er å åpne den lagrete filen på nytt i en fersk TPdf-instans og lese de lagrete dataene tilbake. TPdf.GetXfaDatasets returnerer datasets-pakken slik den er lagret i dokumentet, ikke den levende XFA-datamodellen, så å kalle den før lagring viser de gamle verdiene. Etter gjenåpning viser den nøyaktig det som ble skrevet. Et enkeltstrøm-dokument har ingen separat navngitte pakker: PDFium rapporterer hele XDP-en som én pakke med et tomt navn, så GetXfaPacketByName('datasets') og GetXfaDatasets returnerer ingenting, og fallbacken leser hele strømmen gjennom 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;          // pakke-matrise-oppsett
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // enkeltstrøm: én navnløs pakke
      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);  // lagret XDP-utdata er UTF-8
  finally
    Pdf.Free;
  end;
end;

Lagringsrutinen forplikter så den ventende redigeringen, sjekker SaveAs-resultatet og sammenligner den gjenåpnede verdien. TPdf.ClearFormFieldFocus dreper skjemafokuset, noe som er øyeblikket PDFium forplikter redigeringsbufferen til feltet i fokus. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean fyller feltet i fokus programmatisk, men den er avhengig av et fokus wrapperen sporer gjennom FocusFormField, som vandrer widget-annotasjoner. En dynamisk XFA-side har normalt ingen, så der kommer teksten vanligvis gjennom tastaturinndata i TPdfView, og funksjonen 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 skriptbasert utfylling; False betyr at 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;              // forplikt redigeringsbufferen
  if not Pdf.SaveAs(FileName) then      // inkluderer siste tømning (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;

Behandle delstreng-testen som en røyktest. Et tomt element kan serialiseres som <Tag/>, attributter kan dukke opp på dataelementer, og escaping utover & og < er et serializer-valg. For produksjonssjekker, last den gjenåpnede XML-en med en ekte XML-parser og sammenlign tekstnoden til det bundne dataelementet. Kjør sjekken to ganger på rad også, fordi linjeskift-defekten bare viste sin fulle form i andre generasjon

Hurtigreferanse: Sjekkliste for XFA-lagringstroskap

  • Distribuer pdfium.v8.dll fra v3.125.2 eller senere for XFA-skjemaer, og v3.125.3 eller senere for den vanlige pdfium.dll, slik at sisteskriving-fiksen finnes i begge
  • Pek LibraryName mot en full sti og sett EnableV8Engine til True; en manglende sti feiler i stedet for å laste en annen kopi
  • Bekreft TPdf.XFA og TPdf.XfaRuntimeAvailable etter åpning av dokumentet
  • Kall ClearFormFieldFocus før SaveAs slik at feltet i fokus forpliktes
  • Aldri ignorer det boolske resultatet av SaveAs; et False-resultat etterlater den forrige filen på plass
  • Verifiser ved å gjenåpne i en ny TPdf og lese GetXfaDatasets, med fallback til GetXfaFormPackets for enkeltstrøm XFA
  • Test med tomme verdier, ledende mellomrom, flerlinjet tekst, & og et supplementært plan-tegn, over to lagringsgenerasjoner
  • Forvent eksplisitte lagringsfeil for DTD-er, XMLDSig og kommentarer inne i de levende pakkene til enkeltstrøm XFA
  • Mister et dynamisk skjema kjøretidsgeometri ved gjenåpning, sjekk rot-subformen for restoreState="auto" før du mistenker biblioteket

For callback-strukturen XFA-kjøretiden forventer fra en vertsapplikasjon, se FPDF_FORMFILLINFO versjon 2 og XFA-ABI-en i Delphi. V8-kjøretiden, Delphi- og C++Builder-wrapperen og viserkontrollen er alle del av PDFium Component for Delphi og C++Builder, som inkluderer begge Windows-kjøretidene for Win32 og Win64