Techninis straipsnis

PDFium Component XFA išsaugojimas ir restoreState

PDFium Component redaguotas XFA formos reikšmes išsaugoja tiksliai, per išsaugojimą ir atvėrimą, kai lekia v3.125.2 ar naujesnėje pristatytoji Windows V8 vykdymo aplinka pdfium.v8.dll. Senesnės vykdymo aplinkos prie lauko reikšmių prikimšdavo eilučių lūžių, emoji nukirpdavo iki nesusijusio BMP simbolio, tyliai praleisdavo vieno srauto XFA išsaugojimus ir galėdavo praryti nepavykusį galutinį rašymą. Vienas atvėrimo simptomas apskritai nėra bibliotekos defektas: dinaminė forma, kurios šakninis poformis be restoreState="auto", savo išdėstymą atkuria iš šablono

Visos šio dalyko klaidų ataskaitos atrodė vienodai. Klientas užpildo XFA deklaracijos formą Delphi peržiūrovoje, išsaugoja, atveria dar kartą, ir kažkas kiek nuklysta. Tuščias komentarų laukas dabar laiko vieną tuščią eilutę, o po antro išsaugojimo – dvi. Vardas, įrašytas su emoji, grįžta su privataus naudojimo rašmeniu. Niekas negauna klaidos – būtent todėl šios klaidos brangios: išsinešiojimas pasirodo po kelių savaičių kažkieno kito eksporte

Kas suyra, kai XFA forma išsaugojama ir atveriama?

Keturi atskiri defektai savojoje XFA išsaugojimo atkarpoje sukėlė reikšmių išsinešiojimą, ir kiekvienas slėpėsi už sėkmingai atrodančio išsaugojimo. Du atkeliavo iš serijizacijos, vienas – iš vieno srauto saugojimo išdėstymo, o vienas – iš pačio PDF rašytojo. Lentelė kiekvieną simptomą susieja su priežastimi ir su leidimu, kuriame PDFium Component tai sutvarkė

Simptomas po atvėrimoPriežastisSutvarkyta
Tuščias laukas laiko eilutės lūžį; reikšmės prie kiekvieno išsaugojimo paauga eiluteAbu XFA rašytojai po atveriamųjų žymių įterpdavo išdėstymo eilučių lūžiusv3.125.2, pdfium.v8.dll
U+1F642 grįžta kaip U+F642, arba emoji dingo iš formos paketo16 bitų wchar_t nukirpimas dekoduojant; pakaitinių filtravimas formos serijizatoriujev3.125.2, pdfium.v8.dll
Vieno srauto XFA dokumento redagavimai tiesiog dingstaSavasis išsaugojimas atmetė srauto išdėstymą, bet grąžinama reikšmė buvo ignoruojamav3.125.2; komentarai ir apdorojimo instrukcijos išlaikomi nuo v3.126.0
Nukirptas failas, nors išsaugojimas pranešė sėkmęGalutinis sukaupptasis rašymas žlugo, kai rašytojas jau buvo grąžinęs sėkmęv3.125.2 V8 vykdymo aplinka; v3.125.3 paprastoji pdfium.dll
Trijų puslapių dinaminė forma atveriama kaip dviejųŠakninis poformis neprašo restoreState="auto"Formos rašymas, ne bibliotekos defektas

Ankstesni apibendrinimai darė išvadą, kad XFA lauko redagavimų su PDFium apskritai negalima išsaugoti, ir tai buvo tiesa tų laikų vykdymo aplinkoms. Naujesnioji V8 vykdymo aplinka XFA reikšmes išsaugoja natyviai, tad gyvoje formoje padarytas redagavimas pasiekia išsaugotąjį datasets paketą be jokios paketų chirurgijos iš jūsų pusės

Kurioji PDFium vykdymo aplinka išsaugoja XFA reikšmes?

XFA išsaugojimo tikslumas priklauso nuo savosios DLL, o ne nuo Delphi aplinkkalbės, tad pirmoji patikra – kurią vykdymo aplinką jūsų procesas iš tikrųjų pakrauna. PDFium Component kiekvienai architektūrai pristato du Windows variantus: paprastąjį pdfium.dll, sukurtą be V8 ir XFA, ir pdfium.v8.dll, nešantį JavaScript variklį ir XFA formų vykdymo aplinką. Tik pdfium.v8.dll gali paleisti XFA formą, tad kiekviena čia aprašyta XFA pataisa gyvena joje – pradedant perstatytomis Win32 ir Win64 V8 bibliotekomis v3.125.2

Galutinio rašymo pataisa – bendrojo PDF rašytojo kodas, tad svarbi ir paprastiems dokumentams. v3.125.3 perstatė paprastąsias pdfium.dll bibliotekas, kad jos neštų tą patį remontą. Bendras šaltinis – ne bendro elgesio įrodymas: kol dvejetainis failas nepertestas, senoji DLL laiko senąją klaidą

Antri spąstai tupėjo krautuve. Iki v3.125.2 EnableV8Engine nustatymas į True priversetų aplinkkalbę rinktis numatytąjį pdfium.v8.dll vardą ir ignoruoti pilnąjį kelią LibraryName viduje. Taikomoji programa, rodžiusi į šviežiai išdėstytą vykdymo aplinką, galėjo toliau krauti senesnę kopiją iš kito aplanko. Nuo v3.125.2 LibraryName, turintis katalogą, abiejuose variklio režimuose renkasi būtent tą failą, o dingęs kelias žlunga vietoj atsitraukimo į kitą pridėtąją biblioteką

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // Katalogas LibraryName viduje prisega šį tikslųjį failą (v3.125.2 ir vėliau);
  // jei failo nėra, krovimas kelia išimtį vietoj atsitraukimo
{$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;  // žlugti starto metu, o ne pirmojo išsaugojimo metu
end;

Atvėrus dokumentą, TPdf.XFA sako, jog faile yra XFA, o TPdf.XfaRuntimeAvailable – jog pakrautoji DLL ją iš tikrųjų geba vykdyti. Jei dar reikia atskirti statines ir dinamines formas, TPdf.FormType grąžina ftXfaFull arba ftXfaForeground; straipsnis XFA formų aptikimas ir XFA paketų ištraukimas Delphi tą žvalgymą dengia detaliai

Kodėl išsaugotieji XFA laukai prisidirba papildomų eilučių lūžių?

Išsaugotieji XFA laukai prisidirbdavo eilučių lūžių, nes abu savosieji XFA rašytojai – bendrasis XML elementų rašytojas ir formos paketo serijizatorius – savo išvestį gražindavo spausdindami su eilutės lūžiu po atveriamųjų žymių. Daugumoje XML tas tarpas – kosmetika. XFA duomenyse – ne: kai datasets paketas vėl išanalizuojamas, tekstas tarp <Comments> ir </Comments> yra lauko reikšmė, kartu su eilutės lūžiu. Tuščias laukas todėl atverdavosi laikydamas vieną LF, o kiekvienas tolesnis išsaugojimo ir atvėrimo ratas galėjo pridėti dar vieną

PDFium Component XFA išsaugojimo ciklo diagrama, kur rašytojas po atveriamųjų žymių prideda eilutės lūžį, atvėrusysis analizatorius LF tarp Comments žymių skaito kaip lauko reikšmę, ir kiekvienas tolesnis išsaugojimas prideda dar vieną eilutės lūžį, kol v3.125.2 pašalina tik serijizatoriaus susintetintą tarpą
Vienas išsaugojimo ir atvėrimo ratas pasodina pirmąjį eilutės lūžį, ir kiekvienas tolesnis ratas prideda dar vieną – todėl išsinešiojimas visą savo formą parodė tik antroje kartoje

Akivaizdusis remontas – reikšmių apkirpimas kraunant – būtų neteisingas. Naudotojai į XFA laukus įrašo priekinius ir galinius tarpus bei sąmoningą kelių eilučių tekstą, ir adreso blokas ar fiksuoto pločio kodas turi išgyventi baitą į baitą. v3.125.2 pataisa todėl šalina tik tą tarpą, kurį pats serijizatorius susintetino aplink žymes. Naudotojų reikšmės, esamieji teksto mazgai ir CDATA sekcijos praeina neliečiami, tad " indented" lieka įtrauktas, o sąmoningai tuščias laukas lieka tuščias

Kodėl emoji grįžta kaip kitas simbolis?

Emoji grįždavo sugedęs, nes Windows wchar_t yra 16 bitų pločio, o dvi dekodavimo atkarpos visą Unicode skaliarą susidėdavo į vieną wchar_t. Taip darydavo ir UTF-8 srauto dekoderis, ir skaitinių simbolių nuorodų, tokių kaip &#x1F642;, analizatorius. U+1F642 – šiek tiek besišypsantis veidas – į 16 bitų netelpa, tad aukštieji bitai nukrisdavo, ir vietoj jo pasirodydavo U+F642: kodo taškas Private Use Area srityje, kurį dauguma šriftų atvaizduoja kaip langelį arba nieko

Formos serijizatorius turėjo priešingą problemą. Jis simbolius filtruodavo po vieną wchar_t, matydavo dvi izoliuotai neteisėtas pakaitines kodo grupes ir abi išmesdavo, tad emoji dingsdavo iš formos paketo visai. v3.125.2 dekoderis kiekvieną skaliaro reikšmę suvartoja pilnai ir išduoda teisingą pakaitinių porą. Kai belieka viena išvesties vieta, žemąją pakaitinę laiko laukiančią ir srauto pabaigos nepraneša, kol ta grupė dar podėlyje. UTF-8 seka, perskelta tarp skaitymo blokų, perduodama į kitą skaitymą vietoj išmetimo. Formos eksportuotojas dabar teisėtas pakaitines poras laiko kartu, o skaitinės simbolių nuorodos duoda teisingas poras taip pat

PDFium Component pakaitinių porų tvarkymo diagrama, kur U+1F642 atkeliauja kaip UTF-16 pora D83D DE42, o du defektų keliai jį sugadina: 16 bitų wchar_t dekoderiai skaliarą nukerpa iki U+F642 private use area srityje, o formos serijizatorius filtruoja vienišas pakaitines ir išmeta emoji visai
Windows wchar_t yra 16 bitų pločio, tad skaliaras, reikalaujantis pakaitinės poros, arba prarasdavo aukštąją pusę, arba dingsdavo iš paketo, kol abu keliai išmoko laikyti poras kartu

Latin-1 bandomieji duomenys nieko iš to niekada neparodo, tad kiekvienam XFA apvalinimo testui reikia bent vieno papildomos plokštumos simbolio

Vieno srauto XFA ir išsaugojimo nesėkmės, kurių niekas nematė

Vieno srauto XFA dokumentas prarasdavo savo redagavimus, nes savasis išsaugojimo pagalbininkas atmesdavo tą saugojimo išdėstymą, o jo kvientėjas nesėkmės nepastebėdavo. ISO 32000-1 §12.7.8 leidžia sąveikios formos žodyno /XFA įrašą – arba paketų vardų ir srautų masyvą, arba vieną srautą, laikantį visą XDP dokumentą. Paketų masyvai – dažnasis atvejis, bet vieni srautai visiškai teisėti, ir PDF išsaugojimas pasibaigdavo tarsi nieko neįvyko, kol formos duomenys likdavo senosiose reikšmėse

Nuo v3.125.2 V8 vykdymo aplinka tvarko palaikomąją vieno srauto aibę. Ji pirmiau abu gyvuosius paketus – datasets ir form – išeksportuoja į paruošimo zoną ir patikrina, ir tik tada pakeičia atitinkamus paketus originaliajame XDP. Kiti paketai ir šakninės vardų tarpų deklaracijos išlaikomi. Jei paruošimas žlunga, liekamąją XFA srautą niekas neliečia, o dokumentas išlaiko savo keitimo žymę

XML komentarai ir apdorojimo instrukcijos reikalavo ypatingos priežiūros, nes vidinis XML DOM juos išmeta. v3.125.2 jų buvimas verčia išsaugojimą žlugti tiesiai, vietoj tylio turinio praradimo. v3.126.0 juos išlaiko: prieš analizavimą kiekvienas komentaras ar apdorojimo instrukcija apkeičiamas žymekliu, pastatytu iš priešdėlio, kurio originaliame tekste niekur nėra. Kai gyvieji paketai pakeisti, kiekvienas žymeklis turi pasirodyti tiksliai kartą, dar prieš atstatant originaliąją leksemą ir rašant srautą. Leksemos už pakeistųjų paketų ribų todėl išlaiko savo tekstą ir tvarką, įskaitant leksemas prologe, šablone ir kituose paketuose

Kai kurios įvestys vis dar atmetamos tyčia, ir kiekvienas atmetimas – aiški išsaugojimo nesėkmė:

  • Komentarai arba apdorojimo instrukcijos gyvųjų datasets arba form paketų viduje, nes jų originaliųjų pozicijų neįmanoma susieti su šviežiai eksportuotu turiniu
  • DTD deklaracijos ir XMLDSig parašai, nes XDP perrašymas negali išlaikyti XML parašo teisėtu
  • Neteisėta UTF-8 arba UTF-16 koduotė, neišbaigtos žymės, neteisėtos simbolių nuorodos, nežinomi objektai ir iškraipytos apdorojimo instrukcijos – atmetamos vietoj tylio remonto
PDFium Component vieno srauto XFA išsaugojimo konvejeris, kur gyvieji datasets ir form paketai išeksportuojami į paruošimo zoną, patikrinami, tada pakeičiami originaliojo XDP viduje, išlaikant komentarus per žymeklius, o paruošimo žlugimai ir įvestys, tokios kaip DTD ar XMLDSig, aiškiai atmeta išsaugojimą
Paruoštasis eksportas patikrinamas dar prieš bet ką keičiant, tad nepavykęs išsaugojimas palieka neliečiamąjį XFA srautą, o dokumentas išlaiko savo keitimo žymę

Vieno srauto išvestis – UTF-8 ir išlaiko XML turinio modelį, o ne originalųjį baitų išdėstymą ar koduotės deklaraciją

Paskutinis defektas tupėjo žemiau XFA. Savasis failų rašytojas išvestį kaupia 32 KB blokais, o galutinį nepilnąjį bloką nuplaudavo tik savo destruktoriuje, kai dokumento rašytojas jau buvo pranešęs sėkmę. Disko pilnumo ar I/O klaida tame paskutiniajame bloke kvientėjui buvo nematoma. Nuo v3.125.2 V8 vykdymo aplinkoje ir v3.125.3 paprastojoje ta galutinė nuplauka yra išsaugojimo rezultato dalis, o XFA keitimo žymė nuimama tik po tikros sėkmės. Delphi pusėje TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean rašo į laikinąjį failą šalia tikslo ir perkelia jį į vietą tik tada, kai išsaugojimas grąžina True, tad nesėkmingas išsaugojimas ankstesnįjį failą palieka nepaliestą

Kodėl dinaminė XFA forma atveriama su mažiau puslapių?

Dinaminė XFA forma atveriama su mažiau puslapių, kai jos šakninis poformis nedeklaruoja restoreState="auto", ir tai formos rašymo sprendimas, o ne PDFium Component defektas. XFA 3.3 restoreState ant šakninio poformio pagal numatymą yra manual. Esant manual, XFA procesorius iš išsaugotojo formos paketo atkuria tik ribotą būseną, o likusį palieka autoriaus scenarijams. Išsaugotosios laukų reikšmės ir kartojamųjų poformių egzempliorių skaičiai vis tiek grįžta, bet vykdymo metu nustatytosios geometrinės savybės – ne

Atvejis, atskleidęs tai, buvo trijų puslapių forma, kurios scenarijus poformį paaugino iki h="450pt". Išsaugotasis formos paketas laikė naująjį aukštį, reikšmes ir egzempliorių skaičius. Bet atvėrus išdėstymas buvo atstatytas iš šablono aukščių, ir forma persisodino į du puslapius. Vykdymo aplinka buvo teisi: šablonas niekada neprašė automatinio atstatymo. Jo deklaravimas ant šakninio poformio atvėrimą sutvarko:

<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">
      <!-- laukai; scenarijai vykdymo metu gali keisti h arba pridėti egzempliorių -->
    </subform>
  </subform>
</template>

Jei šablono nesavaldote, negydykite jo aplink peržiūrovoje: forma, pasitikinti manual režimu, tikisi savų scenarijų, atstatysiančių būseną. Gyvasis persiskirstymas, kol naudotojas rašo, – atskira tema, dengiama straipsnyje kaip PDFium Component seka dinaminio XFA puslapių skaičių ir perkeltus laukus

Kaip Delphi patikrinti XFA išsaugojimą?

Vienintelė patikima XFA išsaugojimo patikra – išsaugotąjį failą atverti šviežiame TPdf egzemplioriuje ir perskaityti saugomuosius duomenis atgal. TPdf.GetXfaDatasets grąžina datasets paketą tokį, koks jis saugomas dokumente, o ne gyvąjį XFA duomenų modelį, tad pakvietus prieš išsaugojimą matysite senąsias reikšmes. Po atvėrimo jis rodo tiksliai tai, kas parašyta. Vieno srauto dokumentas atskirai pavadintų paketų neturi: PDFium visą XDP praneša kaip vieną paketą su tuščiu vardu, tad GetXfaPacketByName('datasets') ir GetXfaDatasets negrąžina nieko, o atsarginis kelias pilnąjį srautą skaito per 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;          // paketų masyvo išdėstymas
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // vienas srautas: vienas bevardis paketas
      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);  // išsaugotoji XDP išvestis – UTF-8
  finally
    Pdf.Free;
  end;
end;

Tada išsaugojimo procedūra patvirtina laukiantįjį redagavimą, tikrina SaveAs rezultatą ir lygina atvėrusiąją reikšmę. TPdf.ClearFormFieldFocus numarina formos fokusą – tai akimirka, kai PDFium patvirtina sufokusuoto lauko redagavimo buferį. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean sufokusuotąjį lauką užpildo programiškai, bet remiasi fokusu, kurį aplinkkalbė seka per FocusFormField, apeinantį valdiklių anotacijas. Dinaminis XFA puslapis paprastai jų neturi, tad ten tekstas paprastai atkeliauja per klaviatūros įvestį TPdfView viduje, o funkcija grąžina False, kai joks sekamas laukas fokusą neturi

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
  // Pasirinktinis scenarijinis užpildymas; False reiškia, jog joks sekamas laukas fokusą neturi
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // patvirtinti redagavimo buferį
  if not Pdf.SaveAs(FileName) then      // įskaita galutinę nuplauką (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;

Poalletės testą laikykite dūmų testu. Tuščias elementas gali būti serijizuotas kaip <Tag/>, atributai gali pasirodyti ant duomenų elementų, o kaušeliai už & ir < ribų – serijizatoriaus pasirinkimas. Gamybinėms patikroms atvėrusįjį XML pakraukite tikru XML analizatoriumi ir lyginkite susietojo duomenų elemento teksto mazgą. Patikrą paleiskite ir du kartus iš eilės, nes eilučių lūžių defektas visą savo formą parodė tik antroje kartoje

Trumpa atmintinė: XFA išsaugojimo tikslumo kontrolinis sąrašas

  • Išdėstykite pdfium.v8.dll iš v3.125.2 ar naujesnės XFA formoms, o v3.125.3 ar naujesnę – paprastajai pdfium.dll, kad galutinio rašymo pataisa būtų abiejose
  • LibraryName nukreipkite į pilnąjį kelią ir nustatykite EnableV8Engine į True; dingęs kelias žlunga vietoj kitos kopijos krovimo
  • Patvirtinkite TPdf.XFA ir TPdf.XfaRuntimeAvailable po dokumento atvėrimo
  • Kvieskite ClearFormFieldFocus prieš SaveAs, kad sufokusuotasis laukas būtų patvirtintas
  • Niekada neignoruokite SaveAs Boolean rezultato; False rezultatas ankstesnįjį failą palieka vietoje
  • Tikrinkite atverdami naujame TPdf ir skaitydami GetXfaDatasets, vieno srauto XFA atveju atsitraukdami į GetXfaFormPackets
  • Bandykite su tuščiomis reikšmėmis, priekiniais tarpais, kelių eilučių tekstu, & ir papildomos plokštumos simboliu, per dvi išsaugojimo kartas
  • Tikėkitės aiškių išsaugojimo nesėkmių dėl DTD, XMLDSig ir komentarų gyvųjų vieno srauto XFA paketų viduje
  • Jei dinaminė forma atvėrusi praranda vykdymo metu nustatytą geometriją, patikrinkite šakninį poformį dėl restoreState="auto", dar nepalejusi bibliotekos

Apie atgalinių kvietimų struktūrą, kurios iš hosto taikomosios programos tikisi XFA vykdymo aplinka, žiūrėkite FPDF_FORMFILLINFO versija 2 ir XFA ABI Delphi. V8 vykdymo aplinka, Delphi ir C++Builder aplinkkalbė ir peržiūrovos valdiklis – visos PDFium Component for Delphi and C++Builder dalys, apimančios abi Windows vykdymo aplinkas Win32 ir Win64