Delphi, C++Builder ve Lazarus için PDFium tabanlı VCL/LCL bileşeni olan PDFium Component'te, bir form alanı indeksi bir annotation indeksi değildir. Bir sayfa, widget'larının yanında Link, Text ve Ink annotation'ları taşır, bu yüzden alan numaralandırması FPDFAnnot_GetSubtype'a göre filtrelemeli ve yalnızca native çağrıda gerçek bir annotation konumuna geri eşlenen sıfır-tabanlı mantıksal bir indeks sunmalıdır
Bunu ortaya çıkaran hata, bir kez görüldükten sonra karıştırılamaz. Bir test uzmanı doldurulmuş bir fatura formunda Tab'a basar ve imleç kaybolur, çünkü odak alt bilgideki bir köprüye gitmiştir. Ya da daha kötüsü, hiçbir şey olmaz: kodunuz alan 3'ü odaklanmış olarak kaydeder, UI paneli güncellenir ve FORM_SetFocusedAnnot tüm süre boyunca sessizce false döndürür. Her iki belirti de aynı tasarım hatasından gelir ve birinin altında gizli bir ikinci kök neden vardır
PDFium'un size verdiği iki indeks uzayı
PDFium aynı sayfa üzerinde iki numaralandırma şeması sunar ve bunlar yalnızca içinde form widget'larından başka bir şey bulunmayan dokümanlarda çakışır. Birincisi annotation indeksidir: sayfa /Annots dizisindeki bir konum, ki bu FPDFPage_GetAnnotCount'un saydığı ve FPDFPage_GetAnnot'un aldığı şeydir (ISO 32000-1 §12.5.2). İkincisi, uygulama-seviyesi bir API'nin sunması gereken, bir kullanıcının gerçekten ulaşabileceği etkileşimli alanlar üzerinden sıfırdan başlayan mantıksal alan indeksidir. ISO 32000-1 §12.5.6.19, widget annotation'larını etkileşimli form alanlarının görsel temsili olarak tanımlar ve §12.7 formun kendisini tanımlar. Sayfadaki geri kalan her şey farklı anlambilime sahip farklı bir alt türdür: bir Link annotation'ının bir hedefi vardır, bir Ink annotation'ının bir vuruş listesi vardır, bir Text annotation bir yapışkan nottur. Hiçbiri bir alan sayısına ait değildir ve hiçbiri form odağını kabul edemez. Yine de /Annots dizisinde, üreten uygulamanın onları yazdığı sırayla, sıklıkla doküman hakkında başka hiçbir şeyin önermediği bir sırayla, widget'larla iç içe otururlar
Tab neden bir sonraki alan yerine bir köprüye iner?
Çünkü alan sayısı gerçekte bir annotation sayısıydı. Orijinal uygulama, FormFieldCount'tan doğrudan FPDFPage_GetAnnotCount döndürüyordu; alan bilgisi erişimcisi, sekme sırası yardımcısı ve odak yardımcısı ise hepsi aynı tamsayıyı bir widget konumu olarak ele alıyordu. Altı widget'ı ve başka hiçbir şeyi olan temiz bir AcroForm sayfasında altı altıya eşittir ve her test geçer. Alt bilgiye bir köprü ve kenar boşluğuna bir gözden geçiren yorumu ekleyin, sayı sekiz alan bildirir, 6 ve 7 indeksleri form-dışı nesnelere çözülür ve Tab doğrudan onlara girer
Numaralandırma ucundaki düzeltme, annotation'lar yerine alt türleri saymaktır. Her annotation'ı açın, alt türünü sorun, widget'ları tutun ve tanıtıcıyı bir finally bloğunda kapatın, çünkü FPDFPage_GetAnnot, FPDFPage_CloseAnnot üzerinden geri gönderilmesi gereken sahipli bir tanıtıcı döndürür
function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
Count, I: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := 0;
if Page = nil then
Exit;
Count := FPDFPage_GetAnnotCount(Page); // every annotation, not just fields
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
Inc(Result);
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Bunun kasıtlı olarak yapmadığı şeye dikkat edin. Form-fill ortamına hiçbir şey sormaz ve bir form tanıtıcısına ihtiyaç duymaz, çünkü alt tür annotation sözlüğünde yaşar ve yalnızca sayfadan okunabilir. Bu sıralama için önemlidir: sayı, dokümanın bir form-fill ortamını hak edip etmediğine karar vermeden önce mevcuttur; AcroForm JavaScript ve host olayları üzerine yazı bunu bir kolaylık değil bir güvenlik kararı olarak ele alır
Mantıksal indeksi native sınırda geri eşlemek
İki uzayın birbirine sızmasını önleyen kural basittir: mantıksal indeks, genel API'nizi geçen tek sayıdır ve native çağrıdan önceki son fonksiyonda bir annotation indeksine dönüştürülür. Alan bilgisi, odak, bayrak ayarlayıcıları ve sekme sırası tarafından ortak kullanılan tek bir eşleme yardımcısı, bu kuralı uygulanabilir kılan şeydir
function AnnotationIndexForField(Page: FPDF_PAGE;
FieldIndex: Integer): Integer;
var
Count, I, Current: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := -1;
if (Page = nil) or (FieldIndex < 0) then
Exit;
Count := FPDFPage_GetAnnotCount(Page);
Current := 0;
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
begin
if Current = FieldIndex then
Exit(I); // real /Annots position: native calls only
Inc(Current);
end;
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Bu yardımcının iki özelliği açıkça belirtilmeye değer. Doğrusal bir taramadır, bu yüzden yüzlerce widget'lı bir sayfada her alan üzerinde saf bir döngü, ikinci dereceden bir annotation açma sayısına mal olur; tüm sayfayı numaralandırıyorsanız, eşleyiciyi alan başına çağırmak yerine annotation'ları bir kez gezin ve widget tanıtıcılarını yol boyunca toplayın. Ve exception fırlatmak yerine -1 döndürür; bu, çağıranın eski bir indeksin bir exception'a değer bir programlama hatası mı yoksa yoksayılmaya değer bir yarış mı olduğuna karar vermesine izin verir, örneğin bir düzenlemenin önbelleğe alınmış bir UI listesinin hâlâ referans verdiği bir annotation'ı kaldırmasından sonra
FORM_SetFocusedAnnot neden headless bir sayfada başarısız olur?
Çünkü PDFium, sayfa görünümü hiçbir zaman geçerli olarak işaretlenmemiş bir widget'a odaklanmayı reddeder. FORM_SetFocusedAnnot, annotation'ı form-fill ortamı içinde bir sayfa görünümüne çözer ve o sayfa görünümü mevcut değilse hiçbir tanılama olmadan false döndürür. İndeks eşlemesini tek başına düzeltmek bu yüzden Tab'ın bir köprüye inmesini düzeltir ama ikinci belirtiyi dokunulmamış bırakır: mantıksal odak kaydınız alan 3'ü söyler, native odaklanmış widget hâlâ hiçbir şeydir ve native odak üzerine kurulu her erişimci — odaklanmış metin, odaklanmış değer, seçim durumu — boş döndürmeye devam eder. Sayfa görünümü FORM_OnAfterLoadPage tarafından oluşturulur ve FORM_OnBeforeClosePage tarafından yok edilir. Görsel bir kontrol etrafında kurulu bir görüntüleyicide bu çağrılar bir sayfayı görüntülemenin parçası olarak gerçekleşir; bu, hatanın neden bu kadar sık yalnızca-headless bir hata gibi göründüğünün nedenidir: GUI demosunda çalışan aynı kod toplu araçta başarısız olur. Yaşam döngüsü görüntüleyiciye değil doküman nesnesine aittir, bu yüzden PDFium Component artık bir form tanıtıcısı mevcutken bir sayfa her yüklendiğinde ya da kaldırıldığında her iki çağrıyı da yayınlar. C imzası önce sayfayı sonra form tanıtıcısını alır, ki bu binding'i elle yazarken tersine çevirmesi kolaydır
procedure ReportFirstField(const FileName: string);
var
Pdf: TPdf;
Idx: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FormFill := True; // form-fill environment, before Active
Pdf.FileName := FileName;
Pdf.Active := True;
Pdf.PageNumber := 1; // page load also runs FORM_OnAfterLoadPage
Idx := Pdf.FocusNextFormField; // logical index, 0-based over widgets
if Idx < 0 then
Exit; // page holds no widget annotations
Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
string(Pdf.FocusedFormFieldValue)); // reads the native focused widget
finally
Pdf.Free; // page unload runs FORM_OnBeforeClosePage
end;
end;
Düzeltmeyi kanıtlayan kontrol, iki tarafı karşılaştıran kontroldür. FocusFormField'i mantıksal bir indeksle çağırın, ardından kendi kaydınız yerine native odaklanmış widget üzerinden geçen bir erişimci aracılığıyla, FocusedFormFieldValue ya da FocusedFormOptionSelected gibi, bir değer okuyun. Mantıksal indeks gidip geliyorsa ama native erişimci boş dönüyorsa, eksik olan sayfa görünümüdür, eşleme değil
Mantıksal alan indeksinin size vaat etmediği şey
Sıfır-tabanlı bir alan indeksi bir kolaylıktır, anlamsal bir kimlik değil, ve bundan dört sınır çıkar. Sayfa başınadır, doküman başına değil, bu yüzden sayfa 2'deki indeks 0, sayfa 1'deki indeks 0'dan farklı bir widget'tır ve onları karşılaştırmak anlamsızdır. Konumsaldır, bu yüzden bir annotation eklemek ya da silmek, değişikliğin üzerindeki her önbelleğe alınmış indeksi geçersiz kılar; kayıtlı bir indeksi yalnızca sayfa yüklü ve düzenlenmemiş kaldığı sürece geçerli sayın
Üçüncü sınır, bir alan listesini gözden geçiren insanları şaşırtan sınırdır. İndeks widget'ları numaralandırır, alanları değil. Bir radio grubu, birkaç widget çocuğu olan tek bir alandır, bu yüzden üç-düğmeli bir grup, hepsi aynı Name'i bildiren üç ardışık indeks katkısında bulunur. TPdfFormFieldInfo kaydı, tam olarak bu durum için GroupCount ve GroupIndex taşır ve bunları yoksayan bir liste UI'si aynı alanı üç kez gösterir. Dördüncü sınır gezinme sırasıyla ilgilidir: burada açığa çıkan sekme sırası, /Annots dizisini takip eden widget numaralandırma sırasıdır, sayfa /Tabs girdisini (ISO 32000-1 §7.7.3.3) ya da AcroForm alan ağacını değil. Çoğu üretici için bunlar uyuşur; sağ sütunu önce yayınlayan bir üreteç tarafından iki sütuna yerleştirilmiş bir form için uyuşmaz ve her indeks doğru olsa bile form alanı gezinme yazısında anlatılan klavye yolu yanlış hissettirir. Bir müşteri dosyası tuhaf davrandığında, teori kurmadan önce her iki indeks uzayını yan yana dökün: aynı sayfanın annotation görünümü ve alan görünümü, birlikte yazdırıldığında, genellikle nedeni bir bakışta belirgin kılar
procedure DumpIndexSpaces(Pdf: TPdf);
var
I: Integer;
Info: TPdfFormFieldInfo;
begin
for I := 0 to Pdf.AnnotationCount - 1 do
Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));
for I := 0 to Pdf.FormFieldCount - 1 do
begin
Info := Pdf.FormFieldInfo[I];
Writeln('field ', I, ': ', string(Info.Name),
' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
end;
end;
Alan sayısının çok üzerinde bir annotation sayısı, sayfanın alt türleri karıştırdığı anlamına gelir; ki bu gözden geçirilmiş dokümanlarda normaldir ve eşlemenin tam olarak var olduğu durumdur; annotation gözden geçirme iş akışı yazısı aynı sayfaya işaretleme tarafından bakar. Öte yandan her test dosyasında eşit sayılar, fixture'larınızın bu hata sınıfını hiç tespit edemeyeceği anlamına gelir ve dürüst yanıt, bir link ve bir yapışkan not taşıyan bir form fixture'ı eklemektir
Burada anlatılan alan numaralandırma, odak ve annotation API'leri, Delphi, C++Builder ve Lazarus için PDFium Component ile birlikte gönderilir; ürün sayfası, alan bilgisi kaydı ve odak erişimcileri dahil tam form-alanı referansını içerir