Teknik Makale

PDF Checkbox Flatten Hatası: Delphi'de Alan Değeri ve Widget

Checkbox'lar ve radio butonları işaretsiz olarak düzleşir, çünkü görünüm durumu /AS hiçbir zaman alan değeri /V ile senkronize edilmedi. Delphi, C++Builder ve Lazarus için PDFium tabanlı VCL ve LCL bileşeni olan PDFium Component, artık o değeri, widget annotation yerine üst alan sözlüğünü çözen FPDFAnnot_GetFormFieldValue ile okuyor

Buraya götüren hata raporu, ilk başta güvenmediğiniz türden bir rapordur. Bir müşteri imzalanmış bir onay formunu düzleştirir, sonucu açar ve her checkbox boştur. Kaynak dosyayı Acrobat'ta açın, kutular görünür biçimde işaretlidir. Kaynak dosyayı aynı bileşen üzerinden geri okuyun, alan değerleri doğrudur. Yalnızca düzleştirilmiş çıktı onları kaybeder ve yalnızca checkbox'lar ve radio butonları için: aynı sayfadaki metin alanları düzgün çıkar

Düzleştirmeden sonra checkbox'lar neden işaretsiz?

Çünkü düzleştirme /V'ye hiç bakmaz. FPDFPage_Flatten, widget görünüm akışını sayfa içeriğine pişirir ve seçtiği görünüm /AS tarafından adlandırılandır. /AS hâlâ /Off diyorsa ve alan değeri kutunun açık olduğunu söylüyorsa, düzleştirme sadakatle kapalı görünümü pişirir. Değer hiçbir zaman kaybolmadı; hiçbir zaman danışılmadı

ISO 32000-1 §12.5.5, /N, /R ve /D olmak üzere üç olası girdiye sahip görünüm sözlüğü /AP'yi tanımlar. Bir check box ya da radio button için /N girdisi bir akış değil, anahtarları görünüm durumu isimleri olan bir alt sözlüktür ve §12.5.2, /N bir alt sözlük olduğunda /AS'yi gerekli seçici yapar. Yani bir checkbox iki önceden oluşturulmuş görünüm ve bir işaretçi taşır. İşaretçiyi yanlış anlayın ve rendering, hiçbir miktarda doğru /V'nin onarmayacağı bir şekilde yanlış olur. Bu aynı zamanda hata modunun, seçmek için hiçbir önceden oluşturulmuş görünümü olmayan metin alanlarından neden farklı olduğunun nedenidir: bir metin alanı /N'si, değer değiştikten sonra sıfırdan yeniden oluşturulması gereken tek bir akıştır, bu yüzden GenerateFormAppearances iki durumu tamamen ayrı kod yolları üzerinden ele alır ve yalnızca buton yolu bozuktu

Checkbox değeri gerçekte nerede yaşar?

Widget üzerinde değil, alan sözlüğü üzerinde. ISO 32000-1 §12.7.5.2, check box'ları ve radio button'ları, /V'si geçerli görünüm durumunu adlandıran bir isim nesnesi olan buton alanları olarak tanımlar ve §12.7.3.1, /V'yi tüm alan sözlüklerinde ortak girdiler arasına yerleştirir. §12.5.6.19'da tanımlanan widget annotation'ı /AS ve /AP'yi katkıda bulunur. Spesifikasyonda hiçbir şey bir widget'ı /V taşımaya zorunlu kılmaz

// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off

{ What the two objects look like when the field has several widgets:

  12 0 obj                          % field dictionary (the parent)
  << /FT /Btn  /T (Consent)  /V /On
     /Kids [ 13 0 R 14 0 R ] >>
  endobj

  13 0 obj                          % widget annotation (a kid)
  << /Type /Annot  /Subtype /Widget  /Parent 12 0 R
     /AS /Off
     /AP << /N << /On 20 0 R  /Off 21 0 R >> >> >>
  endobj }

FPDFAnnot_GetStringValue kusurlu değildir. Sözleşmesi tam olarak isminin söylediği şeydir: kendisine verdiğiniz annotation sözlüğünden bir dize girdisi getirin. Nesne 13'te /V'yi sormak hiçbir şey döndürmez, çünkü nesne 13'ün gerçekten /V'si yoktur. Kusur, ISO 32000-1'in hiçbir zaman vaat etmediği düz bir nesne modeli varsayan çağıranda idi

Alan ve widget ne zaman bir sözlüğü paylaşır?

Bir alanın tam olarak bir widget'ı olduğunda. §12.5.6.19, alan sözlüğünün ve tek widget annotation'ının bir nesnede birleştirilmesine izin verir ve çoğu yazım aracı bu kısayolu alır. Birleştirilmiş bir nesnede /FT, /T, /V, /AS ve /AP hepsi yan yana oturur, bu yüzden /V'nin widget-seviyesi bir okuması başarılı olur ve tüm hata görünmez kalır

Bir alan iki ya da daha fazla widget'a sahip olduğu anda birleştirme imkânsızdır ve §12.7.3.1, widget'ların ayrı bir alan sözlüğünün /Kids'i olmasını gerektirir. Her radio grubu yapı gereği bu şekildedir. Bir üstbilgi ve bir altbilgide tekrarlanan onay checkbox'ları da öyledir ve bir yazım aracının ikinci bir sayfaya kopyaladığı herhangi bir alan da öyledir. Bu, kusurun bir regresyon paketinden neden sağ çıktığının tam açıklamasıdır: test derlemi tek-widget'lı formlarla doluydu ve müşteri dosyaları değildi. Widget'ları bileşene güvenmek yerine kendiniz gezerseniz, aynı asimetri numaralandırma sırasında ortaya çıkar ve PDFium Component ile PDF form alanı gezinme üzerine notlar, sayfa-seviyesi bir annotation gezintisinin doküman-seviyesi alan ağacıyla nasıl ilişkili olduğunu ele alır

Değeri PDFium'un amaçladığı şekilde okumak

FPDFAnnot_GetFormFieldValue doğru API'dir ve checkbox yolu onu kullanmadan bir süre bileşende bağlanmıştı. Hem form tanıtıcısını hem de annotation'ı alır, ki bu önemli olan sinyaldir: form-fill ortamı mevcutken, PDFium annotation'ı form kontrolüne çözer ve değeri alan nesnesinden okur, bu yüzden birleştirilmiş ve bölünmüş düzenler için de doğru cevabı döndürür

FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
  begin
    // /AP is prebuilt per state; only /AS has to be synchronised with /V.
    // FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
    // which is where ISO 32000-1 12.7.5.2 keeps the value.
    buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
    if buflen >= 4 then
    begin
      SetLength(OrigVal, buflen div 2 - 1);
      FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
      FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
    end;
  end;

O parçacıktaki iki ayrıntının yanlış anlaşılması kolaydır. Döndürülen uzunluk, sonlandırıcı dahil UTF-16 metni için bir bayt sayısıdır, bu yüzden karakter sayısı buflen div 2 - 1'dir ve 2 değeri boş bir dize anlamına gelir. buflen >= 4 koruması bu yüzden en az bir gerçek karakter anlamına gelir; bu, hiç /V'si olmayan bir alanın /AS'sinin boş bir isimle üzerine yazılmasını önleyen şeydir

/AS ve /AP /N gerçekte ne konusunda anlaşır

Bir isim üzerinde anlaşırlar ve isim, dosyayı üreten kişi tarafından seçilir. §12.7.5.2, kapalı durumun /Off olarak adlandırılmasını gerektirir ve açık durumu tamamen üreticiye bırakır. /Yes bir kuraldır, bir kural değil. Acrobat /Yes yazar, ama birçok üreteç /On, /1, /Choice1 ya da yerelleştirilmiş bir kelime yazar ve bir radio grubu normalde her çocuğa ayrı bir açık-durum ismi verir, böylece grup hangi butonun seçili olduğunu ifade edebilir. Bu tam olarak /V'yi kelimesi kelimesine /AS'ye kopyalamanın bir hile değil doğru işlem olmasının nedenidir: işaretli bir kontrol için PDFium, dosyanın kendisinin tanımladığı açık-durum ismini bildirir ve işaretsiz biri için Off bildirir, bu yüzden /AS'ye yazdığınız değer o widget'ın /AP /N alt sözlüğünde var olan bir anahtar olması garanti edilir. /Yes'i sabit kodlamak Acrobat çıktısında çalışırdı ve her yerde başka sessizce bozulurdu

İşlem sırası ve hâlâ dikkat gerektiren yer

Sıra sabittir ve affetmezdir: form doldurmayı etkinleştir, değerleri ata, görünümleri yeniden oluştur, düzleştir, sonra kaydet. Yeniden oluşturma adımını atlayın ve FPDFPage_Flatten, boş ya da bayat görünüm akışlarını hiçbir şikayet olmadan pişirir; bu, bir hata dönüşü değil sessiz bir veri kaybıdır

Pdf.FileName := FormPath;
Pdf.FormFill := True;          // required: FormHandle must exist
Pdf.Active := True;

Pdf.FormField[0] := 'On';      // writes /V only

Pdf.GenerateFormAppearances;   // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
  Pdf.SaveAs('consent-flat.pdf');

İki dürüst sınır kalıyor. Birincisi, senkronizasyon alan değerini o alanın her widget'ının /AS'sine yazar; bu, checkbox'lar için doğrudur ama her çocuğu kendi açık-durum ismini tanımlayan radio gruplar için yaklaşıktır; /AP /N'si yazılan /AS'ye eşleşen bir girdisi olmayan bir çocuğun §12.5.5 altında seçecek görünümü yoktur, bu yüzden seçilmemiş bir buton boş bir daire yerine hiçbir şeye düzleşebilir. Düzleştirmeden önce FPDFAnnot_GetFormControlIndex ile bir radio grubunu denetlemek birkaç satıra değer. İkincisi, bunların hiçbiri, değerin AcroForm sözlükleri yerine bir XML veri paketinde yaşadığı XFA'ya uygulanmaz; bu ayrım kalıcı olmayan XFA alan düzenlemeleri üzerine notlarda ele alınmıştır. Genel ders bu tek düzeltmenin ötesinde de tutmaya değer: bir API annotation'a ek olarak form tanıtıcısını da aldığında, sizin için alan hiyerarşisini çözeceğini söylüyordur ve yalnızca annotation'ı aldığında, size verdiğiniz nesneyi tam olarak okuyacaktır. Bu ayrım veri alışverişini de yönetir, çünkü XFDF form verisi dışa ve içe aktarma, widget konumlarında değil, tam nitelikli alan isimlerinde çalışır

Form düzleştirme, tek bir API çağrısı gibi görünen ama üç sözlük arasında bir sözleşme olduğu ortaya çıkan özelliklerden biridir. Bu sözleşmeyi zaten kodlamış bir bileşene karşı çalışmayı tercih ederseniz, Delphi ve C++Builder için PDFium Component, burada anlatılan görünüm yeniden oluşturmayı, düzleştirmeyi ve form alanı erişimini sıradan özellikler ve metotlar olarak sunar