Teknik Makale

Delphi'de FPDF_FORMFILLINFO Sürüm 2: DLL ABI'sini İzleyin

PDFium Component artık başlattığı her form-fill ortamı için FPDF_FORMFILLINFO.version değerini 2'ye ayarlıyor, çünkü yerel bir PDFium derlemesinin kabul ettiği sürüm o derlemenin bir özelliğidir, açılan belgenin değil. XFA destekli bir pdfium.v8.dll sürüm 1'i doğrudan reddeder, bu yüzden onun üzerinden açılan düz bir AcroForm PDF'i eskiden ortada hiç XFA yokken FPDFDOC_InitFormFillEnvironment içinde başarısız oluyordu. v3.116.0 düzeltmesi küçüktür, ama arkasındaki hata genel bir hatadır ve adını koymaya değer: bir protokol sürüm alanı, karşı tarafın beklediği bellek düzenini tarif eder ve o düzenin taşıdığı özelliklere o an ihtiyacınız olup olmamasından asla türetilmemelidir

FPDFDOC_InitFormFillEnvironment pdfium.v8.dll ile düz bir PDF'te neden başarısız oluyor?

Ortam başarısız olur, çünkü XFA destekli bir PDFium derlemesi her şeyden önce version alanını doğrular ve eski sarmalayıcı mantığı, geçerli belge bir XFA formu olmadığında ona 1 veriyordu. Bir Delphi ana uygulamasındaki belirti, TPdf.InitializeFormFill içinden Cannot initialize form fill environment iletisiyle fırlatılan bir EPdfError; içinde AcroForm metin alanlarından başka bir şey olmayan sıradan bir fatura ya da vergi formu açılırken olur. Aynı dosya düz pdfium.dll karşısında sorunsuz açılır. Aynı DLL gerçek bir XFA belgesini de sorunsuz açar. Yalnızca V8 derlemesi ile XFA olmayan belgenin bileşimi bozulur; bu da bir ana uygulamanın AcroForm JavaScript'i için EnableV8Engine açtıktan sonra ya da LoadDocument içindeki otomatik seçim daha önceki bir XFA dosyası yüzünden süreci pdfium.v8.dlle çoktan bağladıktan sonra düştüğü bileşimin ta kendisidir. Bu bağlılık süreç genelindedir: EnableV8Engine ilk LoadLibrary çağrısından önce okunur ve XFA derlemesi bir kez yüklendikten sonra her düz PDF aynı ikili karşısında aynı ortam kurulumundan geçer. Ana uygulama yanlış bir şey yapmadı; sarmalayıcı kaydı doldururken yanlış soruyu sordu. Hangi ikiliyi yayınlayacağınıza hâlâ karar veriyorsanız, PDFium DLL dağıtımı ve yükleme hatalarını teşhis etme notumuz düz ve V8 seçimini ele alıyor; bu makale V8 derlemesinin süreçte zaten bulunduğunu varsayıyor

PDFium Component'te düz pdfium.dll ve XFA destekli pdfium.v8.dll ile AcroForm ve XFA belgelerinin dört bileşiminin şeması: sürüm 1 kaydı yalnızca V8 derlemesini düz bir formla bozuyor ve FPDFDOC_InitFormFillEnvironment içinde EPdfError üretiyordu, düzeltilmiş sürüm 2 kaydı ise dördünü de açıyor
Tek bir koşullu ifade ABI sürümünü belgeye bağlıyordu, bu yüzden süreç genelindeki V8 ikili seçimi sonraki her düz PDF'i başarısız bir ortam başlatmasına çeviriyordu

FPDF_FORMFILLINFO içindeki version alanı aslında neyi vaat ediyor?

FPDF_FORMFILLINFO.version, PDFium'a kaydın hangi alanlarını okumasına izin verildiğini söyler ve genel başlık fpdf_formfill.h, kabul edilebilir değerleri belgeye değil kütüphanenin nasıl derlendiğine bağlar. Kabaca ifade edersek sözleşmenin üç parçası var. Sürüm 1, FFI_Invalidate ile FFI_DoGoToAction arasındaki kararlı callback'leri ve m_pJsPlatform işaretçisini kapsar. XFA modülü olmayan bir derleme 1'i de 2'yi de kabul eder ve 2 ile ek deneysel callback'leri de çağırır. XFA modülü olan bir derleme 2 ister, nokta, ve başlık bu gereği insanların gözden kaçırmasını bekliyormuş gibi iki kez yineler. Sözleşme belgeden hiçbir yerde söz etmez. Sürüm, ayırdığınız kayıt hakkında bir beyandır: 2 verdiğinizde m_pJsPlatform sonrasındaki belleğin var olduğunu ve içinde ya geçerli fonksiyon işaretçileri ya da NULL bulunduğunu taahhüt edersiniz

Sürüm 2 bölgesi, XFA mekanizmasının tamamının yaşadığı yerdir. xfa_disabled ile başlar — başlığın sürüm 2 altında yok sayıldığını ve yalnızca XFA modülü derlenmişse anlamlı olduğunu söylediği bir FPDF_BOOL — ve FFI_DisplayCaret ile FFI_DoURIActionWithKeyboardModifier arasındaki on yedi fonksiyon işaretçisiyle sürer. Bunların her biri XFA için zorunlu, aksi hâlde NULL'a ayarlanacak diye belgelenmiştir. Bütün düzeltmenin anahtarı bu ifadedir. NULL, o yuvalar için bir hata durumu değil, XFA sürmeyen bir ana uygulama için belgelenmiş durumdur. FillChar ile temizlenip ardından sürüm 2 olarak işaretlenmiş bir kayıt, XFA olmayan bir derlemede sözleşmeyi tıpkı bir sürüm 1 kaydı kadar iyi karşılar ve XFA derlemesinin kabul edeceği tek kayıt da odur

PDFium Component'te Delphi'deki FPDF_FORMFILLINFO kaydının şeması: sürüm 1 FFI_Invalidate ile FFI_DoGoToAction arasındaki callback'leri ve m_pJsPlatform işaretçisini kapsar, sürüm 2 xfa_disabled ile FFI_DisplayCaret dönemi on yedi işaretçiyi ekler, FillChar her baytı temizler ve NULL yuvalar XFA sürmeyen bir ana uygulama için belgelenmiş durumdur
Pascal kaydı her zaman eksiksiz sürüm 2 düzenidir, bu yüzden XFA destekli bir derleme onu kabul eder, düz bir derleme ise NULL kalan deneysel yuvaları hiç çağırmaz

Eski seçim ABI'yi belgeye bağlıyordu

Kusur, tek başına bakıldığında makul görünen tek bir koşullu ifadeydi. TPdf.InitializeFormFill, RuntimeReady bayrağını üç olgudan hesaplıyor: belgenin TPdf.XFA üzerinden bir XFA form tipi bildirmesi, XFA string yardımcılarının XfaFeaturesAvailable üzerinden çözümlenmesi ve V8 dışa aktarımlarının V8FeaturesAvailable üzerinden çözümlenmesi. v3.116.0 öncesinde aynı bayrak sürümü de seçiyordu

// v3.115.0 ve öncesi: ABI sürümü belgeyi izliyordu
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

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

// ... ve runtime-eksik dalı onu yeniden sabitliyordu
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Başlık elinizdeyken okuyun, hata apaçık. RuntimeReady, her düz AcroForm belgesi için false olur, yani her düz belge sürüm 1 bildirir. pdfium.dll üzerinde bu sorun değildir. XFA destekli derleme olan pdfium.v8.dll üzerindeyse PDFium alanı kontrol eder, gereken 2'nin altında bulur ve null bir FPDF_FORMHANDLE döndürür; CheckPdf de bunu yukarıdaki istisnaya çevirir. Eski kodun niyeti savunmacıydı: sürüm 1'i koruyup XFA derlemesinin atanmamış sürüm 2 yuvalarını hiç okumamasını sağlamak. Başlığın zaten dışladığı bir soruna karşı savunma yaparken, başlığın açıkça uyardığı bir sorunu yarattı. Düzeltilmiş kod sürüme bir kez, baştan ve kaydın fiziksel olarak ne olduğundan karar veriyor

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinel: statik sayfa ağacını kullan
  if not FormFill then
    Exit;

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

  // Tam sürüm 2 kaydı yukarıda ayrılıp temizlendi. PDFium sürüm 2'yi
  // XFA olmadan da kabul eder ve XFA destekli her derlemede şart koşar;
  // bu belge hiç XFA formu içermediğinde de.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady XFA callback'lerini ve xfa_disabled'ı koşullar, sürümü asla.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

RuntimeReady hâlâ nereye ait: callback'ler ve xfa_disabled

RuntimeReady XFA davranışının kapısı olma işini koruyor; yalnızca artık kayıt düzenine dokunmuyor. Sürüm 1 callback'leri — FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction ve o bloğun geri kalanı — koşulsuz bağlanıyor, çünkü hem AcroForm hem XFA onlara bağlı. On yedi sürüm 2 işaretçisi ise yalnızca RuntimeReady dalının içinde, xfa_disabled := 0 ile birlikte atanıyor. Belge XFA olduğu hâlde runtime yoksa kayıt sürüm 2'de, xfa_disabled 1'de ve sürüm 2 yuvaları NULL kalacak biçimde durur; sarmalayıcı da OnXfaRuntimeMissing olayını tetikler, böylece ana uygulama pdfium.v8.dll ile yeniden başlatmayı önerebilir. Ortam var olduktan sonra FPDF_LoadXFA yalnızca RuntimeReady true idiyse çağrılır ve FXfaRuntimeUsable değerini yalnızca true dönüş ayarlar; TPdf.XfaRuntimeAvailable da bunu bildirir

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA etkin
    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 ile FFI_DoURIActionWithKeyboardModifier arası
  end
  else if XFA then
  begin
    // Runtime yok: sürüm 2'yi koru, XFA'yı kapalı bırak, ana uygulamaya haber ver.
    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;

Kendi binding'inizi yazarken bu bloktaki iki ayrıntıyı yanlış yapmak kolaydır. FXfaPageCountOverride, her şeyden önce bir sentinel olarak -1'e sıfırlanır, böylece FFI_PageEvent bir yeniden sayfalamayı bildirene kadar PageCount statik sayfa ağacına düşer; oradaki bir sıfır sessizce boş bir belge iddia ederdi. Sürüm 2 callback'lerinin her biri ayrıca, kayıttan sahibi olan TPdfyi bulan ve PDFium'a dönmeden önce her Pascal istisnasını yutan statik bir cdecl rutinidir; Delphi'de PDFium ABI'yi sağlamlaştırma notumuzun FFI_OpenFile için ayrıntılı anlattığı disiplin budur. Sürüm değişikliği bu iki kuraldan hiçbirini gevşetmiyor

DLL'de XFA modülü yokken sürüm 2 güvenli mi?

Evet ve nedeni kütüphanenin bir vaadinde değil, kaydın kendisinde. XFA olmayan bir derlemede başlık, sürüm 2'nin deneysel callback'lerin de çağrılmasına yol açtığını söyler; yani soru, PDFium baktığında ne bulduğudur. TPdfFormFillInfo, Info üyesi her sürüm 2 alanını içeren tam FPDF_FORMFILLINFO olan packed bir kayıttır ve InitializeFormFill tek bir bayta dokunmadan önce her şeyi FillChar ile temizler. Böylece düz bir pdfium.dll ve düz bir belge ile kütüphane sürüm 2'yi, ayarlı xfa_disabled değerini ve her deneysel yuvada NULL'u görür; bu da başlığın XFA uygulamayan bir ana uygulama için öngördüğü durumun tam olarak kendisidir. Kütüphanenin ötesini okuyacağı kesik bir kayıt yoktur, çünkü kayıt zaten hiçbir zaman sürüm 2'den kısa olmadı. Eski mantık, Pascal bildiriminin çoktan ortadan kaldırdığı bir düzen uyuşmazlığına karşı savunuyordu

Dürüstçe söylenmesi gereken sınır, kaydın kapsayamadığı sınırdır. Düz bir belgede sürüm 2, JavaScript'i, XFA scripting'i ya da o callback'lerin arkasındaki ana uygulama olaylarını açmaz. m_pJsPlatform yalnızca V8FeaturesAvailable true olduğunda bağlanır, XFA RuntimeReady true olmadıkça kapalı kalır ve TPdf.XFA, ortamın neyi müzakere ettiğinden bağımsız olarak form tipini FPDF_GetFormType üzerinden bildirmeye devam eder. Dinamik XFA'nın gerçekten render edilip edilmeyeceğini bilmek isteyen bir ana uygulama, sürüm alanından bir şey çıkarmak yerine Active true olduktan sonra XfaRuntimeAvailable değerini okumaya devam etmelidir; XFA formlarını algılama ve XFA packet'lerini çıkarma notumuzun önerdiği de budur

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Belge XFA olduğu hâlde yüklü pdfium.dll motoru çalıştıramadığında
  // InitializeFormFill içinden tetiklenir. Form ortamı yine açılır,
  // çünkü sürüm 2 her iki durumda da geçildi; yalnızca XFA runtime kapalı.
  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;   // pdfium.v8.dll altında düz bir PDF'te artık istisna fırlatmaz
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Protokol sürümü ve özellik kullanılabilirliği iki ayrı eksen

Bu düzeltmeden çıkan genel kural şu: bir callback yapısındaki sürüm alanı bu kayıt ne kadar büyük ve ondan neyi okuyabilirsin sorusunu yanıtlar, özellik algılaması ise o yuvalardan hangisi işe yarar bir şey yapar sorusunu. Birincisi yerel ikili ve derlendiğiniz Pascal bildirimi tarafından sabitlenir. İkincisi belgeye, DLL dışa aktarım tablosuna ve ana uygulama yapılandırmasına göre değişir. İkisini tek bir boolean'a indirmek caziptir, çünkü XFA durumu ikisine birden ihtiyaç duyar; ama bir derleme asgari bir sürümü zorunlu kıldığı anda bu indirgeme, o özelliğe ihtiyaç duymayan her belge için bozulur. ISO 32000-1 §12.7.8'de AcroForm sözlüğünün yanında yaşayan bir XML yükü olarak tarif edilen XFA formları burada özelliktir; kayıt düzeni ise protokoldür ve PDFium dosyaya hiç bakmadan önce düzeni talep etmeye hakkı vardır. Aynı biçim, bir C kütüphanesinin yapılarını sürümlediği her yerde karşımıza çıkar: bir viewer-info bloğu, bir render seçenekleri kaydı, bir platform callback tablosu. Güvenli desen, düzeltilmiş InitializeFormFillin izlediği desendir. Anladığınız en yeni düzeni bildirin, tamamen temizleyin, sürümü koşulsuz olarak o düzene uyacak biçimde ayarlayın ve hangi yuvaların doldurulacağına yetenek kontrolleri karar versin. Gelecekteki bir PDFium başlığı sürüm 3 eklerse değişiklik, kimsenin test etmediği bileşimde yanlış olacak belgeye bağlı bir dala değil, bildirime ve o tek atamaya olur

PDFium Component'te FPDF_FORMFILLINFO'nun arkasındaki iki ekseni ayıran şema: kayıt düzeni ve yerel ikili tarafından sabitlenen protokol sürümü ile belgeye ve ana uygulamaya göre xfa_disabled, on yedi sürüm 2 yuvası, FPDF_LoadXFA ve m_pJsPlatform değerlerini koşullayan RuntimeReady üzerinden özellik kullanılabilirliği
Sürüm alanı karşı tarafın okuyabileceği belleği tarif eder, yetenek kontrolleri hangi yuvaların işe yaradığına karar verir ve ikisini tek boolean'da birleştirmek asgari sürüm zorlayan derlemeyi bozar

Düzeltilmiş form-fill başlatması Delphi, Lazarus ve C++Builder için PDFium Component içinde geliyor ve her iki derleme de aynı kayıt bildirimini paylaştığı için Win32 ile Win64'te aynı şekilde geçerli. Uygulamanız JavaScript ile sürülen AcroForm'lar için pdfium.v8.dll seçimini zaten yapıyorsa, form ortamını özel olarak ele almadan PDF arşivinizin geri kalanını aynı ikili üzerinden açmasını sağlayan değişiklik budur