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
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
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
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