Teknik Makale

PDFium: form-field navigation and viewer validation

Kodunuzun oluşturduğu bir PDF formunda Tab tuşuna basın; imleç olması gereken yerden iki alan uzağa iner, ya da ikinci sütunu tamamen atlar, ya da üçüncü alandan sonra dördüncü yerine en başa döner. Görüntüleyicinizde bir fatura dolduran kişi, klavyenin formu, şimdiye kadar kullandığı her web formunda yürüdüğü gibi yürümesini bekler. Bu olmadığında fareye uzanır, bir sonraki kutuyu arar ve sessizce aracınızın bitmemiş olduğuna karar verir. Öngörülebilir alan gezinimi, insanların katlandığı bir veri girişi görüntüleyicisi ile güvendikleri bir görüntüleyici arasındaki farktır ve neredeyse tamamen simüle edilmiş tıklamalarla klavye girdisini taklit etmek yerine doğru odak API'sini kullanma meselesidir

Aşağıdaki örnekler, Delphi, C++Builder ve Lazarus için PDFium tabanlı bir VCL/LCL bileşeni olan PDFium Component'i kullanır. Gezinim, bir form görüntüleyicisinin doğru yapması gereken üç şeyden biridir; diğer ikisi, formu doğru açmak ve doldurulmuş değerleri gerçekten görünecek şekilde kaydetmek, sürprizlerin çoğunun saklandığı yerdir, bu yüzden aşağıda üçü de ele alınmıştır

Bir formu açmak: FormFill, FormType ve XFA sorusu

Alan erişimi, belge açılmadan önce FormFill özelliğiyle kontrol edilen form doldurma alt sisteminin etkinleştirilmesini gerektirir. Etkinleştirildikten sonra FormType ne tür bir formla karşı karşıya olduğunuzu söyler ve cevap, vaat edebileceğiniz özellik kümesini değiştirir:

Bir Delphi PDFium Component görüntüleyicide FormFill kurulumu ve FormType algılama dallarının şeması: ftNone, ftAcroForm ve ftXfaFull işlemesini ayırır
FormType, FormFill etkinleştirildiğinde dallanır ve her dal farklı bir özellik kümesi vadedir
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // Active'den önce etkinleştirin; herhangi bir alan erişimi için gereklidir
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // tam alan gezinimi ve düzenleme mevcuttur
  ftXfaFull:
    ShowXfaNotice;      // XFA kendi XML şablonundan render edilir;
                        // alan düzenlemesini sınırlı sayın
end;

Bu anahtardan iki pratik not çıkar. AcroForm, standart ISO 32000 form modelidir ve buradaki her API'nin hedeflediği şey budur. XFA belgeleri kendi XML form mimarilerini gömer, bu yüzden hızlı bir AcroForm demosundan sonra bir müşteriye tam XFA düzenleme vaat etmek pişman olacağınız bir taahhüttür. İkinci not yan etkilerle ilgilidir: FormFill'i True olarak ayarlamak aynı zamanda belge JavaScript'ini de başlatır. Bir veri girişi görüntüleyicisinde bu tam olarak doğrudur, çünkü hesaplama betikleri, biri yazarken çalışan bir toplamı güncel tutan şeydir. Kökeni bilinmeyen dosyalar için bir önizleme penceresinde ise tam olarak yanlıştır. Güvenli PDF önizleme makalesi, bu ödünleşimin FormFill := False tarafını ele alır

Kullanıcıların beklediği yere inen Tab tuşu gezinimi

Başlangıçtaki klavye problemine geri dönelim. Cazibe, bir sonraki widget'ın dikdörtgeninde bir fare tıklaması sentezleyerek Tab'ı taklit etmektir, ki bu bir alan ekran dışına kaydırıldığı ya da iki widget çakıştığı anda bozulur. Odak API'si bunun yerine formun kendi odağını, hiçbir geometri tahmini olmadan doğrudan taşır. Beş çağrı bunu kapsar: indekse göre FocusFormField, adım adım ilerlemek için FocusNextFormField ve FocusPreviousFormField, nerede olduğunuzu okumak için FocusedFormFieldIndex ve odağı tamamen bırakmak için ClearFormFieldFocus

Bir Delphi PDFium Component görüntüleyicide Tab tuşu odak gezinmesinin şeması: FocusNextFormField tek sayfanın sekme sırası içinde sarar ve beş odak API'si klavye gezinmesini kapsar
Gezinme tek bir sayfanın sekme sırası içinde döngülenir; bir sonraki sayfaya geçmek görüntüleyicinin işi olarak kalır
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // örn. "Field 4 of 17: InvoiceDate"
end;

İnsanları takılmasına neden olan tek davranış parçası sarmalamadır. Gezinim, geçerli sayfanın tab sırası boyunca çalışır ve bunun içinde döngü yapar: son alanı geçin, ilkine geri dönmüş olursunuz. Her iki adım fonksiyonu da yeni alan indeksini döndürür, ya da sayfa hiç alan içermiyorsa -1 döndürür. Bu döngü sayfa başınadır, belge başına değildir, bu da bir sonraki sayfaya geçmenin sizin işiniz olduğu, kütüphanenin işi olmadığı anlamına gelir. Döndürülen indeksi başladığınızla karşılaştırın, ne zaman sarmaladığını fark edin ve form tek bir sürekli sıra olarak okunmak üzere tasarlanmışsa PageNumber'ı kendiniz ilerletin. Bu kontrolü atlarsanız, iki sayfalık bir form imleci sessizce birinci sayfada hapseder, ki bu da bozuk-Tab şikayetinin kendi çeşididir

Gezinim, arayüzün geri kalanı ona tepki verdiğinde faydalı hale gelir. OnFormFieldEnter olayı odak vardığında tetiklenir ve görüntüleyicide OnFormFieldFocusChange yeni alan indeksini bildirir, böylece bir yan panel klavyenin az önce seçtiği şeyle adım adım kalabilir. Ters eşlemeye, bir ekran konumundan bir alana ihtiyaç duyduğunuzda, indeksli FormFieldAt özelliği araç ipucu önizlemeleri ve tıkla-düzenle panelleri için isabet testini yapar. Bunların hepsinde sessiz bir erişilebilirlik kazancı var: odak belgenin kendi alan sırasını izlediğinden, Tab tuşu için bağladığınız yol, hiçbir ek çalışma olmadan bir ekran okuyucunun anons ettiği aynı yoldur

Ham indeks numaraları yerine alan adlarını göstermek bir özellik daha gerektirir. FormFieldInfo[], indeks başına alan adını, türünü, font boyutunu, işaretli durumunu, dışa aktarma değerini ve grup üyeliğini taşıyan bir TPdfFormFieldInfo kaydı döndürür, ki bir gezinim listesinin göstermesi gereken de budur ("4" yerine "Field 4 of 17: InvoiceDate"). Radyo grupları özel bir test dosyasını hak eden bir durumdur. Birkaç widget tek bir alan adını paylaşabilir, bu yüzden widget'lardan saf bir şekilde derlenen bir liste aynı grubu birkaç kez gösterir ve onu okuyan herkesi şaşırtır

Doldurulmuş değerler neden boş çıkıyor ve bunu düzelten çağrı

Destek kuyruklarını dolduran diğer şikayet, kötü davranan bir Tab tuşundan daha alarm verici: bir form programatik olarak doldurulur, müşteri onu Acrobat'ta açar ve her alan boş görünür. Bir alana tıklayın, değeri hemen görünür hale gelir. Veri, tüm bu süre boyunca dosyadadır. Eksik olan verinin resmidir ve neden, bir kez anlaşılmaya değer, çünkü bütün bir hata ailesini açıklar

Bir AcroForm metin alanı, değerini alan sözlüğünün /V girdisinde saklar (ISO 32000-1 §12.7.3.3). Bir görüntüleyicinin gerçekte boyadığı şey ayrı bir şeydir: /AP altındaki widget'ın görünüm akışı (§12.5.5), küçük, önceden render edilmiş bir içerik parçası. /V'yi yazın ve /AP'ye dokunmayın, ikisi birbirinden uzaklaşır. Değer oradadır; onun render edilmiş sürümü ise bayat ya da yok. Acrobat, bir alan odak kazandığında onun görünümünü yeniden oluşturur, ki bu yalnızca tıklamada görünen değerlerin tüm açıklamasıdır. Görüntüleyicilerden görünümleri sizin için yeniden oluşturmalarını isteyen eski NeedAppearances bayrağı hiçbir zaman düzgün çalışmadı ve PDF 2.0'da kullanımdan kaldırıldı, yazıcı sunucuları ve küçük resim oluşturucuları onu tamamen görmezden gelir. Yalnızca /AP'yi boyarlar, başka hiçbir şeyi değil, bu yüzden /AP boşsa boş bir kutu yazdırırlar

FormField[i] aracılığıyla bir değer atamak yalnızca /V yazar. Bu yüzden bir formu doldurmak üç adımlık bir dizidir ve ekiplerin düşürdüğü adım ortadaki adımdır:

AcroForm alanlarında /V değeri ile /AP görünüm kaymasının ve GenerateFormAppearances etrafında kurulu üç adımlı Delphi doldurma dizisinin şeması
Değer atamak yalnızca /V yazar ve orta adım, baskı sunucularının gerçekten işlediğini yeniden boyayandır
procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // yalnızca /V yazar

  // /AP görünüm akışlarını yeniden oluşturur; bu olmadan form
  // her alan tıklanana kadar Acrobat'ta boş görünür
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances, düzeltmenin tamamıdır. Geçerli değerlerden, fontlardan ve hizalamadan her widget'ın görünüm akışını yeniden oluşturur, böylece hiçbir zaman bir odak olayı çalıştırmayan bir görüntüleyici, bir yazıcı sunucusu ya da bir küçük resim oluşturucu, yine de doldurulmuş durumu boyar. Onu atama toplu işinden sonra bir kez çağırın, alan başına bir kez değil. Görünüm oluşturma gerçek bir düzen işi yapar ve alan başına çağrılar bunu büyük bir formda boşuna çoğaltır

Görünümleri yeniden oluşturmak aynı zamanda fontların ve hizalamanın kendini gösterdiği andır, ki bu da ikinci derece bir sürprizin kaynağıdır. Yeni akış, her değeri widget dikdörtgeninin içine, alanın fontunu, boyutunu ve hizalamasını kullanarak yerleştirir. Test formunuzda rahatça oturan bir değer, aynı alanın daha dar olduğu bir müşteri kopyasında kırpılabilir veya küçülebilir. Otomatik boyutlandırılmış alanlar (font boyutu sıfır) metni sığdırmak için küçültür; sabit boyutlu alanlar ise onu yalnızca kırpar. İkisi de geçerlidir ve verilen bir formun hangisini yaptığını bilmenin tek dürüst yolu, yazdığınız dizeye değil, yeniden oluşturulan çıktıya bakmaktır. Birisi bir kutunun kenarında kesilmiş metin bildirdiğinde, neredeyse her zaman neden budur

Doğrulamayı işin bitirilmesinin bir parçası olarak ele alın, sonradan akla gelen bir şey değil. Kaydedilmiş dosyayı Acrobat'ta açın ve herhangi bir alana dokunmadan önce değerlerin görünür olduğunu doğrulayın. Ardından onu form mantığını tamamen görmezden gelen farklı bir görüntüleyiciden PDF'e ya da bir görüntüye yazdırın ve değerlerin bu yolu da atlattığını doğrulayın. İkisi birlikte, /V'ye karşı /AP kaymasının her varyantını yakalar

Demoyu geçen ve sahada başarısız olan alan yapılandırmaları

Temiz demo formları, müşteri dosyalarının gizlemediği bir dizi uç durumu gizler. Bunlardan dördü "benim makinemde çalıştı" raporlarının çoğunu oluşturur

  • Onay kutusu dışa aktarma değerleri. "Açık" durum her zaman Yes değildir. Bir form kendi dışa aktarma değerini tanımlamakta özgürdür ve yanlış dizeyi yazmak, kodunuz onu ayarladığına ikna olmuşken kutuyu görsel olarak işaretsiz bırakır. Bir değer varsaymak yerine dışa aktarma değerini FormFieldInfo[]'dan okuyun
  • Paylaşılan adlı radyo grupları. Bir alan, birkaç widget. Atadığınız değer, hangi widget'ın seçili olarak okunacağına karar verir, bu yüzden bir adın bir dikdörtgene eşlendiğini varsayan arayüz kodu, odak halkasını yanlış düğmeye çizmekle sonuçlanır
  • Hesaplanan alanlar. Belge JavaScript'i tarafından tutulan toplamlar, alan olaylarına yanıt olarak güncellenir. Bu olayları atlayan programatik bir doldurma, ya yeniden hesaplamayı tetiklemeli ya da hesaplanan alanların üzerine doğrudan yazmalıdır. Kalem kalemler ile toplamın uyuşmadığı bir form, iki düzeltmenin herhangi birinden daha kötüdür
  • Gizli zorunlu alanlar. Koşullu formlar, hâlâ zorunlu olarak işaretlenmiş alanları gizler. Doğrulamanızın görünürlüğe mi yoksa ham zorunlu bayrağına mı saygı göstereceğine önceden karar verin, ardından bu kararı desteğin bulabileceği bir yere yazın

Sizi ısırmadan önce çözülmeye değer bir ayrım var: görünüm oluşturmak düzleştirmek değildir. GenerateFormAppearances, alanları düzenlenebilir bırakırken değerleri her yerde görünür yapar. Düzleştirme ise görünümü statik sayfa içeriğine gömer ve etkileşimi kalıcı olarak çıkarır, ki bu bir arşiv kopyası için doğru, bir sonraki kişinin hâlâ doldurması gereken bir form için yanlıştır. FormType, ftAcroForm yerine ftXfaFull bildiriyorsa, buradaki düzenleme yüzeyinin hiçbiri zaten temiz bir şekilde uygulanmaz, çünkü belge kendi XML şablonundan render edilir; bu durumu tespit edin ve kullanıcının sınırı kendi başına bulmasına izin vermek yerine ona söyleyin

Burada gösterilen form doldurma alt sistemi, odak gezinimi ve görünüm oluşturma, Delphi, C++Builder ve Lazarus/FPC için PDFium Component'in bir parçasıdır. Görüntüleyiciniz form verisinin yanı sıra inceleyici işaretlemesini de ele alıyorsa, açıklama incelemesi makalesi bu komşu modeli ele alır