Műszaki cikk

PDFium Component XFA mentés: sortörés, emoji, restoreState

A PDFium Component akkor menti pontosan a szerkesztett XFA űrlapértékeket, mentésen és újrameneten át, ha a v3.125.2-től szállított Windows V8 runtime, a pdfium.v8.dll fut. A régebbi runtimeok soremeléseket csúsztattak a mezőértékekbe, az emojikat egy tőlük független BMP karakterre csonkították, néma csendben kihagyták az egyetlen streames XFA mentéseket, és elnyelhették az elbukott záró írást. Az újramenet tüneteinek egyike pedig egyáltalán nem library hiba: az a dinamikus űrlap, aminek a gyökér subformja nem kér restoreState="auto"-t, a sablonból építi újra a layoutját

A hibajelentések ebben mind ugyanúgy néztek ki. Egy ügyfél kitölt egy XFA igénylő űrlapot egy Delphi viewerben, ment, újra megnyitja, és valami kicsit csúszik. Egy üres megjegyzésmező mostantól egy üres sort tartalmaz, egy második mentés után kettőt. Egy emojival beírt név private-use glifusszal jön vissza. Senki nem kap hibát, és pont ez teszi drágává ezeket a hibákat: a csúszás hetekkel később, valaki más exportjában bukik fel

Mi romlik el, amikor egy XFA űrlapot mentesz és újra megnyitsz?

Négy különálló hiba a natív XFA mentési úton okozta az értékcsúszást, és mindegyik sikeresnek tűnő mentés mögé bújt. Kettő a szerializációból jött, egy az egyetlen streames tárolási layoutból, egy pedig magából a PDF írótól. A táblázat tünetenként rendeli hozzá az okot és azt a kiadást, amiben a PDFium Component megjavította

Tünet újramenet utánOkJavítva
Üres mező soremelést tart; az értékek mentésenként egy soremeléssel nőnekMindkét XFA író layout soremelést szúrt be a nyitó tagek utánv3.125.2, pdfium.v8.dll
Az U+1F642 U+F642-ként jön vissza, vagy az emoji eltűnik a form packetből16 bites wchar_t csonkítás a dekódolásban; surrogate szűrés a form szerializátorbanv3.125.2, pdfium.v8.dll
Egyetlen streames XFA dokumentum szerkesztései egyszerűen eltűnnekA natív mentés elutasította a stream layoutot, de a visszatérési értéket ignoráltákv3.125.2; megjegyzések és feldolgozási utasítások megőrzése v3.126.0 óta
Csonka fájl, pedig a mentés sikert jelentettA záró pufferelt írás hibázott, miután az író már sikert jelzettv3.125.2 V8 runtime; v3.125.3 rendes pdfium.dll
Háromoldalas dinamikus űrlap két oldalasként nyílik újraA gyökér subform nem kér restoreState="auto"-tŰrlapszerkesztés, nem library hiba

A korábbi írások azt a következtetést vonták le, hogy XFA mezőszerkesztéseket PDFiummal egyáltalán nem lehet perzisztálni, ami a korabeli runtimeokra igaz volt. Az újabb V8 runtime natívan menti az XFA értékeket, így az élő űrlapban elvégzett szerkesztés packetműtét nélkül eljut a mentett datasets packetbe

Melyik PDFium runtime menti az XFA értékeket?

Az XFA mentés hűsége a natív DLL-en múlik, nem a Delphi wrapperen, ezért az első ellenőrzés az, hogy a folyamatod valójában melyik runtimeot töltötte. A PDFium Component architektúránként két Windows buildet szállít: a rendes pdfium.dll-t, ami V8 és XFA nélkül készül, és a pdfium.v8.dll-t, ami hordozza a JavaScript engineet és az XFA űrlap runtimeot. Csak a pdfium.v8.dll képes XFA űrlapot futtatni, így az itt leírt minden XFA javítás ott lakik, kezdve a v3.125.2-ben újrafordított Win32 és Win64 V8 librarykkel

A záró írás javítása általános PDF író kód, tehát rendes dokumentumoknál is számít. A v3.125.3 újrafordította a rendes pdfium.dll libraryket, hogy hordozzák ugyanazt a javítást. A közös forrás nem bizonyíték a közös viselkedésre: amíg a binárist újra nem fordították, a régi DLL-ben lakik a régi hiba

Egy másik csapda a loaderben ült. v3.125.2 előtt az EnableV8Engine True-ra állítása azt okozta, hogy a binding a default pdfium.v8.dll nevet választotta, és ignorált egy teljes elérési utat a LibraryName-ben. Egy frissen kideployzott runtime-ra mutató alkalmazás nyugodtan tölthette tovább a régebbi másolatot egy másik mappából. v3.125.2 óta egy könyvtárat tartalmazó LibraryName pontosan azt a fájlt választja mindkét engine módban, és a hiányzó útvonal hibázik, nem esik vissza egy másik mellékelt libraryre

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // A LibraryName-ben lévő könyvtár rögzíti pontosan ezt a fájlt (v3.125.2 és később);
  // ha a fájl hiányzik, a betöltés kivételt dob, visszaesés helyett
{$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;  // hibázzon induláskor, ne az első mentésnél
end;

Dokumentum megnyitása után a TPdf.XFA megmondja, hogy a fájl tartalmaz XFA-t, a TPdf.XfaRuntimeAvailable pedig azt, hogy a betöltött DLL ténylegesen tudja-e futtatni. Ha a statikus és dinamikus űrlapokat is meg kell különböztetned, a TPdf.FormType ftXfaFull-t vagy ftXfaForeground-ot ad vissza; a XFA űrlapok felismerése és XFA packetek kinyerése Delphiben cikk részletesen lefedi azt a vizsgálatot

Miért kapnak plusz soremeléseket a mentett XFA mezők?

A mentett XFA mezők azért kaptak soremeléseket, mert mindkét natív XFA író, az általános XML elem író és a form packet szerializátor, pretty-printelte a kimenetét nyitó tag utáni soremeléssel. A legtöbb XML-ben ez a whitespace kozmetika. XFA adatban nem az: amikor a datasets packetet újra parse-olják, a <Comments> és a </Comments> közti szöveg a mezőérték, soremeléssel együtt. Egy üres mező ezért egyetlen LF-fel nyílt újra, és minden további mentés-újramenet ciklus tehetett mellé még egyet

PDFium Component XFA mentési ciklus ábra: az író soremelést szúr a nyitó tagek után, az újra megnyitott parse-oló a Comments tagek közti LF-et olvassa mezőértékként, és minden további mentés még egy soremelést fűz hozzá, amíg a v3.125.2 csak a szerializátor által szintetizált whitespace-t távolítja el
Egy mentés-újramenet ciklus ülteti el az első soremelést, és minden további kör még egyet ad hozzá, ezért a csúszás csak a második generáción mutatta meg a teljes alakját

A kézenfekvő javítás, a betöltéskori trim, rossz lenne. A felhasználók vezető szóközöket, záró szóközöket és szándékos többsoros szöveget írnak XFA mezőkbe, és egy címbloknak vagy fix szélességű kódnak bájtról bájtra kell túlélnie. A v3.125.2 javítás ezért csak azt a whitespace-t távolítja el, amit maga a szerializátor szintetizált a tagek körül. A felhasználói értékek, a meglévő szövegcsomópontok és a CDATA szakaszok érintetlenül mennek át, így a " indented" behúzott marad, és a szándékosan üres mező üres marad

Miért jön vissza az emoji másik karakterként?

Az emoji azért jött vissza rosszul, mert a Windows wchar_t-ja 16 bites széles, és két dekódolási út egy teljes Unicode skalárértéket tárolt egyetlen wchar_t-ban. Az UTF-8 stream dekóder és az olyan numerikus karakterhivatkozások parse-olója, mint az &#x1F642;, ugyanezt csinálta. Az U+1F642, a kissé mosolygó arc, nem fér 16 bitbe, így a magas bitek leestek, és U+F642 jelent meg helyette: egy kódpont a Private Use Areában, amit a legtöbb font dobozként vagy semmiként renderel

A form szerializátornál az ellenkező probléma volt. Egyszerre egy wchar_t-t szűrt, két olyan surrogate kódegységet látott, ami önmagában érvénytelen, és mindkettőt eldobta, így az emoji teljesen eltűnt a form packetből. A v3.125.2-ben a dekóder minden skalárértéket teljesen elfogyaszt, és egy szabályos surrogate párt ad ki. Ha már csak egy kimeneti hely van, a low surrogate-t várakoztatja, és nem jelent end-of-streamet, amíg az egység pufferelve van. Az olvasási blokkok határán kettéhasadt UTF-8 szekvencia a következő olvasásra továbbítódik, eldobás helyett. A form exportőr mostantól egyben tartja a szabályos surrogate párokat, és a numerikus karakterhivatkozások is szabályos párokat adnak

PDFium Component surrogate kezelés ábra: az U+1F642 a D83D DE42 UTF-16 párként érkezik, és két hibás út rongálja meg: a 16 bites wchar_t dekóderek a skalárt U+F642-re csonkítják a private use areában, a form szerializátor pedig kiszűri a magányos surrogate-okat, és az emoji teljesen eltűnik
A Windows wchar_t-ja 16 bites, így egy surrogate párt igénylő skalár vagy a magas felét veszítette el, vagy eltűnt a packetből, amíg mindkét út meg nem tanulta egyben tartani a párokat

Latin-1 tesztadaton mindez soha nem mutatkozik meg, ezért minden XFA round-trip tesztnek legalább egy supplementary-plane karakterre szüksége van

Egyetlen streames XFA és mentési hibák, amiket senki nem látott

Egyetlen streames XFA dokumentum azért vesztette el a szerkesztéseit, mert a natív mentési helper elutasította azt a tárolási layoutot, és a hívója ignorálta a hibát. Az ISO 32000-1 §12.7.8 megengedi, hogy az interaktív űrlap szótár /XFA bejegyzése packet nevek és streamek tömbje legyen, vagy egyetlen stream, ami a teljes XDP dokumentumot tartja. A packet tömb a gyakori eset, de az egyetlen streames forma teljesen legális, és a PDF mentés befejeződött, mintha semmi sem történt volna, miközben az űrlapadat a régi értékein maradt

v3.125.2 óta a V8 runtime kezeli a támogatott egyetlen streames részhalmazt. Előbb mindkét élő packetet, a datasets-et és a form-ot, egy staging területre exportálja, és validálja őket, és csak azután cseréli le az illeszkedő packeteket az eredeti XDP-ben. A többi packet és a gyökér namespace deklarációk megmaradnak. Ha a staging elbukik, a perzisztens XFA streamet soha nem érinti senki, és a dokumentum megtartja a módosításjelét

Az XML megjegyzések és feldolgozási utasítások külön gondoskodást igényeltek, mert a belső XML DOM eldobja őket. A v3.125.2-ben a jelenlétük a mentés nyílt bukását okozta, nem a tartalom néma elvesztését. A v3.126.0 megőrzi őket: parse előtt minden megjegyzést vagy feldolgozási utasítást felcserélnek egy markerre, amit egy olyan prefixből építenek, ami sehol nem fordul elő az eredeti szövegben. Miután az élő packetek le vannak cserélve, minden markernek pontosan egyszer kell megjelennie, mielőtt az eredeti token visszaállítódik, és a stream leíródik. A kicserélt packeteken kívüli tokenek így megtartják a szövegüket és a sorrendjüket, a prologban, a template-ben és a többi packetben lévő tokeneket is beleértve

Néhány bemenetet továbbra is szándékosan utasítanak el, és minden elutasítás explicit mentési hiba:

  • Megjegyzések vagy feldolgozási utasítások az élő datasets vagy form packeteken belül, mert az eredeti pozíciójuk nem képezhető le frissen exportált tartalomba
  • DTD deklarációk és XMLDSig aláírások, mert az XDP átírása nem tud egy XML aláírást érvényesen hagyni
  • Érvénytelen UTF-8 vagy UTF-16 kódolás, befejezetlen tagek, érvénytelen karakterhivatkozások, ismeretlen entitások és rossz formájú feldolgozási utasítások, amik néma javítás helyett elutasításra kerülnek
PDFium Component egyetlen streames XFA mentési pipeline: az élő datasets és form packetek stagingre exportálódnak, validálódnak, aztán az eredeti XDP-ben cserélődnek le, a megjegyzések markereken át megőrizve, miközben a staging hibák és a DTD-s vagy XMLDSig-s bemenetek explicit módon utasítják el a mentést
A stagelt export azelőtt validálódik, hogy bármi lecserélődne, így egy elbukott mentés érintetlenül hagyja a perzisztens XFA streamet, és a dokumentum megtartja a módosításjelét

Az egyetlen streames kimenet UTF-8, és az XML tartalommodellt őrzi meg, nem az eredeti bájt layoutot vagy a kódolási deklarációt

Utolsó hiba az XFA alatt ült. A natív fájlíró 32 KB-os blokkokban puffereli a kimenetet, és a végső részleges blokkot csak a destruktorában öblítette ki, miután a dokumentumíró már sikert jelentett. A legutolsó blokkon lévő disk-full vagy I/O hiba láthatatlan volt a hívó számára. v3.125.2-től a V8 runtimeban és v3.125.3-tól a rendes runtimeban az a végső öblítés a mentés eredményének része, és az XFA módosításjel csak valódi siker után törlődik. Delphi oldalon a TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean ideiglenes fájlba ír a cél mellett, és csak akkor helyezi a helyére, ha a mentés True-t ad vissza, így egy elbukott mentés érintetlenül hagyja az előző fájlt

Miért nyílik újra kevesebb oldallal egy dinamikus XFA űrlap?

Egy dinamikus XFA űrlap azért nyílik újra kevesebb oldallal, ha a gyökér subformja nem deklarál restoreState="auto"-t, és ez űrlaptervezési döntés, nem PDFium Component hiba. Az XFA 3.3-ban a restoreState a gyökér subformon defaultban manual. manual alatt az XFA processzor csak korlátozott állapotot állít vissza a mentett form packetből, a többit a szerző scripteire hagyja. A mentett mezőértékek és a repeating-subform instance számok így is visszajönnek, de a futásidőben beállított geometriai tulajdonságok nem

Az az eset leplezte le ezt, ahol egy háromoldalas űrlap scriptje egy subformot h="450pt"-re növesztett. A mentett form packet tartalmazta az új magasságot, az értékeket és az instance számokat. Újramenetséskor viszont a layout a sablon magasságaiból épült újra, és az űrlap két oldalra folyt szét. A runtime-nak igaza volt: a sablon soha nem kért automatikus visszaállítást. A gyökér subformon deklarálva megjavul az újramenet:

<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">
      <!-- mezők; a scriptek futásidőben megváltoztathatják az h-t vagy instance-okat adhatnak hozzá -->
    </subform>
  </subform>
</template>

Ha a sablon nem a tiéd, ne foltozgasd körül a viewerben: egy manual módra támaszkodó űrlap a saját scriptjeit várja el az állapot újjáépítéséhez. Az élő újravetítés, miközben a felhasználó gépel, külön téma, amit a hogyan követi a PDFium Component a dinamikus XFA oldalszámokat és elmozdult mezőket fed le

Hogyan ellenőrzöd az XFA mentést Delphiben?

Az egyetlen megbízható XFA mentés ellenőrzés az, hogy újra megnyitod a mentett fájlt egy friss TPdf példányban, és visszaolvasod a tárolt adatot. A TPdf.GetXfaDatasets a datasets packetet adja vissza úgy, ahogy a dokumentumban tárolódik, nem az élő XFA adatmodellt, tehát mentés előtt meghívva a régi értékeket mutatja. Újramenet után pontosan azt mutatja, ami leíródott. Egyetlen streames dokumentumnak nincsenek külön nevű packetjei: a PDFium a teljes XDP-t egy üres nevű packetként jelenti, így a GetXfaPacketByName('datasets') és a GetXfaDatasets semmit nem ad vissza, és a fallback a teljes streamet olvassa a GetXfaFormPackets-en át

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-tömb layout
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // egyetlen stream: egy névtelen 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);  // a mentett XDP kimenet UTF-8
  finally
    Pdf.Free;
  end;
end;

A mentő rutin aztán véglegesíti a függőben lévő szerkesztést, megnézi a SaveAs eredményét, és összehasonlítja az újraolvasott értéket. A TPdf.ClearFormFieldFocus lelövi az űrlapfókuszt, és ez az a pillanat, amikor a PDFium véglegesíti a fókuszált mező szerkesztési pufferjét. A TPdf.SetFocusedFormFieldText(const Value: WString): Boolean programozottan tölti a fókuszált mezőt, de egy fókuszra támaszkodik, amit a wrapper a FocusFormField-ön át követ, ami widget annotációkon jár végig. Egy dinamikus XFA oldalon normál módon nincs ilyen, ott a szöveg általában a TPdfView-beli billentyűzeten át érkezik, és a függvény akkor ad False-t, ha nincs követett mező fókuszban

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
  // Opcionális scriptelt kitöltés; a False azt jelenti, nincs követett mező fókuszban
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // véglegesíti a szerkesztési puffert
  if not Pdf.SaveAs(FileName) then      // tartalmazza a végső öblítést (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;

Az részkaraktersorozat tesztet kezeld smoke testnek. Egy üres elem <Tag/>-ként szerializálódhat, attribútumok megjelenhetnek adatelemeken, és az &-en és <-en túli escape-ölés a szerializátor döntése. Production ellenőrzésekhez töltsd be az újrameneti XML-t egy igazi XML parserrel, és hasonlítsd össze a bekötött adatelem szövegcsomópontját. Futtasd az ellenőrzést kétszer egymás után azért is, mert a soremelés hiba csak a második generáción mutatta meg a teljes alakját

Gyorsreferencia: XFA mentési hűség ellenőrzőlista

  • XFA űrlapokhoz deployold a v3.125.2-től vagy újabb pdfium.v8.dll-t, rendes dokumentumokhoz v3.125.3-tól vagy újabb pdfium.dll-t, hogy a záró írás javítása mindkettőben bent legyen
  • A LibraryName-t teljes elérési útra állítsd, az EnableV8Engine-t True-ra; hiányzó útvonal hibázik, másik másolatot nem tölt
  • Dokumentum megnyitása után ellenőrizd a TPdf.XFA-t és a TPdf.XfaRuntimeAvailable-t
  • Hívd a ClearFormFieldFocus-t a SaveAs előtt, hogy a fókuszált mező véglegesítődjön
  • Soha ne ignoráld a SaveAs Boolean eredményét; a False érintetlenül hagyja az előző fájlt
  • Ellenőrizz úgy, hogy újra megnyitod egy új TPdf-ben, és kiolvasod a GetXfaDatasets-t, egyetlen streames XFA-nál GetXfaFormPackets fallbackfel
  • Tesztelj üres értékekkel, vezető szóközökkel, többsoros szöveggel, &-tal és egy supplementary-plane karakterrel, két mentési generáción át
  • Számítsz explicit mentési hibákra DTD-knél, XMLDSignál és az egyetlen streames XFA élő packetjein belüli megjegyzéseknél
  • Ha egy dinamikus űrlap elveszti a futásidejű geometriáját újramenetséskor, nézd meg a gyökér subformon a restoreState="auto"-t, mielőtt a libraryt gyanúsítanád

Az XFA runtime által a host alkalmazástól elvárt callback struktúráról a FPDF_FORMFILLINFO version 2 és az XFA ABI Delphiben szól. A V8 runtime, a Delphi és C++Builder wrapper és a viewer vezérlő mind része annak a PDFium Component for Delphi and C++Builder terméknek, ami mindkét Windows runtimeot tartalmazza Win32-re és Win64-re