Teknik Makale

PDFium Component XFA kaydı: LF, emoji ve restoreState

PDFium Component, v3.125.2 ve sonrasıyla gelen Windows V8 runtime'ı pdfium.v8.dll'i koşturduğunda, düzenlenmiş XFA form değerlerini kaydetme ve yeniden açma boyunca harfi harfine saklar. Eski runtime'lar field değerlerine satır beslemeleri ekliyor, emoji'yi ilgisiz bir BMP karakterine kısaltıyor, tek-stream XFA kayıtlarını sessizce atlıyor ve başarısız bir son yazımı yutabiliyordu. Tek bir yeniden açma belirtisi hiçbir kütüphane kusuru değildir: kök subform'unda restoreState="auto" bulunmayan dinamik bir form, layout'unu şablondan yeniden kurar

Bunun hata bildirimlerinin hepsi birbirine benziyordu. Bir müşteri, bir Delphi görüntüleyicide bir XFA talep formunu doldurur, kaydeder, yeniden açar ve bir şey hafifçe şaşmıştır. Boş bir yorum kutusu artık bir boş satır tutar ve ikinci kayıttan sonra ikisini tutar. Bir emojiyle yazılmış bir ad, özel kullanım alanı bir glifle geri gelir. Kimse hata almaz; bu hataları pahalı kılan da budur: kayma, haftalar sonra bir başkasının dışa aktarımında görünür

Bir XFA formu kaydedilip yeniden açıldığında ne ters gider?

Yerel XFA kayıt yolundaki dört ayrı kusur değer kaymasına neden oldu ve her biri, başarılı görünen bir kaydın arkasına saklandı. İkisi serileştirmeden, biri tek-stream saklama düzeninden, biri de PDF yazıcısının kendisinden geldi. Tablo her belirtiyi nedenine ve PDFium Component'in düzelttiği sürüme eşler

Yeniden açma sonrası belirtiNedenDüzeltildiği sürüm
Boş alan bir satır beslemesi tutar; değerler her kayıtta bir satır sonu büyürHer iki XFA yazıcısı da başlangıç etiketlerinden sonra layout satır sonları eklediv3.125.2, pdfium.v8.dll
U+1F642, U+F642 olarak geri gelir ya da emoji form paketinden kaybolurÇözümlemede 16 bitlik wchar_t kısaltması; form serileştiricide surrogate filtrelemesiv3.125.2, pdfium.v8.dll
Tek-stream XFA belgesindeki düzenlemeler sadece yokturYerel kayıt stream düzenini reddetti ama dönüş değeri yok sayıldıv3.125.2; yorumlar ve işlem talimatları v3.126.0'dan beri korunur
Kayıt başarı bildirdiği hâlde kırpılmış dosyaYazıcı çoktan başarı döndürdükten sonra son tamponlu yazım başarısız olduv3.125.2 V8 runtime; v3.125.3 sıradan pdfium.dll
Üç sayfalık dinamik form iki sayfa olarak yeniden açılırKök subform restoreState="auto" istemiyorForm yazarlığı, kütüphane kusuru değil

Önceki yazılar, XFA field düzenlemelerinin PDFium ile hiçbir şekilde kalıcı kılınamayacağı sonucuna varmıştı; o günün runtime'ları için doğruydu. Daha yeni V8 runtime XFA değerlerini yerel olarak kaydeder; dolayısıyla canlı formda yapılan bir düzenleme, sizin tarafınızda paket cerrahisi olmadan kaydedilen datasets paketine ulaşır

Hangi PDFium runtime'ı XFA değerlerini kaydeder?

XFA kayıt sadakati Delphi sarmalayıcısına değil yerel DLL'ye bağlıdır; dolayısıyla ilk kontrol, sürecinizin gerçekte hangi runtime'ı yüklediğidir. PDFium Component, mimari başına iki Windows derlemesi taşır: V8'siz ve XFA'sız derlenen sıradan pdfium.dll ile JavaScript motorunu ve XFA form runtime'ını taşıyan pdfium.v8.dll. Yalnızca pdfium.v8.dll bir XFA formu koşturabilir; dolayısıyla burada anlatılan her XFA düzeltmesi orada yaşar — v3.125.2'deki yeniden derlenmiş Win32 ile Win64 V8 kütüphanelerinden başlayarak

Son-yazım düzeltmesi genel PDF yazıcı kodudur; dolayısıyla sıradan belgeler için de önemlidir. v3.125.3, sıradan pdfium.dll kütüphanelerini aynı tamiri taşımaları için yeniden derledi. Paylaşılan kaynak, paylaşılan davranışın kanıtı değildir: binary yeniden derlenene dek eski DLL eski hatayı taşır

İkinci bir tuzak yükleyicide oturuyordu. v3.125.2 öncesinde EnableV8Engine'i True yapmak, bağlamayı varsayılan pdfium.v8.dll adını seçmeye ve LibraryName'deki tam yolu yok saymaya itiyordu. Taze dağıtılmış bir runtime'ı gösteren bir uygulama, başka bir klasördeki eski bir kopyayı yüklemeye devam edebiliyordu. v3.125.2'den beri bir dizin içeren bir LibraryName, iki motor modunda da tam olarak o dosyayı seçer ve eksik bir yol, başka bir paketli kütüphaneye düşmek yerine başarısız olur

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // LibraryName'deki bir dizin tam bu dosyayı sabitler (v3.125.2 ve sonrası);
  // dosya eksikse yükleme, geri düşmek yerine exception yükseltir
{$IFDEF WIN64}
  PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
  PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
  PDFium.EnableV8Engine := True;
  PDFium.LoadLibrary;  // ilk kayıtta değil, başlangıçta başarısız ol
end;

Bir belgeyi açtıktan sonra TPdf.XFA dosyanın XFA içerdiğini, TPdf.XfaRuntimeAvailable ise yüklenen DLL'nin onu gerçekten çalıştırabildiğini söyler. Statik ile dinamik formları ayırt etmeniz de gerekiyorsa TPdf.FormType, ftXfaFull ya da ftXfaForeground döndürür; XFA formlarını saptama ve Delphi'de XFA paketlerini çıkarma yazısı o yoklamayı ayrıntılı işler

Kaydedilen XFA field'ları neden fazladan satır beslemeleri kazanır?

Kaydedilen XFA field'ları satır beslemeleri kazandı, çünkü iki yerel XFA yazıcısı da — genel XML element yazıcısı ile form paketi serileştiricisi — çıktılarını başlangıç etiketlerinden sonra bir satır sonuyla güzel-baskılıyordu. Çoğu XML'de o boşluk kozmetiktir. XFA verisinde değildir: datasets paketi yeniden ayrıştırıldığında <Comments> ile </Comments> arasındaki metin, satır sonu dâhil, field değeridir. Bu yüzden boş bir alan tek bir LF tutarak yeniden açılıyordu ve her sonraki kaydet-aç döngüsü bir tane daha ekleyebiliyordu

PDFium Component XFA kayıt döngüsü şeması: yazıcı, başlangıç etiketlerinden sonra bir satır sonu ekler; yeniden açılan parser, Comments etiketleri arasındaki LF'yi field değeri diye okur; ve v3.125.2 yalnızca serileştiricinin sentezlediği boşluğu kaldırana dek her sonraki kayıt bir satır beslemesi daha ekler
Bir kaydet-yeniden açma döngüsü ilk satır beslemesini eker ve her sonraki tur bir tane daha ekler; kaymanın tam biçimini bu yüzden yalnızca ikinci nesilde gösterdi

Bariz tamir olan, yüklemede değerleri kırpmak yanlış olurdu. Kullanıcılar XFA field'larına baştaki boşluklar, sondaki boşluklar ve kasıtlı çok satırlı metin yazar ve bir adres bloğu ya da sabit genişlikli bir kod bayt bayt hayatta kalmak zorundadır. v3.125.2 düzeltmesi bu yüzden yalnızca serileştiricinin etiketlerin çevresinde kendisinin sentezlediği boşluğu kaldırır. Kullanıcı değerleri, var olan metin node'ları ve CDATA bölümleri dokunulmadan geçer; dolayısıyla " indented" girintili kalır ve kasıtlı boş bir alan boş kalır

Bir emoji neden başka bir karakter olarak geri gelir?

Bir emoji yanlış geri geldi, çünkü Windows'ta wchar_t 16 bit genişliğindedir ve iki çözümleme yolu tam bir Unicode skaler değerini tek bir wchar_t'de saklıyordu. UTF-8 stream decoder'ı ile &#x1F642; gibi sayısal karakter referanslarının parser'ı ikisi de bunu yaptı. U+1F642, hafifçe gülümseyen yüz, 16 bit'e sığmaz; dolayısıyla yüksek bitler döküldü ve yerine U+F642 belirdi: çoğu fontun kutu ya da hiçbir şey olarak render ettiği Private Use Area'da bir kod noktası

Form serileştiricisinin sorunu tersydu. Karakterleri birer wchar_t filtreliyordu; yalnız başına geçersiz olan iki surrogate kod birimi görüyor ve ikisini de atıyordu; dolayısıyla emoji form paketinden tümüyle kayboluyordu. v3.125.2'de decoder her skaler değerini tümden tüketir ve düzgün bir surrogate çifti boşaltır. Yalnızca bir çıkış yuvası kaldığında alçak surrogate'i bekletir ve o birim hâlâ tampondayken stream sonu bildirmez. Okuma blokları arasında bölünmüş bir UTF-8 dizisi atılmak yerine sonraki okumaya taşınır. Form exporter'ı artık geçerli surrogate çiftlerini birlikte tutar ve sayısal karakter referansları da doğru çiftler üretir

PDFium Component surrogate işlemesi şeması: U+1F642, UTF-16 çifti D83D DE42 olarak gelir ve iki kusurlu yol onu bozar; 16 bitlik wchar_t decoder'ları skaleri özel kullanım alanındaki U+F642'ye kısaltırken form serileştiricisi yalnız surrogate'leri filtreler ve emoji'yi tümüyle düşürür
Windows wchar_t'si 16 bit genişliğindedir; dolayısıyla surrogate çifti gerektiren bir skaler ya da yüksek yarısını kaybetti ya da paketten kayboldu — iki yol da çiftleri birlikte tutmayı öğrenene dek

Latin-1 test verisi bunların hiçbirini göstermez; dolayısıyla her XFA round-trip testinin en azından bir tamamlayıcı düzlem karakteri gerekir

Tek-stream XFA ve kimsenin görmediği kayıt başarısızlıkları

Tek-stream bir XFA belgesi düzenlemelerini kaybetti; çünkü yerel kayıt yardımcısı o saklama düzenini reddediyor ve çağıranı başarısızlığı yok sayıyordu. ISO 32000-1 §12.7.8, etkileşimli form sözlüğünün /XFA girdisinin ya paket adları ile stream'lerden oluşan bir dizi ya da bütün XDP belgesini tutan tek bir stream olmasına izin verir. Paket dizileri sıradan durumdur ama tek stream'ler tamamen meşrudur ve PDF kaydı, form verisi eski değerlerinde kalırken hiçbir şey olmamış gibi tamamlanıyordu

v3.125.2'den beri V8 runtime, desteklenen tek-stream alt kümesini işler. Önce iki canlı paketi — datasets ile form — bir staging alanına export eder ve doğrular; ancak ondan sonra özgün XDP içindeki eşleşen paketleri değiştirir. Öteki paketler ile kök namespace bildirimleri korunur. Staging başarısız olursa kalıcı XFA stream'ine hiç dokunulmaz ve belge değişiklik işaretini korur

XML yorumları ile işlem talimatları ekstra özen istedi; çünkü dahili XML DOM onları düşürür. v3.125.2'de varlıkları, kaydı içerik sessizce kaybetmek yerine tümüyle başarısız kılıyordu. v3.126.0 onları korur: ayrıştırmadan önce her yorum ya da işlem talimatı, özgün metinde hiçbir yerde geçmeyen bir önekten kurulan bir işaretçiyle değiştirilir. Canlı paketler değiştirildikten sonra özgün token geri yüklenip stream yazılmadan önce her işaretçinin tam bir kez görünmesi zorunludur. Değiştirilen paketlerin dışındaki token'lar bu yüzden metinlerini ve sıralarını korur; prologdaki, template'teki ve öteki paketlerdeki token'lar dâhil

Bazı girdiler hâlâ bilerek reddedilir ve her red, açık bir kayıt başarısızlığıdır:

  • Canlı datasets ya da form paketlerinin içindeki yorumlar ya da işlem talimatları; çünkü özgün konumları taze export edilmiş içeriğe haritalanamaz
  • DTD bildirimleri ile XMLDSig imzaları; çünkü XDP'yi yeniden yazmak bir XML imzasını geçerli tutamaz
  • Geçersiz UTF-8 ya da UTF-16 kodlaması, eksik etiketler, geçersiz karakter referansları, bilinmeyen entity'ler ve hatalı kurgulu işlem talimatları; sessizce onarılmak yerine reddedilirler
PDFium Component tek-stream XFA kayıt hattı şeması: canlı datasets ile form paketleri staging'e export edilir, doğrulanır, sonra işaretçilerle korunmuş yorumlarla birlikte özgün XDP içinde değiştirilir; staging başarısızlıkları ile DTD ya da XMLDSig gibi girdiler kaydı açıkça reddeder
Aşamalı export, herhangi bir şey değiştirilmeden önce doğrulanır; dolayısıyla başarısız bir kayıt kalıcı XFA stream'ine dokunmaz ve belge değişiklik işaretini korur

Tek-stream çıktısı UTF-8'dir ve XML içerik modelini korur; özgün bayt düzenini ya da kodlama bildirimini değil

Son kusur, XFA'nın altında oturuyordu. Yerel dosya yazıcısı çıktıyı 32 KB'lik bloklarda tamponluyor ve son kısmi bloğu yalnızca yıkıcısında, belge yazıcısı çoktan başarı bildirdikten sonra boşaltıyordu. O son bloktaki bir disk-dolu ya da I/O hatası çağırana görünmüyordu. V8 runtime'da v3.125.2'den, sıradan runtime'da v3.125.3'ten beri o son boşaltım kayıt sonucunun parçasıdır ve XFA değişiklik işareti yalnızca gerçek bir başarıdan sonra temizlenir. Delphi tarafında TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean, hedefin yanındaki geçici bir dosyaya yazar ve onu yalnızca kayıt True döndürdüğünde yerine taşır; dolayısıyla başarısız bir kayıt önceki dosyayı olduğu gibi bırakır

Dinamik bir XFA formu neden daha az sayfayla yeniden açılır?

Dinamik bir XFA formu, kök subform'u restoreState="auto" bildirmediğinde daha az sayfayla yeniden açılır; bu da bir PDFium Component kusuru değil form yazarlığı kararıdır. XFA 3.3'te kök subform'daki restoreState varsayılan olarak manual'dır. manual altında XFA işlemcisi, kaydedilmiş form paketinden yalnızca sınırlı durumu geri yükler ve gerisini yazarın script'lerine bırakır. Kaydedilen field değerleri ile yinelenen-subform instance sayıları yine de geri gelir ama çalışma zamanında set edilen geometrik property'ler gelmez

Bunu açığa vuran durum, script'i bir subform'u h="450pt"'ye büyüten üç sayfalık bir formdu. Kaydedilen form paketi yeni yüksekliği, değerleri ve instance sayılarını tutuyordu. Ama yeniden açılışta layout şablon yüksekliklerinden yeniden kuruluyordu ve form iki sayfaya yeniden akıyordu. Runtime haklıydı: şablon otomatik geri yüklemeyi hiç istememişti. Onu kök subform'da bildirmek yeniden açmayı düzeltir:

<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
  <subform name="form1" layout="tb" restoreState="auto">
    <pageSet>
      <pageArea name="Page1">
        <contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
        <medium stock="letter"/>
      </pageArea>
    </pageSet>
    <subform name="Details" layout="tb" w="7.5in">
      <!-- field'lar; script'ler h'yi değiştirebilir ya da çalışma zamanında instance ekleyebilir -->
    </subform>
  </subform>
</template>

Şablonun sahibi değilseniz görüntüleyicide etrafını yamamayın: manual moduna bel bağlayan bir form, durumunu kendi script'lerinin yeniden kurmasını bekler. Kullanıcı yazarken canlı yeniden sayfalama ayrı bir konudur; PDFium Component'in dinamik XFA sayfa sayılarını ve taşınan field'ları nasıl izlediği yazısında işlenir

Delphi'de bir XFA kaydını nasıl doğrularsınız?

Güvenilir tek XFA kayıt kontrolü, kaydedilen dosyayı taze bir TPdf instance'ında yeniden açıp saklanan veriyi geri okumaktır. TPdf.GetXfaDatasets, datasets paketini belgede saklandığı hâliyle döndürür; canlı XFA veri modelini değil, dolayısıyla kaydetmeden önce çağırmak eski değerleri gösterir. Yeniden açtıktan sonra yazılanı harfi harfine gösterir. Tek-stream bir belgenin ayrıca adlandırılmış paketleri yoktur: PDFium bütün XDP'yi boş adlı tek bir paket olarak bildirir; dolayısıyla GetXfaPacketByName('datasets') ile GetXfaDatasets hiçbir şey döndürmez ve fallback, bütün stream'i GetXfaFormPackets üzerinden okur

uses
  System.SysUtils, PDFium, FPdfXfa;

function ReadSavedXfaData(const FileName: string): string;
var
  Pdf: TPdf;
  Packets: TXfaPacketList;
  Bytes: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Bytes := Pdf.GetXfaDatasets;          // paket-dizisi düzeni
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // tek stream: adlandırılmamış tek paket
      if Length(Packets) = 1 then
      begin
        SetLength(Bytes, Length(Packets[0].Content));
        if Length(Bytes) > 0 then
          Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
      end;
    end;
    Result := TEncoding.UTF8.GetString(Bytes);  // kaydedilen XDP çıktısı UTF-8'dir
  finally
    Pdf.Free;
  end;
end;

Kayıt rutini sonra bekleyen düzenlemeyi işler, SaveAs sonucunu kontrol eder ve yeniden açılan değeri karşılaştırır. TPdf.ClearFormFieldFocus form odağını öldürür; PDFium'un odaklı field'ın düzenleme tamponunu işlediği an budur. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean odaklı field'ı programatik olarak doldurur ama wrapper'ın FocusFormField üzerinden izlediği bir odağa bel bağlar; o da widget annotation'larını gezer. Dinamik bir XFA sayfasında normalde hiçbiri yoktur; dolayısıyla orada metin çoğunlukla TPdfView'deki klavye girişiyle gelir ve izlenen hiçbir field odaklı değilken fonksiyon False döndürür

function XmlText(const S: string): string;
begin
  Result := StringReplace(S, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
end;

procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
  Expected: string);
var
  Saved: string;
begin
  // İsteğe bağlı scriptli doldurma; False, izlenen hiçbir field'ın odaklı olmadığı demektir
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // düzenleme tamponunu işle
  if not Pdf.SaveAs(FileName) then      // son boşaltımı içerir (v3.125.2+)
    raise EPdfError.CreateFmt('Saving %s failed', [FileName]);

  Saved := ReadSavedXfaData(FileName);
  if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
    Saved) = 0 then
    raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;

Substring testini bir duman testi sayın. Boş bir element <Tag/> diye serileştirilebilir, attribute'lar data elementlerinde görünebilir ve & ile <'nin ötesindeki kaçış bir serileştirici seçimidir. Üretim kontrolleri için yeniden açılan XML'i gerçek bir XML parser'ıyla yükleyin ve bağlı data elementinin metin node'unu karşılaştırın. Kontrolü üst üste iki kez de koşturun; çünkü satır sonu kusuru tam biçimini yalnızca ikinci nesilde gösterdi

Hızlı başvuru: XFA kayıt sadakati kontrol listesi

  • XFA formları için v3.125.2 ve sonrasından pdfium.v8.dll'i, sıradan pdfium.dll için v3.125.3 ve sonrasını dağıtın; böylece son-yazım düzeltmesi ikisinde de olur
  • LibraryName'i tam bir yola işaretletin ve EnableV8Engine'i True'ya set edin; eksik bir yol, başka bir kopyayı yüklemek yerine başarısız olur
  • Belgeyi açtıktan sonra TPdf.XFA ile TPdf.XfaRuntimeAvailable'i teyit edin
  • Odaklı field işlenmiş olsun diye SaveAs'ten önce ClearFormFieldFocus çağırın
  • SaveAs'in Boolean sonucunu asla yok saymayın; False bir sonuç önceki dosyayı yerinde bırakır
  • Yeni bir TPdf'de yeniden açıp GetXfaDatasets okuyarak doğrulayın; tek-stream XFA için GetXfaFormPackets'e düşerek
  • Boş değerlerle, baştaki boşluklarla, çok satırlı metinle, & ile ve bir tamamlayıcı düzlem karakteriyle, iki kayıt nesli boyunca test edin
  • DTD'ler, XMLDSig ve tek-stream XFA'nın canlı paketleri içindeki yorumlar için açık kayıt başarısızlıkları bekleyin
  • Dinamik bir form, yeniden açılışta çalışma zamanı geometrisini kaybederse kütüphaneden şüphelenmeden önce kök subform'da restoreState="auto" olup olmadığına bakın

XFA runtime'ının bir ana uygulamadan beklediği callback yapısı için FPDF_FORMFILLINFO sürüm 2 ve Delphi'de XFA ABI'si yazısına bakın. V8 runtime, Delphi ile C++Builder sarmalayıcısı ve görüntüleyici kontrolü, Win32 ile Win64 için her iki Windows runtime'ını da içeren Delphi ve C++Builder için PDFium Component'in parçalarıdır