Műszaki cikk

FPDF_FORMFILLINFO 2. verzió Delphiben: kövessük a DLL ABI-t

A PDFium Component mostantól 2-re állítja a FPDF_FORMFILLINFO.version értékét minden űrlapkitöltési környezetben, amelyet inicializál, mert az, hogy egy natív PDFium-build melyik verziót fogadja el, annak a buildnek a tulajdonsága, nem pedig a megnyitott dokumentumé. Egy XFA-t támogató pdfium.v8.dll egyszerűen visszautasítja az 1-es verziót, így a rajta keresztül megnyitott sima AcroForm PDF korábban a FPDFDOC_InitFormFillEnvironment hívásban bukott el, holott sehol nem volt XFA a közelben. A v3.116.0 javítása kicsi, a mögötte lévő tévedés viszont általános, és érdemes megnevezni: egy protokollverzió-mező azt a memóriaelrendezést írja le, amelyet a másik fél vár, és soha nem vezethető le abból, hogy épp szükségünk van-e azokra a funkciókra, amelyeket ez az elrendezés hordoz

Miért bukik el az FPDFDOC_InitFormFillEnvironment egy sima PDF-nél a pdfium.v8.dll-lel?

A környezet azért bukik el, mert az XFA-t támogató PDFium-build minden más előtt érvényesíti a version mezőt, a régi wrapperlogika pedig 1-est adott át neki, valahányszor az aktuális dokumentum nem XFA-űrlap volt. A tünet egy Delphi-hosztban egy EPdfError, amelyet a TPdf.InitializeFormFill dob a Cannot initialize form fill environment üzenettel, méghozzá egy olyan hétköznapi számla vagy adóűrlap megnyitásakor, amelyben nincs más, csak AcroForm szövegmezők. Ugyanez a fájl gond nélkül megnyílik a sima pdfium.dll-lel szemben. Ugyanez a DLL gond nélkül megnyit egy valódi XFA-dokumentumot. Csak a V8 build és a nem XFA dokumentum kombinációja törik el, és pontosan ebbe a kombinációba kerül a hoszt, miután bekapcsolta az EnableV8Engine beállítást az AcroForm JavaScript eléréséhez, vagy miután a LoadDocument automatikus kiválasztása egy korábbi XFA-fájl miatt már a pdfium.v8.dll használatára kötelezte a folyamatot. Ez az elköteleződés folyamatszintű: az EnableV8Engine az első LoadLibrary előtt olvasódik be, és ha egyszer betöltődött az XFA-build, minden későbbi sima PDF ugyanazon a binárison, ugyanazon környezetbeállításon megy át. A hoszt semmit nem tett rosszul; a wrapper tett fel rossz kérdést, amikor kitöltötte a rekordot. Ha még mindig azon gondolkodik, melyik binárist szállítsa egyáltalán, a PDFium DLL telepítéséről és a betöltési hibák diagnosztizálásáról szóló jegyzetünk tárgyalja a sima és a V8 változat közötti választást, ez a cikk pedig azt feltételezi, hogy a V8 build már a folyamatban van

A PDFium Component ábrája a sima pdfium.dll és az XFA-t támogató pdfium.v8.dll, valamint az AcroForm és XFA dokumentumok négy kombinációjáról: az 1-es verziójú rekord csak a V8 buildet törte el sima űrlapnál, EPdfError kivétellel az FPDFDOC_InitFormFillEnvironment hívásban, a javított 2-es verziójú rekord viszont mind a négyet megnyitja
Egyetlen feltétel kötötte az ABI-verziót a dokumentumhoz, így a V8 bináris folyamatszintű kiválasztása minden későbbi sima PDF-et sikertelen környezetinicializálássá tett

Mit ígér valójában az FPDF_FORMFILLINFO version mezője?

A FPDF_FORMFILLINFO.version megmondja a PDFiumnak, hogy a rekord mely mezőit olvashatja, a nyilvános fpdf_formfill.h fejléc pedig az elfogadható értékeket ahhoz köti, hogyan fordították le a könyvtárat, nem a dokumentumhoz. Saját szavakkal a szerződésnek három része van. Az 1-es verzió az FFI_Invalidate-től az FFI_DoGoToAction-ig terjedő stabil callbackeket fedi le, plusz a m_pJsPlatform mutatót. Az XFA-modul nélküli build 1-est vagy 2-est egyaránt elfogad, és 2-esnél a további kísérleti callbackeket is meg fogja hívni. Az XFA-modullal rendelkező build 2-est követel meg, pont, és a fejléc ezt a követelményt kétszer is megismétli, mintha számítana rá, hogy az emberek elnézik. A szerződés sehol nem említi a dokumentumot. A verzió az általunk lefoglalt rekordról szóló állítás: egy 2-essel azt ígérjük, hogy a m_pJsPlatform utáni memória létezik, és vagy érvényes függvénymutatókat, vagy NULL értéket tartalmaz

A 2-es verziójú tartomány az, ahol a teljes XFA-gépezet lakik. A xfa_disabled értékkel kezdődik, egy FPDF_BOOL-lal, amelyet a fejléc a 2-es verzió alatt figyelmen kívül hagyottként ír le, és csak akkor tekint értelmesnek, ha az XFA-modul bele van fordítva, majd tizenhét függvénymutatóval folytatódik, az FFI_DisplayCaret-től az FFI_DoURIActionWithKeyboardModifier-ig. Mindegyikükről az áll a dokumentációban, hogy XFA-hoz kötelező, egyébként pedig NULL-ra kell állítani. Ez a megfogalmazás a kulcsa a teljes javításnak. A NULL nem hibaállapot ezeknél a helyeknél; az a dokumentált állapot egy olyan hoszt számára, amely nem hajt meg XFA-t. Az a rekord, amelyet FillChar-ral nulláztunk ki, majd 2-es verziójúként jelöltünk meg, egy nem XFA builden pontosan ugyanolyan jól kielégíti a szerződést, mint egy 1-es verziójú, és ez az egyetlen rekord, amelyet egy XFA-build elfogad

A PDFium Component ábrája az FPDF_FORMFILLINFO rekordról Delphiben: az 1-es verzió az FFI_Invalidate-től az FFI_DoGoToAction-ig terjedő callbackeket fedi le plusz a m_pJsPlatform mutatót, a 2-es verzió hozzáadja az xfa_disabled mezőt és tizenhét FFI_DisplayCaret környéki mutatót, a FillChar minden bájtot nulláz, a NULL-ra hagyott helyek pedig a dokumentált állapotot jelentik egy XFA-t nem hajtó hoszt számára
A Pascal-rekord mindig a teljes 2-es verziójú elrendezés, így egy XFA-t támogató build elfogadja, egy sima build pedig egyszerűen soha nem hívja meg a NULL-on maradó kísérleti helyeket

A régi kiválasztás a dokumentumhoz kötötte az ABI-t

A hiba egyetlen feltételes utasítás volt, amely önmagában véve ésszerűnek látszott. A TPdf.InitializeFormFill három tényből számítja ki a RuntimeReady jelzőt: a dokumentum a TPdf.XFA úton XFA-űrlaptípust jelent, az XFA-sztringsegédfüggvények feloldódtak az XfaFeaturesAvailable révén, és a V8-exportok feloldódtak a V8FeaturesAvailable révén. A v3.116.0 előtt ugyanez a jelző választotta meg a verziót is

// v3.115.0 és korábbi: az ABI-verzió a dokumentumot követte
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

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

// ... a hiányzó futtatókörnyezet ága pedig újra rögzítette
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Olvassuk el a fejléccel a kezünkben, és a hiba nyilvánvaló. A RuntimeReady minden sima AcroForm dokumentumnál hamis, így minden sima dokumentum 1-es verziót hirdetett. A pdfium.dll-nél ez rendben van. A pdfium.v8.dll-nél, vagyis az XFA-t támogató buildnél a PDFium megnézi a mezőt, a megkövetelt 2 alatt találja, és null FPDF_FORMHANDLE értéket ad vissza, amiből a CheckPdf csinálja a fenti kivételt. A régi kód szándéka védekező volt: tartsuk meg az 1-es verziót, hogy az XFA-build soha ne olvassa a hozzá nem rendelt 2-es verziójú helyeket. Olyan probléma ellen védekezett, amelyet a fejléc már kizár, és olyat teremtett, amelyre a fejléc kifejezetten figyelmeztet. A javított kód egyszer dönti el a verziót, előre, abból, hogy a rekord fizikailag mi

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // őrszemérték: a statikus oldalfát használjuk
  if not FormFill then
    Exit;

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

  // A teljes 2-es verziójú rekord fentebb lefoglalódott és nullázódott. A PDFium
  // XFA nélkül is elfogadja a 2-es verziót, és megköveteli minden XFA-t támogató
  // buildnél, akkor is, ha ez a dokumentum nem tartalmaz XFA-űrlapot.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // A RuntimeReady az XFA-callbackeket és az xfa_disabled mezőt kapuzza, a verziót soha.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Ahol a RuntimeReady továbbra is a helyén van: a callbackek és az xfa_disabled

A RuntimeReady megtartja a szerepét mint az XFA-viselkedés kapuja; egyszerűen többé nem nyúl a rekord elrendezéséhez. Az 1-es verziójú callbackek — FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction és a blokk többi tagja — feltétel nélkül bekötődnek, mert az AcroForm és az XFA egyaránt támaszkodik rájuk. A tizenhét 2-es verziójú mutató kizárólag a RuntimeReady ágában kap értéket, az xfa_disabled := 0 beállítással együtt. Amikor a dokumentum XFA, a futtatókörnyezet viszont nincs ott, a rekord 2-es verzióban marad, az xfa_disabled 1-en, a 2-es verziójú helyek pedig NULL-on, a wrapper pedig kiváltja az OnXfaRuntimeMissing eseményt, hogy a hoszt felajánlhassa az újraindítást a pdfium.v8.dll-lel. Miután a környezet létezik, a FPDF_LoadXFA csak akkor hívódik meg, ha a RuntimeReady igaz volt, és csak az igaz visszatérési érték állítja be a FXfaRuntimeUsable mezőt, amit a TPdf.XfaRuntimeAvailable jelent

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA engedélyezve
    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-tól FFI_DoURIActionWithKeyboardModifier-ig
  end
  else if XFA then
  begin
    // A futtatókörnyezet nem elérhető: marad a 2-es verzió, az XFA letiltva, a hoszt értesítve.
    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;

Két részletet könnyű elrontani ebben a blokkban, amikor az ember saját kötést ír. Az FXfaPageCountOverride őrszemértékként -1-re áll vissza, mielőtt bármi más történne, így a PageCount a statikus oldalfára esik vissza egészen addig, amíg az FFI_PageEvent újratördelést nem jelent; egy ott hagyott nulla csendben üres dokumentumot állítana. A 2-es verziójú callbackek pedig mind statikus cdecl rutinok, amelyek visszanyerik a rekordból a tulajdonos TPdf példányt, és elnyelnek minden Pascal-kivételt, mielőtt visszatérnének a PDFiumba; ez az a fegyelem, amelyet a PDFium ABI Delphiben való megerősítéséről szóló jegyzetünk fejt ki az FFI_OpenFile kapcsán. A verzióváltás egyik szabályon sem enyhít

Biztonságos-e a 2-es verzió, ha a DLL-ben nincs XFA-modul?

Igen, és az ok a rekordban van, nem a könyvtár ígéretében. Egy nem XFA buildnél a fejléc azt mondja, hogy a 2-es verzió hatására a kísérleti callbackek is meghívódnak, így a kérdés az, mit talál a PDFium, amikor odanéz. A TPdfFormFillInfo egy pakolt rekord, amelynek az Info tagja a teljes FPDF_FORMFILLINFO, minden 2-es verziójú mezővel együtt, az InitializeFormFill pedig az egészet nullázza a FillChar hívással, mielőtt egyetlen bájthoz hozzányúlna. Egy sima pdfium.dll-nél és sima dokumentumnál tehát a könyvtár 2-es verziót, beállított xfa_disabled értéket és NULL-t lát minden kísérleti helyen, ami pontosan az az állapot, amelyet a fejléc előír egy XFA-t nem megvalósító hoszt számára. Nincs csonkolt rekord, amelyen túl a könyvtár olvashatna, mert a rekord soha nem volt rövidebb a 2-es verziónál. A régi logika olyan elrendezésbeli eltérés ellen védekezett, amelyet a Pascal-deklaráció már megszüntetett

A határ, amelyet érdemes őszintén kimondani, az, amelyet a rekord nem tud lefedni. A 2-es verzió egy sima dokumentumon nem kapcsolja be a JavaScriptet, az XFA-szkriptelést, sem az ezen callbackek mögötti hoszteseményeket. A m_pJsPlatform csak akkor kapcsolódik be, ha a V8FeaturesAvailable igaz, az XFA letiltva marad, hacsak a RuntimeReady nem volt igaz, a TPdf.XFA pedig továbbra is az FPDF_GetFormType-ból jelenti az űrlaptípust, függetlenül attól, mit tárgyalt meg a környezet. Az a hoszt, amely tudni akarja, hogy a dinamikus XFA valóban renderelődik-e, olvassa továbbra is az XfaRuntimeAvailable értékét, miután az Active igazra váltott, ahogy a XFA-űrlapok felismeréséről és XFA-csomagok kinyeréséről szóló jegyzetünk javasolja, ahelyett hogy bármit is kikövetkeztetne a version mezőből

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Az InitializeFormFill váltja ki, amikor a dokumentum XFA, a betöltött
  // pdfium.dll viszont nem tudja futtatni a motort. Az űrlapkörnyezet így is megnyílik,
  // mert a 2-es verzió mindkét esetben átadódott; csak az XFA futtatókörnyezet van kikapcsolva.
  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;   // már nem dob kivételt sima PDF-nél a pdfium.v8.dll alatt
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

A protokollverzió és a funkcióelérhetőség két külön tengely

Az ebből a javításból kirajzolódó általános szabály az, hogy egy callback-struktúra verziómezője arra a kérdésre válaszol, hogy „mekkora ez a rekord, és mit olvashatsz ki belőle", a funkciófelismerés pedig arra, hogy „ezek közül a helyek közül melyik tesz bármi hasznosat". Az elsőt a natív bináris és az a Pascal-deklaráció rögzíti, amelyre fordítottunk. A második dokumentumonként, DLL-exporttáblánként és hosztkonfigurációnként változik. A kettőt egyetlen logikai értékbe összevonni csábító, mert az XFA eset épp mindkettőt igényli, de abban a pillanatban, amikor egy build minimális verziót kényszerít ki, az összevonás eltörik minden olyan dokumentumnál, amelynek nincs szüksége a funkcióra. Az XFA-űrlapok, amelyeket az ISO 32000-1 §12.7.8 az AcroForm szótár mellett élő XML-terhelésként ír le, itt a funkció; a rekord elrendezése a protokoll, és a PDFium jogosult ragaszkodni az elrendezéshez, mielőtt egyáltalán a fájlra nézne. Ugyanez az alak jelenik meg mindenhol, ahol egy C-könyvtár verziózza a struktúráit: egy viewer-info blokk, egy renderelési opciók rekordja, egy platform-callbacktábla. A biztonságos minta az, amelyet a javított InitializeFormFill követ. Deklaráljuk a legújabb elrendezést, amelyet értünk, nullázzuk ki teljesen, állítsuk a verziót feltétel nélkül ehhez az elrendezéshez, majd a képességvizsgálatok döntsék el, mely helyeket töltsük ki. Ha egy jövőbeli PDFium-fejléc hozzáad egy 3-as verziót, a változás a deklarációt és azt az egy értékadást érinti, nem egy dokumentumfüggő ágat, amely épp arra a kombinációra lesz hibás, amelyet senki nem tesztelt

A PDFium Component ábrája az FPDF_FORMFILLINFO mögötti két tengely szétválasztásáról: a protokollverzió, amelyet a rekord elrendezése és a natív bináris rögzít, valamint a funkcióelérhetőség, ahol a RuntimeReady kapuzza az xfa_disabled mezőt, a tizenhét 2-es verziójú helyet, az FPDF_LoadXFA hívást és az m_pJsPlatform mutatót dokumentumonként és hosztonként
A verziómező azt a memóriát írja le, amelyet a másik fél olvashat, a képességvizsgálatok döntik el, mely helyek tesznek bármi hasznosat, a kettő egy logikai értékbe vonása pedig eltöri azt a buildet, amely minimális verziót kényszerít ki

A javított űrlapkitöltés-inicializálás a PDFium Componentben érkezik Delphihez, Lazarushoz és C++Builderhez, és Win32 és Win64 alatt egyaránt érvényes, mivel mindkét build ugyanazt a rekorddeklarációt használja. Ha az alkalmazása már a pdfium.v8.dll-t választja a JavaScript-vezérelt AcroFormokhoz, ez az a változás, amely lehetővé teszi, hogy ugyanazon binárison keresztül nyissa meg a PDF-archívuma többi részét, anélkül hogy külön kezelné az űrlapkörnyezetet