Tekninen artikkeli

Miksi XFA-kenttämuokkaukset katoavat tallennuksessa PDFiumissa Delphille

TPdf.SetFocusedFormFieldText PDFium-komponentissa kirjoittaa parhaillaan fokusoidun lomakekentän elävään muokkauspuskuriin, ja XFA-lomakkeelle tuo puskuri ei koskaan tavoita datasets-pakettia, joka sarjallistetaan levylle — joten arvo, jonka käyttäjä kirjoittaa, ja jonka koodisi vahvistaa hyväksytyksi, on hiljaa poissa seuraavalla kerralla, kun tiedosto avataan. AcroForm-kentillä ei ole tätä ongelmaa: sama kutsu sitoo kentän /V-merkintään heti, kun fokus siirtyy pois. Käyttäjä, joka täyttää XFA-vastaanottolomakkeen, tallentaa ja avaa uudelleen löytääkseen määräkentän jälleen tyhjänä, ei osu renderöintihäiriöön — hän osuu siihen rajaan, mitä itse PDFium-moottori paljastaa lomaketiedon kirjoittamiselle

Tämä on kapeampi kysymys kuin XFA-lomakkeen tunnistaminen ylipäätään, tai sen JavaScriptin saaminen ajoon: ei "tukeeko PDFium XFA:ta" eikä "miten suoritan AcroForm-skriptejä" vaan nimenomaan mitä arvolle tapahtuu sen jälkeen, kun SetFocusedFormFieldText raportoi onnistumisen. Lyhyt versio on, että AcroForm ja XFA eivät ole kaksi murretta samasta lomakemallista PDFiumin kirjoituspolun kannalta — ne ovat kaksi lomakemallia kahdella täysin erilaisella suhteella siihen, mitä käyttäjä kirjoittaa ja mitä tallennus todella vangitsee, ja näiden kahden sekoittaminen on se, mikä muuttaa yhden rivin API-kutsun tukipyynnöksi kolme viikkoa sen jälkeen, kun asiakkaan pilottikäyttöönotto käynnistyy. AcroForm JavaScript -artikkeli näyttää yhden rivin kutsun ja toteaa AcroForm-vastaan-XFA-lopputuloksen koodikommentissa; tämä pysyy samassa API:ssa ja käy läpi sisäisen kirjoituspolun, datasets-paketin todistuksen siitä, ettei XFA-kirjoitus koskaan osu perille, miksi aukko istuu itse PDFiumissa eikä Delphi-sidonnassa, ja paikkaa-oma-XML-työkierron asiakirjoille, joiden muokkauksen on selvittävä tallennuksesta

Miten SetFocusedFormFieldText kirjoittaa kentän arvon?

TPdf.SetFocusedFormFieldText toimii simuloimalla näppäintason muokkausta, ei tökkäämällä arvoa suoraan asiakirjamalliin. Sisäisesti se kutsuu FORM_SelectAllText-funktiota valitakseen fokusoidun kentän nykyisen sisällön, sitten FORM_ReplaceSelection-funktiota ylikirjoittaakseen valinnan uudella merkkijonolla — samat kaksi toimintoa, jotka näppäimistöohjattu valitse-kaikki-ja-kirjoita laukaisisi. Koska kirjoitus kulkee PDFiumin interaktiivisen tekstinmuokkauspolun kautta eikä sen ohi, mikä tahansa kenttään sidottu näppäin-, muoto- tai laskentaskripti laukeaa täsmälleen kuten se laukeaisi ihmiselle, joka kirjoittaa, mikä tekee API:sta hyödyllisen ohjelmalliseen lomakkeen täyttöön katseluohjelmassa, joka pitää JavaScriptin elossa. Lukupuolen vastine on FocusedFormFieldText, tuettuna FORM_GetFocusedText-funktiolla, ja se heijastaa samaa elävää puskuria, johon SetFocusedFormFieldText juuri kirjoitti

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

Miksi AcroForm säilyttää arvon ja XFA menettää sen?

AcroForm-teksti- ja yhdistelmäkentät säilyvät, koska PDFiumin oma lomaketäyttöympäristö sitoo muokkauspuskurin puolestasi: heti kun kenttä menettää fokuksen, puskuri kirjoitetaan kentän /V-merkintään, samaan avaimeen, jota jokainen standardinmukainen PDF-lukija katsoo tietääkseen kentän tallennetun arvon. TPdf.ClearFormFieldFocus — joka kutsuu FORM_ForceToKillFocus-funktiota konepellin alla — pakottaa tuon sitomisen pyynnöstä, joten koodi, joka asettaa arvon ohjelmallisesti, ei tarvitse odottaa todellista hiiriklikkausta jossain muualla käyttöliittymässä. Tallenna heti sen jälkeen, ja uusi teksti on osa asiakirjan olio-kaaviota ennen kuin TPdf.SaveAs koskaan ajetaan, koska /V on todellinen merkintä todellisessa kenttäsanakirjassa, ei jotain, mikä on pultattu päälle jälkikäteen

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

Missä XFA-kenttämuokkaus todella asuu?

XFA-kentillä ei ole tällaista kytkentää. Teksti, jonka käyttäjä kirjoittaa, päätyy CPWL_Edit-puskuriin, joka kuuluu PDFiumin XFA-renderöinti- ja vuorovaikutuskerrokselle, eikä tuolla kerroksella ole koodipolkua, joka kopioisi puskurin takaisin PDF:ään tallennettuun datasets-pakettiin. TPdf.GetXfaDatasets tekee aukon näkyväksi: kutsu sitä ennen ja jälkeen muokkausta XFA-kentässä, ja tavut, jotka se palauttaa, ovat identtiset, koska metodi lukee alkuperäisen paketin, jolla asiakirja avattiin, ei koskaan sen widgetin elävää tilaa, jota juuri muokkasit. Mikään tästä ei ole välimuistibugi tai virkistysajoitusongelma — datasets-paketti levyllä ja muokkauspuskuri muistissa ovat yksinkertaisesti kaksi eri tilan palaa, joita PDFiumin julkinen API ei koskaan yhdistä

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

Onko tämä PDFium-komponentin bugi vai PDFiumin rajoitus?

Puuttuva pala istuu itse PDFiumissa, ei sen päällä olevassa Delphi-sidonnassa. PDFiumin julkisella API:lla ei ole FPDF_SetXFAPacket-funktiota päivitetyn paketin injektoimiseksi, eikä FPDF_SaveAsXFA-funktiota pyytääkseen XFA-moottoria sarjallistamaan nykyisen DOM:insa takaisin datasets-XML:ksi ennen tallennusta. FPDF_SaveAsCopy — vienti, joka tukee TPdf.SaveAs-metodia — kirjoittaa ulos asiakirjan olio-kaavion, joka PDFiumilla jo on; sillä ei ole koukkua pyytää XFA-moottoria tyhjentämään elävää tilaansa ensin, koska tuota koukkua ei ole olemassa ylävirrassa. PDFium-komponentti ei voi lisätä sovittelua, jota PDFium itse ei koskaan toteuttanut, ja oman kotikutoisen DOM-XML-sarjallistajan toimittaminen, joka arvaa PDFiumin sisäistä XFA-tilaa, olisi pahempi kuin rehellinen aukko: se näyttäisi toimivan, kunnes seuraava PDFium-versio muuttaa jotain, mitä kukaan projektin ulkopuolella ei näe

Tämä raja paljastui saman v2.13.2-tarkastuksen aikana, joka rakensi SetFocusedFormFieldText-funktion alun perin. FORM_ReplaceSelection oli sidottu DLL:n tuontitauluun useiden versioiden ajan koskaan kutsumatta sitä Pascal-koodista, ja kirjoituspolun lisääminen, joka lopulta käytti sitä, on se, mikä teki säilyvyysaukosta riittävän konkreettisen dokumentoitavaksi teoreettisen sijaan. Sama tarkastuskierros paljasti toisiinsa liittymättömän mutta hengeltään liittyvän aukon: AcroForm JavaScript oli poistettu käytöstä hiljaa v2.13.0:sta lähtien, koska JS-alusta oli kytketty vain XFA-alustushaaran sisään, joten tavalliset AcroForm-asiakirjat, joissa oli app.alert tai lasketut kentät, eivät koskaan saaneet skriptimoottoria lainkaan. Tuo oli korjattavissa — JS-alustan laajentaminen jokaiseen asiakirjaan riippumatta XFA:sta — ja se julkaistiin samassa versiossa; tässä käsitelty säilyvyysaukko ei ollut korjattavissa, yllä mainituista syistä. JavaScript-korjaus ja sen ympärillä olevat isäntä-veto-tapahtumat käsitellään artikkelissa AcroForm JavaScriptin ajaminen PDFium-komponentilla

Mitä sinun pitäisi tehdä asialle Delphissä?

AcroForm-asiakirjoille korjaus ei ole muuta kuin hyvä tapa: kutsu ClearFormFieldFocus (tai muuten siirrä fokus pois) ennen SaveAs-kutsua aina, kun arvo asetettiin ohjelmallisesti, sen sijaan että oletettaisiin myöhemmän käyttöliittymävuorovaikutuksen laukaisevan sitomisen puolestasi. Asiakirjalle, joka voi olla joko AcroForm tai XFA — mikä on yleinen tapaus yleiskäyttöisessä katseluohjelmassa — tarkista FormType tai XFA-totuusarvo ennen kuin lupaat kutsujalle, että tallennus pysyy, ja lue XFA-lomakkeiden tunnistaminen ja XFA-pakettien poiminta saadaksesi koko joukon tunnistimia, mukaan lukien XFAF-tapaus, jossa XFA-sisältö on kerrostettu muuten tavallisten AcroForm-widgetien päälle, jotka kunnioittavat /V-kenttää

Aidolle dynaamiselle XFA-lomakkeelle, jossa muokattujen arvojen on selvittävä tallennuksesta, interaktiivinen muokkauspuskuri ei ole oikea työkalu ollenkaan. Kestävä polku on kohdella GetXfaDatasets-metodia perustasonasi, ei tuloksenasi: lue se kerran, kun asiakirja avataan, pidä oma tietue siitä, mitä käyttäjä muutti kenttä kerrallaan — täsmälleen ne arvot, jotka käyttöliittymälläsi jo on, koska PDFium ei anna niitä sinulle takaisin jälkikäteen — paikkaa ne perustasoon XML:ään itse, ja ohjaa omaa tulostettasi. Kirjoitus, joka kulkee oman koodisi hallitseman XML:n kautta, selviää tallennuksesta, josta CPWL_Edit-puskuri ei koskaan selviäisi

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

Aukon nappaaminen ennen kuin asiakas tekee sen

TPdf.SaveAs palauttaa True:n riippumatta siitä, selvisikö XFA-kentän arvo, koska PDFiumin näkökulmasta tallennus aidosti onnistui — se kirjoitti jokaisen tavun, jota pyydettiin kirjoittamaan. Se tekee tästä juuri sellaisen vian, joka luiskahtaa savutestin ohi ja saavuttaa asiakkaan: mikään ei nosta poikkeusta, mikään ei kirjaa lokiin, tiedosto avautuu hyvin, vain tietty arvo on väärin. Edestakainen testi, joka todella avaa tallennetun tiedoston uudelleen ja vertaa kentän arvoa — tai vertaa GetXfaDatasets-arvoa ennen ja jälkeen, aiemman esimerkin mukaisesti — kuuluu regressiotestisarjaan jokaiselle katseluohjelmalle, joka antaa käyttäjien muokata XFA-sisältöä, ei vain AcroForm-polkuihin, jotka sattuvat toimimaan oletuksena

Mikään tästä ei ole vika, joka pitäisi ilmoittaa PDFium-komponentille, vaan pikemminkin raja, jonka ympärille suunnitellaan: SetFocusedFormFieldText tekee täsmälleen sen, mitä sen nimi sanoo, molemmille lomakemalleille, ja ero lopputuloksessa jäljittyy puhtaasti siihen, mihin AcroForm ja XFA kumpikin kytkevät tuon puskurin PDFiumin puolella. API, fokus- ja tallennusprimitiivit sekä pakettilukijat, joihin tässä viitataan, ovat osa PDFium-komponenttia Delphille ja C++Builderille