Tekninen artikkeli

FPDF_FORMFILLINFO version 2: seuraa DLL:n ABI:a

PDFium Component asettaa nyt FPDF_FORMFILLINFO.version-arvoksi 2 jokaiselle alustamalleen lomakeympäristölle, koska versio, jonka natiivi PDFium-build hyväksyy, on tuon buildin ominaisuus eikä avattavan asiakirjan. XFA-käytössä oleva pdfium.v8.dll hylkää version 1 suoraan, joten sen kautta avattu tavallinen AcroForm-PDF epäonnistui aiemmin FPDFDOC_InitFormFillEnvironment-kutsussa ilman että XFA:ta oli missään näkyvissä. v3.116.0:n korjaus on pieni, mutta sen takana oleva virhe on yleinen ja nimeämisen arvoinen: protokollan versiokenttä kuvaa muistiasettelua, jota toinen osapuoli odottaa, eikä sitä saa koskaan johtaa siitä, tarvitsetko sattumalta ominaisuuksia, joita tuo asettelu kantaa

Miksi FPDFDOC_InitFormFillEnvironment epäonnistuu tavallisella PDF:llä pdfium.v8.dll:n kanssa?

Ympäristö epäonnistuu, koska XFA-käytössä oleva PDFium-build validoi version-kentän ennen kuin tekee mitään muuta, ja vanha wrapper-logiikka antoi sille arvon 1 aina kun nykyinen asiakirja ei ollut XFA-lomake. Oire Delphi-isännässä on TPdf.InitializeFormFill-kutsusta nostettu EPdfError viestillä Cannot initialize form fill environment, heitettynä avattaessa tavallista laskua tai verolomaketta, jossa ei ole muuta kuin AcroForm-tekstikenttiä. Sama tiedosto avautuu hyvin tavallista pdfium.dll-kirjastoa vasten. Sama DLL avaa aidon XFA-asiakirjan hyvin. Vain V8-buildin ja ei-XFA-asiakirjan yhdistelmä hajoaa, ja juuri siihen yhdistelmään isäntä päätyy kun se kytkee EnableV8Engine-asetuksen saadakseen AcroForm-JavaScriptin, tai kun LoadDocument-funktion automaattivalinta on jo sitouttanut prosessin pdfium.v8.dll-kirjastoon aiemman XFA-tiedoston takia. Tuo sitoumus on prosessinlaajuinen: EnableV8Engine luetaan ennen ensimmäistä LoadLibrary-kutsua, ja kun XFA-build on ladattu, jokainen myöhempi tavallinen PDF kulkee saman ympäristön alustuksen läpi samaa binääriä vasten. Isäntä ei tehnyt mitään väärin; wrapper kysyi väärän kysymyksen täyttäessään tietuetta. Jos olet yhä päättämässä, mitä binääriä ylipäänsä toimitat, muistiinpanomme PDFium-DLL:n käyttöönotosta ja latausvirheiden diagnosoinnista käsittelee tavallisen ja V8-version valintaa, ja tämä artikkeli olettaa, että V8-build on jo prosessissa

PDFium Componentin kaavio tavallisen pdfium.dll:n ja XFA-käytössä olevan pdfium.v8.dll:n neljästä yhdistelmästä AcroForm- ja XFA-asiakirjoja vasten: version 1 -tietue rikkoi vain V8-buildin tavallisella lomakkeella, EPdfError FPDFDOC_InitFormFillEnvironment-kutsussa, kun taas korjattu version 2 -tietue avaa kaikki neljä
Yksi ehtolause sitoi ABI-version asiakirjaan, joten prosessinlaajuinen valinta V8-binääristä muutti jokaisen myöhemmän tavallisen PDF:n epäonnistuneeksi ympäristön alustukseksi

Mitä FPDF_FORMFILLINFO:n versiokenttä oikeastaan lupaa?

FPDF_FORMFILLINFO.version kertoo PDFiumille, mitä tietueen kenttiä se saa lukea, ja julkinen otsaketiedosto fpdf_formfill.h sitoo hyväksyttävät arvot siihen, miten kirjasto on käännetty, eikä asiakirjaan. Vapaasti sanoen sopimuksessa on kolme osaa. Version 1 kattaa vakaat takaisinkutsut FFI_Invalidate-kutsusta FFI_DoGoToAction-kutsuun sekä m_pJsPlatform-osoittimen. Build ilman XFA-moduulia hyväksyy joko 1:n tai 2:n, ja versiolla 2 se kutsuu myös uusia kokeellisia takaisinkutsuja. Build, jossa XFA-moduuli on, vaatii version 2, piste, ja otsake toistaa tuon vaatimuksen kahdesti kuin odottaen ihmisten jättävän sen huomaamatta. Missään sopimus ei mainitse asiakirjaa. Versio on lausunto varaamastasi tietueesta: versiolla 2 lupaat, että muisti m_pJsPlatform-osoittimen jälkeen on olemassa ja sisältää joko kelvollisia funktio-osoittimia tai NULL-arvoja

Version 2 -alue on se, missä kaikki XFA-koneisto asuu. Se alkaa xfa_disabled-kentällä, joka on FPDF_BOOL ja jonka otsake kuvaa ohitettavaksi versiota 2 alemmilla ja merkitykselliseksi vain kun XFA-moduuli on käännetty mukaan, ja jatkuu seitsemällätoista funktio-osoittimella, FFI_DisplayCaret-osoittimesta FFI_DoURIActionWithKeyboardModifier-osoittimeen. Jokainen niistä on dokumentoitu pakolliseksi XFA:lle ja muussa tapauksessa asetettavaksi NULL-arvoon. Tuo sanamuoto on koko korjauksen avain. NULL ei ole virhetila noille paikoille; se on dokumentoitu tila isännälle, joka ei aja XFA:ta. Tietue, joka on tyhjennetty FillChar-kutsulla ja merkitty sitten version 2 -tietueeksi, täyttää sopimuksen ei-XFA-buildissa täsmälleen yhtä hyvin kuin version 1 -tietue, ja se on ainoa tietue, jonka XFA-build ottaa vastaan

PDFium Componentin kaavio FPDF_FORMFILLINFO-tietueesta Delphissä: version 1 kattaa FFI_Invalidate-kutsusta FFI_DoGoToAction-kutsuun ulottuvat takaisinkutsut sekä m_pJsPlatform-osoittimen, version 2 lisää xfa_disabled-kentän ja seitsemäntoista FFI_DisplayCaret-aikakauden osoitinta, FillChar tyhjentää jokaisen tavun, ja NULL-paikat ovat dokumentoitu tila isännälle joka ei aja XFA:ta
Pascal-tietue on aina täysi version 2 -asettelu, joten XFA-käytössä oleva build hyväksyy sen ja tavallinen build yksinkertaisesti ei koskaan kutsu kokeellisia paikkoja, jotka jäävät NULL-arvoisiksi

Vanha valinta sitoi ABI:n asiakirjaan

Vika oli yksi ehtolause, joka näytti järkevältä yksinään tarkasteltuna. TPdf.InitializeFormFill laskee RuntimeReady-lipun kolmesta tosiasiasta: asiakirja ilmoittaa XFA-lomaketyypin TPdf.XFA-kautta, XFA-merkkijonoapufunktiot ratkesivat XfaFeaturesAvailable-kautta ja V8-viennit ratkesivat V8FeaturesAvailable-kautta. Ennen versiota v3.116.0 tuo sama lippu valitsi myös version

// v3.115.0 ja aiemmat: ABI-versio seurasi asiakirjaa
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

if RuntimeReady then
  FFormFillInfo.Info.version := 2
else
  FFormFillInfo.Info.version := 1;

// ... ja puuttuvan runtime-tuen haara kiinnitti sen uudelleen
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Lue se otsake kädessä ja vika on ilmeinen. RuntimeReady on epätosi jokaiselle tavalliselle AcroForm-asiakirjalle, joten jokainen tavallinen asiakirja ilmoitti version 1. pdfium.dll-kirjastolla se on kunnossa. pdfium.v8.dll-kirjastolla, joka on XFA-käytössä oleva build, PDFium tarkistaa kentän, havaitsee sen vaaditun 2:n alapuolella ja palauttaa null-arvon FPDF_FORMHANDLE-kahvana, minkä CheckPdf muuttaa yllä olevaksi poikkeukseksi. Vanhan koodin tarkoitus oli puolustava: pitää version 1, jotta XFA-build ei koskaan lue asettamattomia version 2 -paikkoja. Se puolusti ongelmaa vastaan, jonka otsake jo sulkee pois, ja loi ongelman, josta otsake nimenomaisesti varoittaa. Korjattu koodi päättää version kerran, etukäteen, siitä mitä tietue fyysisesti on

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinel: käytä staattista sivupuuta
  if not FormFill then
    Exit;

  FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
  FFormFillInfo.Pdf := Self;

  // Täysi version 2 -tietue varataan ja tyhjennetään yllä. PDFium
  // hyväksyy version 2 ilman XFA:ta ja vaatii sen jokaisessa XFA-käytössä
  // olevassa buildissa, myös kun tämä asiakirja ei sisällä XFA-lomaketta.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady portittaa XFA-takaisinkutsut ja xfa_disabled-arvon, ei koskaan versiota.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Mihin RuntimeReady yhä kuuluu: takaisinkutsut ja xfa_disabled

RuntimeReady pitää tehtävänsä XFA-käyttäytymisen porttina; se ei enää vain koske tietueen asetteluun. Version 1 -takaisinkutsut, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction ja loput tuosta lohkosta, kytketään ehdottomasti, koska sekä AcroForm että XFA riippuvat niistä. Seitsemäntoista version 2 -osoitinta sijoitetaan vain RuntimeReady-haaran sisällä, yhdessä xfa_disabled := 0 -asetuksen kanssa. Kun asiakirja on XFA mutta runtime ei ole paikalla, tietue jää versioon 2 xfa_disabled-arvolla 1 ja version 2 -paikat jäävät NULL-arvoisiksi, ja wrapper nostaa OnXfaRuntimeMissing-tapahtuman, jotta isäntä voi ehdottaa uudelleenkäynnistystä pdfium.v8.dll-kirjastolla. Kun ympäristö on olemassa, FPDF_LoadXFA kutsutaan vain kun RuntimeReady oli tosi, ja vain tosi paluuarvo asettaa FXfaRuntimeUsable-lipun, minkä TPdf.XfaRuntimeAvailable raportoi

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA käytössä
    FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
    FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
    FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
    FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
    FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
    FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
    FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
    FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
    FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
    // ... FFI_UploadTo ... FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime ei käytettävissä: pidä version 2, jätä XFA pois päältä, kerro isännälle.
    if Assigned(FOnXfaRuntimeMissing) then
      FOnXfaRuntimeMissing(Self);
  end;

  FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
  CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
  if RuntimeReady then
    FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;

Kaksi yksityiskohtaa tuossa lohkossa on helppo saada väärin, kun kirjoitat oman sidoksesi. FXfaPageCountOverride nollataan arvoon -1 sentinel-arvona ennen kuin mitään muuta tapahtuu, joten PageCount putoaa takaisin staattiseen sivupuuhun kunnes FFI_PageEvent raportoi uudelleensivutuksen; nolla siinä väittäisi hiljaisesti asiakirjaa tyhjäksi. Ja jokainen version 2 -takaisinkutsu on staattinen cdecl-rutiini, joka poimii omistavan TPdf-olion tietueesta ja nielee kaikki Pascal-poikkeukset ennen PDFiumiin palaamista, mikä on se kurinalaisuus, jonka muistiinpanomme PDFium-ABI:n kovettamisesta Delphissä kirjoittaa auki FFI_OpenFile-funktiolle. Mikään version vaihdoksessa ei löysää kumpaakaan sääntöä

Onko version 2 turvallinen kun DLL:ssä ei ole XFA-moduulia?

Kyllä, ja syy on tietueessa eikä kirjaston lupauksessa. Ei-XFA-buildissa otsake sanoo, että versio 2 saa kokeelliset takaisinkutsut kutsutuiksi myös, joten kysymys on, mitä PDFium löytää katsoessaan. TPdfFormFillInfo on pakattu tietue, jonka Info-jäsen on täysi FPDF_FORMFILLINFO kaikkine version 2 -kenttineen, ja InitializeFormFill tyhjentää koko sen FillChar-kutsulla ennen kuin koskee yhteenkään tavuun. Niinpä tavallista pdfium.dll-kirjastoa ja tavallista asiakirjaa vasten kirjasto näkee version 2, asetetun xfa_disabled-arvon ja NULL-arvon jokaisessa kokeellisessa paikassa, mikä on täsmälleen se tila, jonka otsake määrää isännälle, joka ei toteuta XFA:ta. Ei ole katkaistua tietuetta, jonka yli kirjasto lukisi, koska tietue ei koskaan ollut versiota 2 lyhyempi. Vanha logiikka puolusti asettelun epäsuhdetta, jonka Pascal-julistus oli jo poistanut

Rehellisesti sanottava raja on se, jota tietue ei kata. Versio 2 tavallisella asiakirjalla ei kytke päälle JavaScriptiä, XFA-komentosarjoja eikä mitään noiden takaisinkutsujen takana olevista isäntätapahtumista. m_pJsPlatform kiinnitetään vain kun V8FeaturesAvailable on tosi, XFA pysyy pois päältä ellei RuntimeReady ollut tosi, ja TPdf.XFA raportoi yhä lomaketyypin FPDF_GetFormType-funktiosta riippumatta siitä, mitä ympäristö neuvotteli. Isännän, joka haluaa tietää renderöityykö dynaaminen XFA todella, kannattaa yhä lukea XfaRuntimeAvailable-arvoa sen jälkeen kun Active on tullut todeksi, kuten muistiinpanomme XFA-lomakkeiden tunnistamisesta ja XFA-pakettien poiminnasta suosittaa, sen sijaan että päättelee mitään versiokentästä

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Laukeaa InitializeFormFill-kutsusta kun asiakirja on XFA mutta ladattu
  // pdfium.dll ei pysty ajamaan moottoria. Lomakeympäristö avautuu silti,
  // koska version 2 välitettiin kummassakin tapauksessa; vain XFA-runtime on pois.
  StatusBar.SimpleText :=
    'XFA form detected; restart with pdfium.v8.dll to enable dynamic rendering';
end;

procedure TMainForm.OpenDocument(const FileName: string);
begin
  Pdf.Active := False;
  Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
  Pdf.FormFill := True;
  Pdf.FileName := FileName;
  Pdf.Active := True;   // ei enää heitä poikkeusta tavallisella PDF:llä pdfium.v8.dll:n alla
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Protokollaversio ja ominaisuuksien saatavuus ovat kaksi eri akselia

Yleinen sääntö, joka tästä korjauksesta seuraa, on että takaisinkutsurakenteen versiokenttä vastaa kysymykseen kuinka iso tämä tietue on ja mitä siitä saa lukea, kun taas ominaisuuksien tunnistus vastaa kysymykseen mikä noista paikoista tekee jotain hyödyllistä. Ensimmäisen kiinnittää natiivibinääri ja se Pascal-julistus, jota vasten käänsit. Toinen vaihtelee asiakirjakohtaisesti, DLL:n vientitaulukon ja isännän kokoonpanon mukaan. Näiden kahden luhistaminen yhdeksi totuusarvoksi on houkuttelevaa, koska XFA-tapaus sattuu tarvitsemaan molempia, mutta heti kun jokin build vaatii vähimmäisversion, luhistaminen hajoaa jokaiselle asiakirjalle, joka ei ominaisuutta tarvitse. XFA-lomakkeet, jotka ISO 32000-1 §12.7.8 kuvaa AcroForm-sanakirjan rinnalla eläväksi XML-hyötykuormaksi, ovat se ominaisuus; tietueen asettelu on se protokolla, ja PDFium on oikeutettu vaatimaan asettelun ennen kuin se edes katsoo tiedostoa. Sama muoto näkyy kaikkialla, missä C-kirjasto versioi rakenteensa: katseluohjelman tietolohko, renderöintiasetusten tietue, alustan takaisinkutsutaulukko. Turvallinen kaava on se, jota korjattu InitializeFormFill noudattaa. Julista uusin ymmärtämäsi asettelu, tyhjennä se kokonaan, aseta versio vastaamaan tuota asettelua ehdottomasti ja anna sitten kyvykkyystarkistusten päättää, mitkä paikat täytetään. Jos tuleva PDFium-otsake lisää version 3, muutos on julistuksessa ja tuossa yhdessä sijoituksessa, ei asiakirjariippuvaisessa haarassa, joka on väärä mille tahansa yhdistelmälle jota kukaan ei testannut

PDFium Componentin kaavio, joka erottaa FPDF_FORMFILLINFO:n kaksi akselia: protokollaversion, jonka tietueen asettelu ja natiivibinääri kiinnittävät, sekä ominaisuuksien saatavuuden, jossa RuntimeReady portittaa xfa_disabled-arvon, seitsemäntoista version 2 -paikkaa, FPDF_LoadXFA-kutsun ja m_pJsPlatform-osoittimen asiakirja- ja isäntäkohtaisesti
Versiokenttä kuvaa muistia, jota toinen osapuoli saa lukea, kyvykkyystarkistukset ratkaisevat mitkä paikat tekevät jotain hyödyllistä, ja näiden kahden luhistaminen yhdeksi totuusarvoksi rikkoo sen buildin, joka vaatii vähimmäisversion

Korjattu lomakeympäristön alustus toimitetaan PDFium Componentissa Delphille, Lazarusille ja C++Builderille, ja se pätee yhtä lailla Win32- ja Win64-alustoilla, koska molemmat buildit jakavat saman tietuejulistuksen. Jos sovelluksesi valitsee jo pdfium.v8.dll-kirjaston JavaScript-vetoisia AcroFormeja varten, tämä on se muutos, joka antaa sen avata loput PDF-arkistostasi samalla binäärillä ilman lomakeympäristön erikoiskäsittelyä