Teknik Makale

FPDF_FORMFILLINFO sürüm 2: Delphi'de DLL ABI'sini izleyin

Bir avuç metin alanı taşıyan, içinde hiç XFA olmayan bir AcroForm faturası, düz bir pdfium.dll üzerinden sorunsuz açılır; ama aynı dosya, proses XFA destekli pdfium.v8.dll'i çalıştırırken FPDFDOC_InitFormFillEnvironment'in NULL döndürmesine yol açabilir. Belgenin baytları iki koşuda da aynıdır. Farklı olan tek bir tamsayıdır: PDFium'un XFA derlemeleri FPDF_FORMFILLINFO.version'un 2 olmasını ister ve XFA'sız belgeler için 1 yazan bir bağlama, PDFium tek bir callback'e bakmadan kapıdan çevrilir

FPDFDOC_InitFormFillEnvironment düz bir AcroForm PDF'i neden reddeder?

Ret ikili dosyadan gelir, belgenin buna sözü yoktur. Form-fill başlığı, PDFium'daki fpdf_formfill.h ve onun FPdfFormFill.pas'taki Pascal çevirisi, XFA modülüyle derlenmiş bir derlemenin sürüm 2 istediğini söyler ve bunu aynı yorum bloğunda iki kez yineler. XFA'sız bir derleme 1'i de 2'yi de kabul eder. Belgeye göre sürüm gönderen bir bağlamada, ikili dosya ile belgenin dört kombinasyonundan yalnızca biri başarısız olur: sürüm 1 kaydın pdfium.v8.dll'e, sıradan bir AcroForm dosyasıyla birlikte verilmesi. Düz DLL o kaydı alır. V8 DLL her belge için sürüm 2 kaydını alır. Belgeye göre sürüm tuşlayan bir bağlama, gerçek bir XFA formunu asla sürüm 1 ile göndermez; dolayısıyla başarısız olan hücre, kimsenin test etmeyi aklına getirmeyeceği hücrenin ta kendisidir

Elle yazılmış bir bağlama o noktada FPDFDOC_InitFormFillEnvironment'in NULL döndürdüğünü görür ve elinde başka bir ipucu olmaz. PDFium Component'teki TPdf, reddedilen bir ortamı Active := True sırasında EPdfError olarak bildirirdi; ama her zaman sürüm 2 yazar, bu yüzden o hücre orada hiç oluşmaz. O hücreye düşmek kolaydır. Bir ana uygulama, AcroForm JavaScript'i istediği için genel EnableV8Engine'i True yapar ve o andan sonra proseste ki her PDF, V8 ikili dosyasından geçer. Ya da ana uygulama bayrağa hiç dokunmaz ama açtığı ilk dosyanın AcroForm sözlüğünde /XFA girdisi ya da kataloğunda /NeedsRendering true vardır: PDFium kütüphanesi henüz yüklenmemişken TPdf dosyayı o iki işaret için tarar ve DLL'i yüklemeden önce EnableV8Engine'i açar. Her iki durumda da seçim prosesin kalanı için geçerlidir ve sonradan açılan her düz AcroForm dosyası XFA derlemesiyle karşılaşır. En başta hangi ikili dosyanın dağıtılacağı PDFium DLL'ini dağıtma ve yükleme başarısızlıklarını teşhis etme kılavuzunda işlenir; aşağıdakiler V8 derlemesinin hâlihazırda yüklü olduğunu varsayar

PDFium Component şeması: düz pdfium.dll ve XFA destekli pdfium.v8.dll'nin AcroForm ve XFA belgelerine karşı dört kombinasyonu; sürüm 1 kayıt yalnızca V8 derlemesinde düz formla başarısız olur, FPDFDOC_InitFormFillEnvironment'te; sürüm 2 kayıt dördünü de açar
ABI sürümünü belgeye bağlamak, V8 ikili dosyasının proses genelindeki seçiminin sonraki her düz PDF'i başarısız bir ortam başlatımına çevirmesi demektir

FPDF_FORMFILLINFO'daki version alanı neyi vaat eder?

FPDF_FORMFILLINFO.version, PDFium'a kaydın ne kadarını okuyabileceğini söyler. Sürüm 1 stabil kısmı kapsar: Release, FFI_Invalidate'den FFI_DoGoToAction'a callback'ler ve m_pJsPlatform işaretçisi. Sürüm 2 deneysel bir kuyruk ekler. XFA'sız bir derleme sürüm 2 aldığında oradaki deneysel callback'leri de çağırır; XFA'lı bir derleme daha azını kabul etmez. Başlığı, belgeyi hiçbir yerde anmaz. 2 yazmak, m_pJsPlatform'dan sonraki belleğin var olduğunu ve içindeki her yuvanın ya geçerli bir cdecl fonksiyon ya da NULL tuttuğunu vaat etmektir

O kuyruk, sürüm 2 altında yok sayılan bir FPDF_BOOL olan xfa_disabled ile başlar, ardından FFI_DisplayCaret'ten FFI_DoURIActionWithKeyboardModifier'e on yedi fonksiyon işaretçisi gelir. On beşi — FFI_DisplayCaret'ten FFI_PutRequestURL'e — "XFA için zorunlu, aksi hâlde NULL" olarak belgelenmiştir. Son ikisi, FFI_OnFocusChange ile FFI_DoURIActionWithKeyboardModifier, her ana uygulama için isteğe bağlıdır ve başlık, FFI_DoURIActionWithKeyboardModifier'i boş bırakan bir sürüm 2 gömücünün URI tıklamalarının FFI_DoURIAction'a yönlendirileceğini ekler. Bir araya getirince: NULL, XFA sürmeyen bir ana uygulama için belirlenmiş değerdir. FillChar ile sıfırlanıp sürüm 2 damgalanmış bir kayıt, düz derlemede sözleşmeyi sürüm 1 kayıt kadar iyi karşılar; üstelik XFA derlemesinin kabul edeceği tek kayıttır

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

JavaScript tarafı aynı kalıbı izler. m_pJsPlatform, V8'siz derlemelerce kullanılmaz; V8 derlemesinde NULL bir işaretçi, belge çizilirken JavaScript'in çalışmasını durdurur. İşaret ettiği IPDF_JsPlatform kaydının kendi version alanı vardır ve başlık o alanın şu anda 2 olması gerektiğini söyler

Tuzak: ABI sürümünü belgenin seçmesine izin vermek

Hata, kendi başına makul okunan tek bir koşuldur. Elle yazılmış bir bağlamanın XFA callback'lerine değer verip vermeyeceğine zaten karar vermesi gerekir ve aynı testin sürüm numarasını da seçmesine izin vermek cazip gelir

// Elle yazılmış bir bağlama için cazip bir biçim: sürüm belgeyi izler
FillChar(FormInfo, SizeOf(FormInfo), 0);
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
if IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable then
  FormInfo.version := 2
else
  FormInfo.version := 1;   // XFA'sız her belge 1 alır

FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo);
// pdfium.v8.dll ile düz AcroForm dosyasında FormHandle burada nil'dir

O dalın ardındaki niyet savunmacıdır: sürümü 1'de tutarak bir XFA derlemesinin ana uygulamanın doldurmadığı yuvaları okumamasını sağlamak. Ama bir satır yukarıdaki FillChar onları çoktan NULL ile doldurmuştur; başlık ise NULL'u, XFA çalıştırmayan bir ana uygulama için doğru değer olarak tanımlar. Dal, bildirimin çoktan kaldırdığı bir tehlikeye karşı koruma kurar ve başlığın uyandırdığı tehlikenin tam içine yürür. Üstelik XFA dosyaları test ederek yakalanamaz; onlar her seferinde sürüm 2 yolunu izler

PDFium Component kaydı ters yönde kurar. TPdf.FormFill True olduğu her an, bir belge açmak kaydın tamamını temizler, sürüm 2 ile xfa_disabled := 1 yazar ve ancak ondan sonra belgenin neye ihtiyacı olduğunu sorar. Her bağlama için kural olarak: sürümü, bildirdiğiniz kayıttan ayarlayın ve ona başka hiçbir şey dokunmasın. Kendi kodunuz için o biçim şöyle; THostFormFillInfo sarmalayıcısı aşağıda bildiriliyor

// FormInfo, görüntüleyici nesnenizin THostFormFillInfo alanıdır; form
// handle'dan uzun yaşar; HostInvalidate, HostGetPage ve ötekiler sizin
// cdecl callback'lerinizdir
FillChar(FormInfo, SizeOf(FormInfo), 0);
FormInfo.Owner := Self;

FormInfo.Info.version := 2;        // bildirilen düzene uyar, her zaman
FormInfo.Info.xfa_disabled := 1;   // kullanılabilir olduğu kanıtlanmadıkça XFA kapalı kalır

FormInfo.Info.FFI_Invalidate := HostInvalidate;
FormInfo.Info.FFI_GetPage := HostGetPage;
FormInfo.Info.FFI_GetCurrentPage := HostGetCurrentPage;
// ... öteki sürüm 1 callback'leri

IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
RuntimeReady := IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
begin
  FormInfo.Info.xfa_disabled := 0;
  FormInfo.Info.FFI_DisplayCaret := HostDisplayCaret;
  FormInfo.Info.FFI_PageEvent := HostPageEvent;
  // ... kalan sürüm 2 yuvaları
end;

FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo.Info);
if FormHandle = nil then
  raise Exception.Create('Cannot initialize form fill environment');
XfaRunning := RuntimeReady and (FPDF_LoadXFA(Doc) <> 0);

RuntimeReady ile V8FeaturesAvailable nereye ait?

Yetenek kontrolleri hangi yuvaların doldurulacağına karar verir ve PDFium Component'te asla düzene dokunmaz. TPdf hazırlığı üç gerçeksiz hesaplar: belgenin TPdf.XFA üzerinden bir XFA form tipi bildirmesi, genel XfaFeaturesAvailable'in yüklenen DLL'in dışa açtığı XFA string yardımcılarını bulması ve V8FeaturesAvailable'in V8 dışa aktarımlarını bulması. Sürüm 1 callback'leri her belge için bağlanır; çünkü AcroForm da XFA da onlara yaslanır. On yedi sürüm 2 yuvasının tümü atanır ve xfa_disabled sıfırlanır, ancak üçü de tuttuğunda. Ortam kurulduktan sonra FPDF_LoadXFA yalnızca bu durum için çalışır ve yalnızca sıfırdan farklı bir dönüş TPdf.XfaRuntimeAvailable'in True bildirmesini sağlar. FPDF_LoadXFA, XFA motoru olmayan derlemelerce bile dışa açılır ve orada yalnızca false döndürür; tek başına dışa aktarım kontrolünün neden yetmediği budur

Belge XFA olduğu hâlde çalışma zamanı eksikse, kayıt sürüm 2'de kalır: xfa_disabled 1'dedir, kuyruk NULL'dur. Ortam açılır, XFA içeriği statik kalır ve TPdf, ana uygulamanın pdfium.v8.dll ile yeniden başlatmayı önerebilmesi için OnXfaRuntimeMissing raise eder. m_pJsPlatform üçüncü bir kuralı izler: V8FeaturesAvailable true olduğunda eklenir, düz AcroForm dosyaları için de; V8 derlemesi altında belge düzeyi script'lerin, alan hesaplamalarının ve app.alert'in çalışmasını sağlayan budur. PageCount, XFA belgeyi FFI_PageEvent ile yeniden sayfalayana dek statik sayfa ağacını okur; o noktada yeni toplam FPDF_GetPageCount'ten alınır ve OnXfaPageCountChanged ile bildirilir

Ana uygulama tarafında iki özellik iki farklı soruyu yanıtlar. XFA, ikili dosyadan bağımsız olarak, dosyanın ne içerdiğini FPDF_GetFormType'tan söyler. XfaRuntimeAvailable, motorun bu belge için gerçekten başlayıp başlamadığını söyler. İkincisini, XFA formlarını algılama ve XFA paketlerini çıkarma yazısının yaptığı gibi, Active True olduktan sonra okuyun ve version alanını muhakemeden tümüyle çıkarın

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Belge açılırken, XFA olduğu hâlde yüklenen DLL motoru
  // çalıştıramıyorsa tetiklenir. Form ortamı hâlâ vardır, XFA kapalıdır
  StatusBar.SimpleText :=
    'XFA form detected; restart with pdfium.v8.dll for 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;   // düz AcroForm dosyaları da pdfium.v8.dll altında açılır
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaNotice;   // kendi UI yordamınız
end;

Sürüm okunmadan kaydı bozan düzen hataları

Doğru bir sürüm numarası, ancak arkasındaki baytlar C derleyicisinin onları koyduğu yerdeyse işe yarar. Birkaç bağlama hatası, hangi sürümü taşırsa taşıdığı PDFium'un yanlış okuduğu bir kayıt üretir ve biri Win32'de görünmez kalır: FPDF_FORMFILLINFO'yu packed record olarak bildirmek. Win32'de her alan dört bayttır; packed ve hizalı düzenler tesadüfen çakışır. Win64'te C struct, Release'in 8 baytlık sınıra düşmesi için version'dan sonra dört bayt dolgu ekler ve xfa_disabled'dan sonra yeniden; packed bir Pascal bildirimi iki boşluğu da düşürür ve her callback işaretçisini dört bayt kaydırır. FPdfFormFill.pas'taki bildirim bu yüzden düz bir record'dur

İkinci hata kesilmiş bir kayıttır: m_pJsPlatform'da duran ama sürüm 2 yazan bir çeviri. PDFium sonra kuyruğu, kaydı izleyen bellekten ne çıkarsa ondan okur. Üçüncüsü ömürdür. Başlık, FPDFDOC_InitFormFillEnvironment'e verilen kaydın, döndürülen form handle kapatılana dek geçerli kalması gerektiğini söyler; dolayısıyla belgeyi açan prosedürün yerel değişkeninde tutulan bir kayıt, o prosedür dönünce sarkan işaretçidir. TPdf kaydını bileşenin alanı olarak tutar; PDFium struct'ı ofset sıfıra, ona hemen ardından sahip TPdf'e geri işaret eden bir işaretçiye yerleştiren bir dış kayıtla sarar — her statik callback'in nesnesini yeniden bulması böyle olur

type
  // Win64'te yanlış: packing, C struct'ın version'dan sonraki dolgusunu
  // kaldırır; Release burada ofset 4'te, PDFium'da ofset 8'de oturur
  TBadFormFillInfo = packed record
    version: Integer;
    Release: Pointer;
    // ... kalan alanlar
  end;

  // Kendi sarmalayıcınız: önce PDFium kaydı, ardından geri işaretçiniz
  THostFormFillInfo = record
    Info: FPDF_FORMFILLINFO;   // ofset 0; pThis doğrudan geri dönüştürülür
    Owner: TObject;
  end;
  PHostFormFillInfo = ^THostFormFillInfo;

procedure HostInvalidate(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE;
  left, top, right, bottom: Double); cdecl;
begin
  try
    // TMyViewer ve InvalidatePdfRect kendi görüntüleyici sınıfınızı temsil eder
    TMyViewer(PHostFormFillInfo(pThis).Owner).InvalidatePdfRect(
      page, left, top, right, bottom);
  except
    // bir Pascal istisnası PDFium'un C++ çerçevelerinden geri açılmamalı
  end;
end;

İki yuva daha dikkat ister. FFI_GetLocalTime, 16 baytlık bir FPDF_SYSTEMTIME'i değerle döndürür ve Delphi ile MSVC, Win32'de gizli sonuç işaretçisinin nereye gittiği konusunda anlaşmaz; bir Pascal gerçeklemesi çağıranın FPDF_FORMFILLINFO'sunu bozar. Güncel PDFium üyeyi kullanılmıyor işaretler ve Pascal bildirimi onu nil bırakma notuyla taşır; PDFium Component öyle yapar. Bileşenin bağladığı her callback, Pascal istisnalarını yakalar ve yerine belgelenmiş başarısızlık değerini döndürür; çünkü C ABI'sinin bir istisnayı sınırın ötesine geri taşıma yolu yoktur. Delphi'de PDFium ABI'sini sağlamlaştırma yazısı o disiplini FFI_OpenFile örneğiyle gezdirir

Protokol sürümü ile özellik erişilebilirliği iki ayrı eksen

Callback yapısındaki bir version alanı, kaydın ne kadar büyük olduğunu ve ne kadarının okunabileceğini yanıtlar. Özellik saptaması, o yuvaların hangisinin bir işe yarayacağını yanıtlar. İlki, yerel ikili dosya ve derlediğiniz bildirimle sabittir; ikincisi belgeye, DLL'in dışa aktarım tablosuna ve ana uygulamanın ayarına göre değişir. XFA durumu iki yanıtı aynı anda ister; tek bir boolean'a katıştırmak cazip gelir ve o katıştırma, bir derleme asgari sürüm dayattığı anda kırılır. XFA formları — ISO 32000-1 §12.7.8'in, AcroForm sözlüğünün yanında taşınan bir XML yükü diye tarif ettiği — bu hikâyedeki özelliktir. Kayıt düzeni protokoldür ve PDFium, dosyayı okumadan önce düzene bakmaya haklıdır

PDFium Component şeması: FPDF_FORMFILLINFO'nun arkasındaki iki ekseni ayırır; kayıt düzeniyle yerel ikili dosyayla sabitlenen protokol sürümü ve özellik erişilebilirliği; RuntimeReady xfa_disabled'ı, on yedi sürüm 2 yuvasını ve FPDF_LoadXFA'yı koşullarken V8FeaturesAvailable m_pJsPlatform'a karar verir; belgeye ve ana uygulamaya göre
Bir version alanı karşı tarafın okuyabileceği belleği tarif eder, yetenek kontrolleri hangi yuvaların işe yarayacağına karar verir; ikisini tek boolean'a katıştırmak asgari sürüm dayatan derlemeyi kırar

Aynı yapı, bir C kütüphanesi struct'larına sürüm verdiği her yerde karşınıza çıkar: FPDF_LIBRARY_CONFIG, IPDF_JsPlatform, boyut ya da sürüm başlığı taşıyan her callback tablosu. Anladığınız en yeni düzeni bildirin, onu tümüyle temizleyin, eşleşen sürümü koşulsuz yazın ve yuvaları belgenin ve DLL'in gerçekten yapabildiklerine göre doldurun. Gelecekte fpdf_formfill.h sürüm 3 getirirse iş, yeni bir bildirimle tek bir değişmiş atamadır. Belge içeriğine göre tuşlanmış bir dalın, derleme ile dosyanın her kombinasyonu için yeniden gözden geçirilmesi gerekirdi; yanlış yaptığı kombinasyon da genelde kimsenin açmadığı olandır

Burada anlatılan kayıt işlemesi, Delphi, Lazarus ve C++Builder için PDFium Component'in parçasıdır; aynı bildirim hem Win32 hem Win64 derlemelerine hizmet eder. JavaScript'li AcroForm'lar için zaten pdfium.v8.dll dağıtan bir uygulama, arşivinin geri kalanını — XFA'lı da değil de — form ortamına özel işlem yapmadan o tek ikili dosya üzerinden açabilir