PDFium Component tallentaa muokatut XFA-lomakkeiden arvot täsmälleen, tallennuksen ja uudelleenavauksen yli, kun se ajaa Windowsin V8-ajonaikaisen pdfium.v8.dllin, joka toimitettiin versiossa v3.125.2 tai uudempana. Vanhemmat ajonaikaiset lisäsivät rivinvaihtoja kenttien arvoihin, katkoivat emojit toisiinsa liittymättömäksi BMP-merkiksi, ohittivat hiljaa yksistreamiset XFA-tallennukset ja saattoivat niellä epäonnistuneen lopullisen kirjoituksen. Yksi uudelleenavauksen oire ei ole lainkaan kirjaston vika: dynaaminen lomake, jonka juurialilomakkeelta puuttuu restoreState="auto", rakentaa layoutinsa mallipohjasta
Tämän bugiraportit näyttivät kaikki samalta. Asiakas täyttää XFA-korvauslomakkeen Delphi-katselimessa, tallentaa, avaa uudelleen, ja jotain on hieman pielessä. Tyhjä kommenttiruutu pitää nyt tyhjää riviä, ja toisen tallennuksen jälkeen se pitää kahta. Emojilla kirjoitettu nimi palaa yksityiskäytön glyyfin kanssa. Kukaan ei saa virhettä, mikä tekee näistä vioista kalliita: ajautuminen näkyy viikkoja myöhemmin jonkun muun viennissä
Mitä menee pieleen, kun XFA-lomake tallennetaan ja avataan uudelleen?
Neljä erillistä vikaa natiivissa XFA-tallennuspolussa aiheuttivat arvojen ajautumisen, ja jokainen piiloutui onnistuneen näyttävän tallennuksen taakse. Kaksi tuli sarjallistuksesta, yksi yksistreamisestä tallennusasettelusta ja yksi PDF-kirjoittajasta itsestään. Taulukko kartoittaa jokaisen oireen syyhynsä ja julkaisuun, jossa PDFium Component korjasi sen
| Oire uudelleenavauksen jälkeen | Syy | Korjattu versiossa |
|---|---|---|
| Tyhjä kenttä pitää rivinvaihtoa; arvot kasvavat rivinvaihdon tallennusta kohden | Molemmat XFA-kirjoittajat lisäsivät layout-rivinvaihtoja aloitustägien jälkeen | v3.125.2, pdfium.v8.dll |
| U+1F642 palaa muodossa U+F642, tai emoji katoaa lomakepaketista | 16-bittinen wchar_t-typistys dekoodauksessa; surrogate-suodatus lomakesarjallistajassa | v3.125.2, pdfium.v8.dll |
| Muokkaukset yksistreamisessä XFA-asiakirjassa ovat vain poissa | Natiivi tallennus torjui streamin asettelun, mutta paluuarvoa ei huomioitu | v3.125.2; kommentit ja käsittelyohjeet säilytetty versiosta v3.126.0 alkaen |
| Typistetty tiedosto, vaikka tallennus ilmoitti onnistuneen | Lopullinen puskuroitu kirjoitus epäonnistui sen jälkeen, kun kirjoittaja oli jo palauttanut onnistumisen | v3.125.2 V8-ajonaikainen; v3.125.3 tavallinen pdfium.dll |
| Kolmisivuinen dynaaminen lomake avautuu uudelleen kahdella sivulla | Juurialilomake ei pyydä restoreState="auto"a | Lomakkeen laadinta, ei kirjaston vika |
Aiemmat kirjoitukset päättelivät, ettei XFA-kenttien muokkauksia voinut säilyttää PDFiumilla lainkaan, mikä oli tarkkaa silloisten ajonaikaisten osalta. Uudempi V8-ajonaikainen tallentaa XFA-arvot natiivisti, joten elävässä lomakkeessa tehty muokkaus saavuttaa tallennetun datasets-paketin ilman pakettileikkausta puolestasi
Mikä PDFium-ajonaikainen tallentaa XFA-arvot?
XFA-tallennuksen uskollisuus riippuu natiivista DLL:stä, ei Delphi-kääreestä, joten ensimmäinen tarkistus on se, minkä ajonaikaisen prosessisi oikeasti latasi. PDFium Component toimittaa kaksi Windows-koontiversiota arkkitehtuuria kohden: tavallisen pdfium.dllin, joka on rakennettu ilman V8:aa ja XFA:aa, ja pdfium.v8.dllin, joka kantaa JavaScript-moottorin ja XFA-lomakeajonaikaisen. Vain pdfium.v8.dll voi ajaa XFA-lomaketta, joten jokainen tässä kuvattu XFA-korjaus asuu siellä, alkaen v3.125.2:n uudelleenrakennetuista Win32- ja Win64-V8-kirjastoista
Lopullisen kirjoituksen korjaus on geneeristä PDF-kirjoittajakoodia, joten sillä on merkitystä myös tavallisille asiakirjoille. v3.125.3 rakensi tavalliset pdfium.dll-kirjastot uudelleen kantamaan saman korjauksen. Jaettu lähdekoodi ei ole todiste jaetusta käytöksestä: kunnes binääri on rakennettu uudelleen, vanha DLL pitää vanhan vikan
Toinen ansa istui lataajassa. Ennen versiota v3.125.2 kohteen EnableV8Engine asettaminen arvoon True sai sidonnan poimimaan oletusarvoisen pdfium.v8.dll-nimen ja ohittamaan täyden polun kohteessa LibraryName. Sovellus, joka osoitti tuoreesti käyttöönotettuun ajonaikaiseen, saattoi yhä ladata vanhemman kopion toisesta kansiosta. Versiosta v3.125.2 alkaen hakemiston sisältävä LibraryName valitsee täsmälleen kyseisen tiedoston kummassakin moottoritilassa, ja puuttuva polku epäonnistuu toiseen mukana toimitettuun kirjastoon palaamisen sijaan
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Hakemisto kohteessa LibraryName kiinnittää täsmälleen tämän tiedoston (v3.125.2 ja uudemmat);
// jos tiedosto puuttuu, lataus nostaa poikkeuksen toiseen palaamisen sijaan
{$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; // epäonnistu käynnistyksessä, ei ensimmäisessä tallennuksessa
end;
Asiakirjan avaamisen jälkeen TPdf.XFA kertoo, että tiedosto sisältää XFA:n, ja TPdf.XfaRuntimeAvailable kertoo, että ladattu DLL voi oikeasti suorittaa sen. Jos sinun on myös erotettava staattiset ja dynaamiset lomakkeet, TPdf.FormType palauttaa arvon ftXfaFull tai ftXfaForeground; artikkeli XFA-lomakkeiden tunnistaminen ja XFA-pakettien poiminta Delphissä kattaa kyseisen tutkinnan yksityiskohtaisesti
Miksi tallennetut XFA-kentät saavat ylimääräisiä rivinvaihtoja?
Tallennetut XFA-kentät saivat rivinvaihtoja, koska molemmat natiivit XFA-kirjoittajat, geneerinen XML-elementtikirjoittaja ja lomakepaketin sarjallistaja, painoivat tuotoksensa siististi rivinvaihdolla aloitustägien jälkeen. Useimmissa XML:ssä kyseinen tyhjä merkki on kosmeettinen. XFA-datassa se ei ole: kun datasets-paketti jäsennetään uudelleen, teksti kohteen <Comments> ja </Comments> välillä on kentän arvo, rivinvaihto mukaan lukien. Tyhjä kenttä avautui siis uudelleen kantamassa yhtä LF:tä, ja jokainen myöhempi tallennus- ja uudelleenavauskierto saattoi lisätä toisen
Ilmeinen korjaus, arvojen karsiminen latauksen yhteydessä, olisi väärin. Käyttäjät kirjoittavat alussa ja lopussa olevia välilyöntejä sekä tahallista monirivistä tekstiä XFA-kenttiin, ja osoitelohkon tai kiinteän levyisen koodin on selvittävä tavu tavulta. Korjaus versiossa v3.125.2 poistaa siksi vain sen tyhjän merkin, jonka sarjallistaja itse syntetisoi tägien ympärille. Käyttäjäarvot, olemassa olevat tekstitietosolmut ja CDATA-osiot kulkevat läpi koskemattomina, joten arvo " indented" pysyy sisennettynä ja tahallisesti tyhjä kenttä pysyy tyhjänä
Miksi emoji palaa eri merkkinä?
Emoji palasi vääränä, koska Windowsin wchar_t on 16 bittiä leveä, ja kaksi dekoodauspolkua tallensi täyden Unicode-skaalaariarvon yhteen wchar_teen. UTF-8-streamin dekooderi ja numeeristen merkkiviittausten, kuten 🙂, jäsentäjä tekivät molemmat näin. U+1F642, hieman hymyilevä kasvo, ei mahdu 16 bittiin, joten ylemmät bitit putosivat pois ja U+F642 ilmestyi tilalle: koodipiste Private Use Area -alueella, jonka useimmat fontit renderöivät laatikkona tai eivät mitään
Lomakesarjallistajalla oli vastakkainen ongelma. Se suodatti merkit yksi wchar_t kerrallaan, näki kaksi surrogate-koodiyksikköä, jotka ovat kelvottomia eristettyinä, ja pudotti molemmat, joten emoji katosi lomakepaketista kokonaan. Versiossa v3.125.2 dekooderi kuluttaa jokaisen skaalaariarvon kokonaan ja emittoi oikean surrogate-parin. Kun jäljellä on vain yksi tulopaikka, se pitää matalan surrogaten odottavana eikä ilmoita streamin loppua, kun kyseinen yksikkö on yhä puskuroituna. Lukulohkojen yli jaettu UTF-8-jakso viedään seuraavaan lukuun hylkäämisen sijaan. Lomakkeenviejä pitää nykyään kelvolliset surrogate-parit yhdessä, ja numeeriset merkkiviittaukset tuottavat oikeat parit myös
Latin-1-testidata ei koskaan näytä mitään tästä, joten jokaisen XFA-kierrostestin tarvitsee vähintään yhden täydentävän tason merkin
Yksistreaminen XFA ja tallennusviat, joita kukaan ei nähnyt
Yksistreaminen XFA-asiakirja menetti muokkauksensa, koska natiivi tallennusavustaja torjui kyseisen tallennusasettelun ja sen kutsuja ohitti epäonnistumisen. ISO 32000-1 §12.7.8 sallii interaktiivisen lomakesanakirjan /XFA-tietueen olla joko pakettien nimien ja streamien taulukko tai yksi stream, joka kantaa koko XDP-asiakirjan. Pakettitaulukot ovat tavallinen tapaus, mutta yksittäiset streamit ovat täysin laillisia, ja PDF-tallennus päättyi ikään kuin mitään ei olisi tapahtunut, kun lomakedata pysyi vanhoissa arvoissaan
Versiosta v3.125.2 alkaen V8-ajonaikainen käsittelee tuetun yksistreamisen osajoukon. Se vie ensin molemmat elävät paketit, datasets ja form, lavastusalueelle ja validoi ne, ja vasta sen jälkeen korvaa täsmäävät paketit alkuperäisessä XDP:ssä. Muut paketit ja juuren nimiavaruusjulistukset säilytetään. Jos lavastus epäonnistuu, pysyvää XFA-streamia ei koskaan kosketeta ja asiakirja pitää muokkausmerkkinsä
XML-kommentit ja käsittelyohjeet vaativat ylimääräistä huolenpitoa, koska sisäinen XML-DOM pudottaa ne. Versiossa v3.125.2 niiden läsnäolo sai tallennuksen epäonnistumaan suoraan sisällön hiljaisen menettämisen sijaan. v3.126.0 säilyttää ne: ennen jäsentämistä jokainen kommentti tai käsittelyohje vaihdetaan merkkiin, joka on rakennettu etuliitteestä, joka ei esiinny missään alkuperäisessä tekstissä. Kun elävät paketit on korvattu, jokaisen merkin on ilmestyttävä täsmälleen kerran ennen kuin alkuperäinen tokeni palautetaan ja stream kirjoitetaan. Korvattujen pakettien ulkopuoliset tokenit säilyttävät siksi tekstinsä ja järjestyksensä, mukaan lukien prologin, mallipohjan ja muiden pakettien tokenit
Jotkin syötteet torjutaan yhä tahallaan, ja jokainen torjunta on eksplisiittinen tallennusvika:
- Kommentit tai käsittelyohjeet elävien
datasets- taiform-pakettien sisällä, koska niiden alkuperäisiä paikkoja ei voi kuvata tuoreesti viettyyn sisältöön - DTD-julistukset ja XMLDSig-allekirjoitukset, koska XDP:n uudelleenkirjoittaminen ei voi pitää XML-allekirjoitusta kelvollisena
- Virheellinen UTF-8- tai UTF-16-koodaus, keskeneräiset tägit, virheelliset merkkiviittaukset, tuntemattomat entiteetit ja epämuodostuneet käsittelyohjeet, jotka torjutaan hiljaisen korjaamisen sijaan
Yksistreaminen tuotos on UTF-8:a ja säilyttää XML-sisältömallin, ei alkuperäistä tavuasettelua tai koodausjulistusta
Viimeinen vika istui XFA:n alapuolella. Natiivi tiedostokirjoittaja puskuroi tuotoksen 32 Kt:n lohkoihin ja huuhteli viimeisen osittaisen lohkon vain tuhoajassaan, sen jälkeen kun asiakirjakirjoittaja oli jo ilmoittanut onnistumisesta. Levy täynnä - tai I/O-vika kyseisessä viimeisessä lohkossa oli näkymätön kutsujalle. Versiosta v3.125.2 alkaen V8-ajonaisessa ja v3.125.3 alkaen tavallisessa ajonaikaisessa kyseinen lopullinen huuhtelu on osa tallennustulosta, ja XFA-muokkausmerkki tyhjennetään vasta aidon onnistumisen jälkeen. Delphi-puolella TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean kirjoittaa väliaikaiseen tiedostoon kohteen viereen ja siirtää sen paikalleen vain, kun tallennus palauttaa Truen, joten epäonnistunut tallennus jättää edellisen tiedoston ehjäksi
Miksi dynaaminen XFA-lomake avautuu uudelleen vähemmällä sivulla?
Dynaaminen XFA-lomake avautuu uudelleen vähemmällä sivulla, kun sen juurialilomake ei julista restoreState="auto"a, ja kyseessä on lomakkeen laadintapäätös eikä PDFium Componentin vika. XFA 3.3:ssa kohteen restoreState oletus juurialilomakkeella on manual. Kohteen manual alla XFA-prosessori palauttaa vain rajatun tilan tallennetusta lomakepaketista ja jättää loput laatijan skripteille. Tallennetut kenttien arvot ja toistuvien alilomakkeiden instanssimäärät palaavat yhä, mutta ajonaikaisesti asetetut geometriset ominaisuudet eivät
Tapaus, joka paljasti tämän, oli kolmisivuinen lomake, jonka skripti kasvatti alilomakkeen arvoon h="450pt". Tallennettu lomakepaketti kantoi uutta korkeutta, arvot ja instanssimäärät. Uudelleenavauksessa layout rakennettiin kuitenkin mallipohjan korkeuksista, ja lomake virtasi uudelleen kahdelle sivulle. Ajonaikainen oli oikeassa: mallipohja ei ollut koskaan pyytänyt automaattista palautusta. Sen julistaminen juurialilomakkeelle korjaa uudelleenavauksen:
<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">
<!-- kentät; skriptit voivat muuttaa h:n tai lisätä instansseja ajonaikaisesti -->
</subform>
</subform>
</template>
Jos et omista mallipohjaa, älä paikkaa sen ympärillä katselimessa: lomake, joka nojaa manual-tilaan, odottaa omien skriptiensä rakentavan tilan. Elävä uudelleensivutus käyttäjän kirjoittaessa on erillinen aihe, käsitelty artikkelissa miten PDFium Component seuraa dynaamisen XFA:n sivumääriä ja siirtyneitä kenttiä
Miten varmistat XFA-tallennuksen Delphissä?
Ainoa luotettava XFA-tallennuksen tarkistus on avata tallennettu tiedosto uudessa TPdf-instanssissa ja lukea tallennettu data takaisin. TPdf.GetXfaDatasets palauttaa datasets-paketin sellaisena kuin se on tallennettu asiakirjaan, ei elävää XFA-datamallia, joten sen kutsuminen ennen tallennusta näyttää vanhat arvot. Uudelleenavauksen jälkeen se näyttää täsmälleen sen, mitä kirjoitettiin. Yksistreamisella asiakirjalla ei ole erikseen nimettyjä paketteja: PDFium ilmoittaa koko XDP:n yhtenä pakettina tyhjällä nimellä, joten GetXfaPacketByName('datasets') ja GetXfaDatasets eivät palauta mitään, ja varapolku lukee koko streamin funktion GetXfaFormPackets kautta
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; // pakettitaulukon asettelu
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // yksi stream: yksi nimetön paketti
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); // tallennettu XDP-tuotos on UTF-8:a
finally
Pdf.Free;
end;
end;
Tallennusrutiini sitoo sitten odottavan muokkauksen, tarkistaa kohteen SaveAs tuloksen ja vertaa uudelleenavattua arvoa. TPdf.ClearFormFieldFocus tappaa lomakekohdistuksen, mikä on se hetki, jolloin PDFium sitoo kohdistetun kentän muokkauspuskurin. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean täyttää kohdistetun kentän ohjelmallisesti, mutta se nojaa kohdistukseen, jonka kääre seuraa funktion FocusFormField kautta, joka kulkee widget-annotaatioiden läpi. Dynaamisella XFA-sivulla ei ole normaalisti yhtään, joten siellä teksti saapuu yleensä näppäimistösyötteenä kohteessa TPdfView, ja funktio palauttaa Falsen, kun yhdelläkään seuratulla kentällä ei ole kohdistusta
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Valinnainen skriptattu täyttö; False tarkoittaa, ettei yhdelläkään seuratulla kentällä ole kohdistusta
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // sito muokkauspuskurin
if not Pdf.SaveAs(FileName) then // sisältää lopullisen huuhtelun (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;
Kohtele alimerkkijonotestiä savutestinä. Tyhjä elementti voidaan sarjallistaa muodossa <Tag/>, attribuutit voivat ilmestyä data-elementeille, ja koodauksen karkaaminen muotojen & ja < ohitse on sarjallistajan valinta. Tuotantotarkistuksiin lataa uudelleenavattu XML aidolla XML-jäsentäjällä ja vertaa sidotun data-elementin tekstitietosolmua. Aja tarkistus kahdesti peräkkäin myös, koska rivinvaihtovika näytti koko muotonsa vasta toisella sukupolvella
Pikaopas: XFA-tallennuksen uskollisuuden tarkistuslista
- Ota käyttöön
pdfium.v8.dllversiosta v3.125.2 tai uudempi XFA-lomakkeille ja v3.125.3 tai uudempi tavallisellepdfium.dllille, jotta lopullisen kirjoituksen korjaus on molemmissa - Osoita
LibraryNametäyteen polkuun ja asetaEnableV8Enginearvoon True; puuttuva polku epäonnistuu toisen kopion lataamisen sijaan - Vahvista
TPdf.XFAjaTPdf.XfaRuntimeAvailableasiakirjan avaamisen jälkeen - Kutsu funktiota
ClearFormFieldFocusennenSaveAsia, jotta kohdistettu kenttä sitoutuu - Älä koskaan ohita kohteen
SaveAstotuusarvoista tulosta; False-tulos jättää edellisen tiedoston paikalleen - Varmista avaamalla uudelleen uudessa
TPdfissä ja lukemallaGetXfaDatasets, palaten funktioonGetXfaFormPacketsyksistreamiselle XFA:lle - Testaa tyhjillä arvoilla, alussa olevilla välilyönneillä, monirivistä tekstiä, merkillä
&ja täydentävän tason merkillä, kahden tallennussukupolven yli - Odota eksplisiittisiä tallennusvikoja DTD:ille, XMLDSigille ja kommenteille elävien pakettien sisällä yksistreamisessä XFA:ssa
- Jos dynaaminen lomake menettää ajonaikaisen geometriansa uudelleenavauksessa, tarkista juurialilomakkeelta
restoreState="auto"ennen kuin epäilet kirjastoa
Callback-rakenteesta, jonka XFA-ajonaikainen odottaa isäntäsovellukselta, kerrotaan artikkelissa FPDF_FORMFILLINFO versio 2 ja XFA-ABI Delphissä. V8-ajonaikainen, Delphi- ja C++Builder-kääre ja katselinohjain ovat kaikki osa pakettia PDFium Component for Delphi and C++Builder, joka sisältää molemmat Windows-ajonaikaiset Win32:lle ja Win64:lle