Odborný článok

Uloženie XFA v PDFium Component: riadky, emoji, restoreState

PDFium Component ukladá upravené hodnoty formulárov XFA presne, cez uloženie aj opätovné otvorenie, keď beží na Windows V8 runtime pdfium.v8.dll dodanom vo v3.125.2 a novšej. Staršie runtimy pridávali odriadkovania k hodnotám polí, usekli emoji na nesúvisiaci znak BMP, poticho preskočili uloženia single-stream XFA a vedeli prehltnúť zlyhaný záverečný zápis. Jeden príznak pri opätovnom otvorení nie je vôbec defekt knižnice: dynamický formulár, ktorého koreňový subformulár postráda restoreState="auto", si prestaví layout z templatu

Bug reporty k tomu všetky vyzerali rovnako. Zákazník vyplní XFA formulár priznania v Delphi prehliadači, uloží, znovu otvorí a niečo je mierne vedľa. Prázdny box komentárov teraz drží prázdny riadok a po druhom uložení drží dva. Meno napísané s emoji sa vráti s glyfom z private use. Nikto nedostane chybu, a presne to robí tieto bugy drahými: drift sa ukáže o týždne v exporte niekoho iného

Čo sa pokazí, keď sa XFA formulár uloží a znovu otvorí?

Štyri samostatné defekty v natívnej ceste ukladania XFA spôsobili drift hodnôt a každý sa skrýval za uložením vyzerajúcim ako úspešné. Dva prišli zo serializácie, jeden z úložného layoutu single-stream a jeden zo samotného PDF zapisovača. Tabuľka mapuje každý príznak na jeho príčinu a na vydanie, v ktorom ho PDFium Component opravil

Príznak po opätovnom otvoreníPríčinaOpravené vo
Prázdne pole drží odriadkovanie; hodnoty rastú o nový riadok na uloženieObidva XFA zapisovače vsadili layoutové nové riadky za začiatočné značkyv3.125.2, pdfium.v8.dll
U+1F642 sa vráti ako U+F642 alebo emoji zmizne z formulového balíka16-bitové skracovanie wchar_t pri dekódovaní; filtrovanie surrogátov vo formulovom serializátorev3.125.2, pdfium.v8.dll
Úpravy v single-stream XFA dokumente jednoducho zmiznúNatívne uloženie odmietlo stream layout, ale návratová hodnota sa ignorovalav3.125.2; komentáre a processing instructions uchované od v3.126.0
Useknutý súbor, hoci uloženie ohlásilo úspechZáverečný bufferovaný zápis zlyhal, keďže zapisovač už vrátil úspechv3.125.2 V8 runtime; v3.125.3 obyčajné pdfium.dll
Trojstranový dynamický formulár sa otvorí ako dve stranyKoreňový subformulár nežiada restoreState="auto"Autorstvo formulára, nie defekt knižnice

Skoršie texty uzavreli, že úpravy polí XFA sa nedajú s PDFiom vôbec uchovať, čo bolo presné pre runtimy tej doby. Novší V8 runtime ukladá hodnoty XFA natívne, takže úprava urobená v živom formulári dorazí do uloženého balíka datasets bez operácie na balíkoch na vašej strane

Ktorý PDFium runtime ukladá hodnoty XFA?

Veranosť ukladania XFA závisí od natívneho DLL, nie od Delphi wrapperu, takže prvá kontrola je, ktorý runtime váš proces naozaj načítal. PDFium Component dodáva dva Windows zostavenia na architektúru: obyčajné pdfium.dll, zostavené bez V8 a XFA, a pdfium.v8.dll, ktoré nesie JavaScript engine aj XFA formulový runtime. Len pdfium.v8.dll dokáže spustiť XFA formulár, takže každá XFA oprava popísaná tu býva tam, začínajúc prebudovanými Win32 a Win64 V8 knižnicami vo v3.125.2

Oprava záverečného zápisu je generický kód PDF zapisovača, takže sa týka aj obyčajných dokumentov. v3.125.3 prebudovala obyčajné knižnice pdfium.dll, aby niesli tú istú opravu. Zdieľaný zdroják nie je dôkazom zdieľaného správania: kým sa binárka neprebuduje, staré DLL drží starý bug

Druhá pasca sedela v loaderi. Pred v3.125.2 nastavenie EnableV8Engine na True prinútilo väzbu zvoliť predvolené meno pdfium.v8.dll a ignorovať plnú cestu v LibraryName. Aplikácia mieriaca na čerstvo nasadený runtime mohla naďalej načítať staršiu kópiu z inej zložky. Od v3.125.2 vyberie LibraryName obsahujúce adresár presne ten súbor v ktoromkoľvek režime engine a chýbajúca cesta zlyhá namiesto spätného pádu na inú dodávanú knižnicu

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // Adresár v LibraryName pripne presne tento súbor (v3.125.2 a novšie);
  // ak súbor chýba, načítanie vyhodí výnimku namiesto spätného pádu
{$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;  // zlyhajte pri štarte, nie pri prvom uložení
end;

Po otvorení dokumentu vám TPdf.XFA povie, že súbor obsahuje XFA, a TPdf.XfaRuntimeAvailable povie, že načítané DLL ho dokáže naozaj vykonať. Ak potrebujete odlíšiť aj statické a dynamické formuláre, TPdf.FormType vráti ftXfaFull alebo ftXfaForeground; článok o rozpoznávaní XFA formulárov a extrakcii XFA balíkov v Delphi popisuje toto prieskum podrobne

Prečo uložené polia XFA pribúdajú odriadkovania navyše?

Uložené polia XFA pribreli odriadkovania, lebo obidva natívne XFA zapisovače, generický zapisovač XML elementov aj serializátor formulového balíka, pretty-printovali výstup novým riadkom za začiatočnými značkami. Vo väčšine XML je tento biely priestor kozmetický. V dátach XFA nie je: keď sa balík datasets rozparsuje znova, text medzi <Comments> a </Comments> je hodnotou poľa, nový riadok vrátane. Prázdne pole sa preto znovu otvorilo držiac jediný LF a každý ďalší cyklus uloženia a otvorenia mohol pridať ďalší

Diagram cyklu ukladania XFA v PDFium Component, kde zapisovač pridá nový riadok za začiatočné značky, znovuotvorený parser číta LF medzi značkami Comments ako hodnotu poľa a každé ďalšie uloženie pripojí ďalšie odriadkovanie, až kým v3.125.2 neodstráni len biely priestor syntetizovaný serializátorom
Jeden cyklus uloženie-opätovné otvorenie zasadí prvé odriadkovanie a každé ďalšie kolo pridáva ďalšie, preto drift ukázal celý tvar až v druhej generácii

Zjavná oprava, odstrihávanie hodnôt pri načítaní, by bola zlá. Používatelia píšu do polí XFA úvodné medzery, koncové medzery a zámerný viacriadkový text a adresný blok ani kód pevnej šírky nemôžu prežiť inak než bajt za bajt. Oprava vo v3.125.2 preto odstraňuje len biely priestor, ktorý sám serializátor syntetizoval okolo značiek. Používateľské hodnoty, existujúce textové uzly a CDATA sekcie prechádzajú nedotknuté, takže " indented" ostáva odsadené a zámerné prázdne pole ostáva prázdne

Prečo sa emoji vráti ako iný znak?

Emoji sa vrátilo zle, lebo Windows wchar_t je široké 16 bitov a dve dekódovacie cesty ukladali plnú Unicode skalárnu hodnotu do jediného wchar_t. Urobil to UTF-8 stream dekodér aj parser numerických znakových referencií ako &#x1F642;. U+1F642, mierne sa usmievajúca tvárička, sa do 16 bitov nevošlo, takže horné bity odpadli a namiesto nich sa objavilo U+F642: kódový bod v Private Use Area, ktorý väčšina písiem vykreslí ako štvorček alebo nič

Formulový serializátor mal opačný problém. Filtroval znaky po jednom wchar_t, videl dve surrogate kódové jednotky, ktoré sú v izolácii neplatné, a obe zahodil, takže emoji zmizlo z formulového balíka úplne. Vo v3.125.2 spotrebuje dekodér každú skalárnu hodnotu celú a emituje riadny surrogate pár. Keď zostáva len jeden výstupný slot, drží low surrogate vo zvisu a nehlási koniec streamu, kým je tá jednotka ešte v bufferi. UTF-8 sekvencia rozdelená cez čítacie bloky sa prenesie do ďalšieho čítania namiesto zahodenia. Formulový exporter teraz drží platné surrogate páry spolu a numerické znakové referencie tiež vyrábajú správne páry

Diagram práce so surrogátmi v PDFium Component, kde U+1F642 prichádza ako UTF-16 pár D83D DE42 a dve chybné cesty ho pokazia: 16-bitové dekodéry wchar_t useknú skalár na U+F642 v private use area, zatiaľ čo formulový serializátor odfiltruje osamelé surrogáty a emoji zahodí úplne
Windows wchar_t je široké 16 bitov, takže skalár, ktorý potrebuje surrogate pár, buď stratil svoju hornú polovicu, alebo zmizol z balíka, kým sa obe cesty nenaučili držať páry spolu

Testové dáta Latin-1 nič z toho nikdy neukážu, takže každý XFA round-trip test potrebuje aspoň jeden znak zo supplementary plane

Single-stream XFA a zlyhania uloženia, ktoré nikto nevidel

Single-stream XFA dokument stratil úpravy, pretože natívny pomocník ukladania odmietol ten úložný layout a jeho volajúci zlyhanie ignorovali. ISO 32000-1 §12.7.8 dovoľuje, aby položka /XFA slovníka interaktívneho formulára bola buď poľom mien balíkov a streamov, alebo jediným streamom držiacim celý XDP dokument. Polia balíkov sú bežný prípad, ale jediné streamy sú úplne legálne a PDF uloženie sa dokončilo, ako keby sa nič nestalo, kým údaje formulára ostali na starých hodnotách

Od v3.125.2 obsluhuje V8 runtime podporovanú single-stream podmnožinu. Najprv vyexportuje oba živé balíky, datasets aj form, do staging oblasti a validuje ich a až potom vymení zodpovedajúce balíky v pôvodnom XDP. Ostatné balíky a deklarácie koreňového namespace sa uchovajú. Ak staging zlyhá, perzistentný XFA stream sa nikdy nedotkne a dokument si drží značku zmeny

XML komentáre a processing instructions potrebovali zvýšenú starostlivosť, lebo interný XML DOM ich zahadzuje. Vo v3.125.2 ich prítomnosť spravila uloženie rovno zlyhávajúcim namiesto tichej straty obsahu. v3.126.0 ich uchováva: pred parsovaním sa každý komentár alebo processing instruction vymení za marker postavený z prefixu, ktorý sa v pôvodnom texte nikde nevyskytuje. Po vymenení živých balíkov sa musí každý marker objaviť presne raz, než sa obnoví pôvodný token a stream sa zapíše. Tokeny mimo vymenených balíkov si preto držia text aj poradie, vrátane tokenov v prologu, v template a v ďalších balíkoch

Niektoré vstupy sa odmietajú aj naďalej zámerne a každé odmietnutie je explicitné zlyhanie uloženia:

  • Komentáre alebo processing instructions vo vnútri živých balíkov datasets alebo form, keďže ich pôvodné pozície sa nedajú namapovať do čerstvo vyexportovaného obsahu
  • Deklarácie DTD a podpisy XMLDSig, keďže prepísanie XDP nedokáže udržať XML podpis platný
  • Neplatné kódovanie UTF-8 alebo UTF-16, neúplné značky, neplatné znakové referencie, neznáme entity a pokazené processing instructions, ktoré sa odmietajú namiesto tichej opravy
Pipeline uloženia single-stream XFA v PDFium Component, kde živé balíky datasets a form sa vyexportujú do stagingu, validujú a potom vymenia vo vnútri pôvodného XDP s komentármi uchovanými cez markery, zatiaľ čo zlyhania stagingu a vstupy ako DTD alebo XMLDSig odmietnu uloženie explicitne
Stagingový export sa validuje, než sa čokoľvek vymení, takže zlyhané uloženie nechá perzistentný XFA stream nedotknutý a dokument si drží značku zmeny

Výstup single-stream je UTF-8 a zachováva XML model obsahu, nie pôvodné bajtové rozloženie ani deklaráciu kódovania

Posledný defekt sedel pod XFA. Natívny súborový zapisovač bufferuje výstup v 32 KB blokoch a záverečný čiastočný blok spláchol až vo svojom deštruktore, po tom, čo dokumentový zapisovač už ohlásil úspech. Chyba disku zaplneného do posledna alebo I/O chyba na tom poslednom bloku bola pre volajúceho neviditeľná. Od v3.125.2 vo V8 runtime a v3.125.3 v obyčajnom runtime je ten záverečný flush súčasťou výsledku uloženia a XFA značka zmeny sa čistí až po skutočnom úspechu. Na strane Delphi zapisuje TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean do dočasného súboru vedľa cieľa a presunie ho na miesto, až keď uloženie vráti True, takže zlyhané uloženie nechá predchádzajúci súbor nedotknutý

Prečo sa dynamický XFA formulár otvorí s menším počtom strán?

Dynamický XFA formulár sa otvorí s menším počtom strán, keď jeho koreňový subformulár nedeklaruje restoreState="auto" a to je rozhodnutie pri tvorbe formulára, nie defekt PDFium Component. V XFA 3.3 má restoreState na koreňovom subformulári predvolené manual. Pod manual obnovuje XFA procesor z uloženého formulového balíka len obmedzený stav a ostatné necháva skriptom autora. Uložené hodnoty polí aj počty inštancií opakovaných subformulárov sa stále vrátia, ale geometrické vlastnosti nastavené za behu nie

Prípad, ktorý to odhalil, bol trojstranový formulár, ktorého skript narastil subformulár na h="450pt". Uložený formulový balík držal novú výšku, hodnoty aj počty inštancií. Pri opätovnom otvorení sa však layout prestaval z výšok templatu a formulár sa prevalil na dve strany. Runtime mal pravdu: template nikdy nežiadal automatickú obnovu. Deklarovanie na koreňovom subformulári opraví opätovné otvorenie:

<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">
      <!-- polia; skripty môžu meniť h alebo pridávať inštancie za behu -->
    </subform>
  </subform>
</template>

Ak nevlastníte template, nepatlajte okolo neho v prehliadači: formulár, ktorý sa spolieha na režim manual, očakáva, že si stav prestavia jeho vlastné skripty. Živá repaginácia počas písania používateľa je osobitná téma, pokrytá v tom, ako PDFium Component sleduje počty strán dynamického XFA a presunuté polia

Ako verifikujete uloženie XFA v Delphi?

Jediná spoľahlivá kontrola uloženia XFA je znovu otvoriť uložený súbor v čerstvej inštancii TPdf a prečítať uložené dáta späť. TPdf.GetXfaDatasets vracia balík datasets tak, ako je uložený v dokumente, nie živý XFA dátový model, takže volanie pred uložením ukáže staré hodnoty. Po opätovnom otvorení ukáže presne to, čo bolo zapísané. Single-stream dokument nemá osobitne pomenované balíky: PDFium reportuje celý XDP ako jeden balík s prázdnym menom, takže GetXfaPacketByName('datasets') aj GetXfaDatasets nevrátia nič a fallback číta kompletný stream cez 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;          // layout poľa balíkov
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // jediný stream: jeden nepomenovaný balík
      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);  // uložený výstup XDP je UTF-8
  finally
    Pdf.Free;
  end;
end;

Ukladacia rutina potom commitne nedokončenú úpravu, skontroluje výsledok SaveAs a porovná znovu otvorenú hodnotu. TPdf.ClearFormFieldFocus zabije fokus formulára, čo je moment, keď PDFium commitne editačný buffer zaostreného poľa. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean naplní zaostrené pole programovo, ale spolieha sa na fokus, ktorý wrapper sleduje cez FocusFormField, ktorý prechádza anotáciami widgetov. Dynamická strana XFA ich normálne nemá, takže tam text obyčajne prichádza cez vstup z klávesnice v TPdfView a funkcia vracia False, keď žiadne sledované pole nemá 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
  // Voliteľné skriptované vyplnenie; False znamená, že žiadne sledované pole nemá fokus
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // commitnúť editačný buffer
  if not Pdf.SaveAs(FileName) then      // vrátane záverečného flushu (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;

Test podreťazca berte ako smoke test. Prázdny element sa môže serializovať ako <Tag/>, atribúty sa môžu objaviť na dátových elementoch a escapovanie nad rámec & a < je voľba serializátora. Pre produkčné kontroly načítajte znovu otvorené XML skutočným XML parserom a porovnajte textový uzol viazaného dátového elementu. Kontrolu pustite aj dvakrát za sebou, lebo defekt nového riadku ukázal celý tvar až v druhej generácii

Rýchly prehľad: kontrolný zoznam vernosti ukladania XFA

  • Nasádzajte pdfium.v8.dll z v3.125.2 a novšej pre formuláre XFA a v3.125.3 a novšiu pre obyčajné pdfium.dll, aby oprava záverečného zápisu bola v oboch
  • Zamerajte LibraryName na plnú cestu a nastavte EnableV8Engine na True; chýbajúca cesta zlyhá namiesto načítania inej kópie
  • Potvrďte TPdf.XFA a TPdf.XfaRuntimeAvailable po otvorení dokumentu
  • Zavolajte ClearFormFieldFocus pred SaveAs, aby sa zaostrené pole commitlo
  • Nikdy neignorujte Boolean výsledok SaveAs; výsledok False nechá predchádzajúci súbor na mieste
  • Verifikujte znovuotvorením v novom TPdf a čítaním GetXfaDatasets s fallbackom na GetXfaFormPackets pre single-stream XFA
  • Testujte s prázdnymi hodnotami, úvodnými medzerami, viacriadkovým textom, & a znakom zo supplementary plane, cez dve generácie uloženia
  • Čakajte explicitné zlyhania uloženia pri DTD, XMLDSig a komentároch vo vnútri živých balíkov single-stream XFA
  • Ak dynamický formulár stratí geometriu z behu pri opätovnom otvorení, skontrolujte koreňový subformulár na restoreState="auto", skôr než začnete podozrievať knižnicu

Štruktúru callbackov, ktorú XFA runtime očakáva od hostiteľskej aplikácie, popisuje FPDF_FORMFILLINFO verzia 2 a XFA ABI v Delphi. V8 runtime, wrapper pre Delphi a C++Builder aj viewer control sú všetko súčasti PDFium Component pre Delphi a C++Builder, ktorý zahŕňa obidva Windows runtimy pre Win32 a Win64