Teknik Makale

Delphi'de Statik XFA Formlarını AcroForm Alanlarına Çevirme

PDFium Component, statik XFA formlarını Delphi tarafından FPdfDocumentXfaFlatten ünitesindeki FlattenPdfXfaToAcroForm ile sıradan AcroForm alanlarına dönüştürür. Fonksiyon XFA template ile datasets paketlerini okur, mutlak konumlu her alanı kendi appearance stream'ine sahip bir AcroForm terminal alanına eşler ve sonucu artımlı bir revizyon olarak ekler; dönüştürülen PDF böylece XFA'yı hiç desteklememiş görüntüleyicilerde normal bir form olarak açılır

Motivasyon genellikle yol haritası maddesi değil bir destek kaydıdır. Devlet ya da sigorta formu, onu XFA olarak kaydeden bir tasarımcıda yazılmıştır; bir masaüstü okuyucuda çalışır, her başka yerde ise belgenin daha yeni bir görüntüleyici gerektirdiğini söyleyen boş bir sayfa olarak açılır. XFA, PDF 2.0'da (ISO 32000-2) kullanımdan kaldırıldı; tarayıcılar ile mobil görüntüleyiciler onu hiç uygulamadı ve PDFium'un kendisi XFA'yı yalnızca V8 etkin derlemeyle render eder. Düzeni tasarım zamanında sabitlenen statik XFA formları, AcroForm alanları olarak yeniden kurulmaya yetecek geometriyi template içinde taşır ve bu dönüştürücünün ele aldığı dilim tam olarak budur

FlattenPdfXfaToAcroForm gerçekte ne üretir?

FlattenPdfXfaToAcroForm, değişmemiş bir kaynak kopyasına eklenen bayt düzeyinde bir AcroForm revizyonu üretir; özgün dosyanın yeniden yazımını asla. PDFium form alanı oluşturmak için genel bir API açmaz; bileşen bu iş için hiç PDFium'dan geçmez: etkin revizyonu kendi nesne okuyucusuyla okur, alan sözlüklerini, widget açıklamalarını, appearance XObject'lerini ve bir /Helv font kaynağını ham PDF nesneleri olarak kurar ve ISO 32000-1 §7.5.6 anlamında seyrek bir artımlı güncellemeyle serileştirir

Yeni /AcroForm sözlüğü /Fields, /DA ile /DR taşır ve hiçbir /XFA girdisi taşımaz. Kaynak AcroForm'unu dolaylı bir nesne olarak tutuyorsa o nesne yerinde yeniden yazılır; satır içi bir sözlük kataloğun içinde değiştirilir. Her iki durumda da etkin revizyon artık XFA paketlerini işaret etmez; her iki modeli de anlayan bir görüntüleyici, XFA formunu dizmeye çalışmak yerine AcroForm alanlarını açar. Özgün XFA baytları hâlâ önceki revizyondadır — artımlı güncellemenin anlamı budur — ama geçerli revizyonda onlara referans veren hiçbir şey yoktur

Delphi için PDFium Component'te statik XFA'dan AcroForm'a dönüştürme hattı: FlattenPdfXfaToAcroForm, template ile datasets paketlerini okur, mutlak konumlu alanları alan sözlükleri ile widget'lara eşler ve AcroForm'u /XFA girdisi olmadan seyrek bir artımlı revizyon olarak ekler
FlattenPdfXfaToAcroForm özgün baytlara hiç dokunmaz: dönüştürülen alanlar, widget'lar ve appearance akışları, /AcroForm'u XFA paketlerini işaret etmeyi yalnızca bırakan bir revizyon olarak eklenir
uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;

procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
  Source: TFileStream;
  Options: TPdfXfaFlattenOptions;
  OutputPdf: TBytes;
  Report: TPdfXfaFlattenReport;
  Fields: TPdfXfaFlattenFields;
begin
  Options := TPdfXfaFlattenOptions.Default;
  Options.MaxFields := 1024;   // varsayılan 512; izin verilen aralık 1..4096

  Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
  try
    if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
      raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
  finally
    Source.Free;
  end;

  TFile.WriteAllBytes(TargetFile, OutputPdf);
  Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
    [Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;

Fonksiyon, reddedilen ya da bozuk her girdi için False döndürür ve Report.ErrorText'i doldurur; çıkarken kaynak akışın konumunu geri koyar. İptal, yayılan tek istisna yoludur: Options.CancellationToken tetiklenirse rapor doldurulduktan sonra EPdfOperationCancelled yeniden fırlatılır. TPdfXfaFlattenReport ayrıca SourceSha256 ile OutputSha256 taşır; toplu işin hangi girdinin hangi dönüştürülmüş dosyayı ürettiğini kanıtlaması gerektiğinde işe yarar. TPdfXfaFlattenOptions.Default 8 MiB paket bütçesi, 512 alan ve 512 MiB çıktı tavanı ayarlar; kaynak şifreli olmamalıdır

Template ile datasets paketlerini okumak

PDFium Component, ISO 32000-1 §12.7.8'in /XFA girdisi için izin verdiği her iki düzeni kabul eder: paket adları ile akış referanslarını sıralayan dizi biçimi ve tek akışın bütün <xdp:xdp> belgesini tuttuğu tek akışlı XDP biçimi. Paket akışları sıkıştırılmamış olabilir ya da tahmincisi (predictor) olmayan bir /FlateDecode olabilir; açma, MaxPacketBytes'le sınırlıdır

Tek akışlı durum, naif XML işlemesinin kırıldığı yerdir. Tasarımcılar genellikle <xfa:datasets>'i ad alanı önekiyle yazar ama template öneksizdir; sabit etiket adına göre dilimlemek bir paketi bulur ötekini kaçırır. Dönüştürücü öğe ile öznitelik adlarını yerel adla eşler; öncesinde açma ve kapama etiketlerinin tam önekli adlarıyla eşleştiğini kontrol eden ayrı bir denetim yapılır. Paketleri dönüştürmek yerine yalnızca incelemeniz gerekiyorsa PDFium VCL ile XFA formlarını algılama ve paketlerini çıkarma rehberi o salt okunur yolu anlatır

Alan değerleri iki yerden gelir. Template içindeki bir <value> öğesi varsayılanı verir; datasets paketi, alan adına göre bağlanan doldurulmuş veriyi verir. Alan adları gerçek formlarda yinelenir; bağlayıcı bu yüzden adlandırılmış <subform> ebeveyn zincirini verideki ebeveyn yoluyla karşılaştırır ve çelişmeyen en uzun eşleşmeyi alır. İki yaprak alan farklı subform'lar altında aynı adı paylaşıyorsa AcroForm /T'si SubformPath.Name olur; iki alan böylece dönüştürmeden sonra ayrı kalır

choiceList neden açık bir Combo bayrağı ister?

XFA ile PDF, seçim alanları için ters varsayılanlara sahiptir; hiçbir bayrağı kopyalamayan bir dönüştürücü her açılır listeyi liste kutusuna çevirir. Varsayılan open="userControl" ile ya da open="onEntry" ile açılmış bir XFA <choiceList> açılır menüdür (drop-down); bayrağı olmayan bir PDF /FT /Ch alanı kaydırmalı liste kutusudur ve onu combo box yapan tek şey Combo bayrağıdır (ISO 32000-1 Tablo 230'da bit konumu 18, değer 131072; PDFium'un FPDF_FORMFLAG_CHOICE_COMBO dediği bit). Dönüştürücü bu yüzden açılır menü durumları için Combo bayrağını ayarlar ve bayrakları yalnızca template open="always" ya da open="multiSelect" söylüyorsa boş bırakır; bunlar listeyi sürekli açık tutar. 3.181.1 öncesi derlemeler Combo ile Pushbutton değerlerini yer değiştirmişti ve standart dışı bir open="on" arıyordu; onlarla düzleştirilmiş formları yeniden çalıştırmaya değer

PDFium Component statik XFA'dan AcroForm'a dönüştürmede seçim alanı bayrağı kararı: userControl ya da onEntry olan bir open özniteliği, açılır combo box üretmek için Combo bayrağını (bit 18, değer 131072) ayarlar; open always ya da multiSelect ise sürekli açık liste kutusu için bayrakları boş bırakır
Combo bayrağı olmadan bir XFA açılır listesi kaydırmalı liste kutusuna düzleşir; dönüştürücü bu yüzden open özniteliğini okur ve bit 18'i yalnızca açılır menü durumları için ayarlar

UI eşlemesinin geri kalanı ISO 32000-1 §12.7.4'ün alan bayrağı tablolarını izler:

  • textEdit, numericEdit ile dateTimeEdit /FT /Tx olur; çok satırlı metin alanı Multiline bayrağını (/Ff 4096, Tablo 228) ve satıra bölünmüş bir appearance akışı alır
  • passwordEdit, Password bayrağıyla (/Ff 8192) UTF-16 kod birimi başına değil Unicode skaler başına bir yıldız çizen bir görünümle /FT /Tx olur
  • checkButton, bağlı veri 1 iken değeri /Yes olan, aksi hâlde /Off olan bir /FT /Btn onay kutusu olur
  • <exclGroup>, /V, /DV, /Opt ile /Kids taşıyan bir /FT /Btn /Ff 32768 radyo ebeveyni olur (Tablo 226); artı üye başına kendi /AS'i ile iki durumlu /AP'si olan bir çocuk widget
  • imageEdit, görünümü görseli boyayan bir pushbutton olur; baseline bir JPEG /DCTDecode altında olduğu gibi gömülür; PNG ise çözülür ve PNG alfa ya da tRNS saydamlığı taşıyorsa ek bir /SMask ile /FlateDecode pikselleri olarak yeniden kodlanır

Satır sonları dahil düz basılabilir ASCII olmayan değerler <FEFF...> UTF-16BE dizgeleri olarak yazılır; bir Japonca ad ya da iki satırlık adres yolculuktan sağ çıkar. Fields çıktısı yazılanı aynalar; kullanışlı bir denetim izi olması bundandır

procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
  KindNames: array[TPdfXfaFlattenFieldKind] of string = (
    'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
  I: Integer;
  F: TPdfXfaFlattenField;
begin
  for I := 0 to High(Fields) do
  begin
    F := Fields[I];
    Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
      [F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
    case F.Kind of
      fxfkChoice:
        Writeln(Format('  %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
      fxfkRadio:
        Writeln(Format('  %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
      fxfkImage:
        Writeln(Format('  %d image bytes', [Length(F.ImageData)]));
    else
      Writeln('  value "' + F.Value + '"');
    end;
  end;
end;

Geometri, ölçü birimleri ve sayfa yönlendirme

XFA geometrisi, her alan kendi sayfasının MediaBox yüksekliğine karşı çevrilerek PDF user space'e dönüştürülür; çünkü XFA sayfanın tepesinden aşağı doğru, PDF alttan yukarı doğru ölçer. İç içe <subform> öğelerinden gelen ofsetler template içinde aşağı inerken birikir ve her alan pozitif genişlik ile yükseklikle bitmek zorundadır; aksi hâlde dönüştürme reddedilir

Delphi için PDFium Component'te XFA'dan PDF'e geometri dönüştürmesi: XFA template içinde üst kenardan aşağı doğru ölçülen bir alan dikdörtgeni, kendi sayfasının MediaBox yüksekliğine karşı çevrilerek PDF user space'e geçer; y koordinatı orada alttan yukarı doğru büyür
Aşağı yönlü XFA template ile yukarı yönlü PDF user space, MediaBox yüksekliğine karşı tek bir çıkarmada buluşur; her dönüştürülen widget'ı tasarlandığı yere koyan budur

Ölçümler gerçek tasarımcıların yazdığı birimleri kabul eder: çıplak sayılar ile pt; ayrıca in (72 nokta), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) ve mp (0.001, milipoint). Ayrıştırma yerelden bağımsızdır; Almanca bir Windows kurulumu 8.5in'i anlamsızlığa çevirmez. Bilinmeyen bir birim eki, alanı sessizce yanlış yere koyacak olan nokta olarak okunmak yerine reddedilir

Sayfa yönlendirme <pageSet>'i izler: kardeş <pageArea> öğeleri bildirim sırasına göre birden itibaren numaralanır ve altlarına yuvalanmış alanları sahiplenir; <pageArea> ebeveyni olmayan alanlar birinci sayfada kalır. Her sayfa kendi /Annots güncellemesini alır. PDF'in son sayfasının ötesine yönlendirilen bir alan, birinci sayfaya bırakılmış bir widget değil bir hatadır

Dönüştürücü hangi XFA formlarını reddeder?

Dönüştürücü, düzeni XFA düzen motorunu çalıştırmadan bilinemeyecek her şeyi reddeder. Bu, bir <subform> ya da <exclGroup> üzerinde position dışı bir layout özniteliği (dinamik formların kurulduğu akan tb, lr-tb ve dostları), herhangi bir <script> öğesi ve yukarıda sayılan yedi dışında herhangi bir UI türü demektir. Çalışma zamanında sayfa büyüten dinamik formlar başka bir araç zincirine aittir; PDFium Component'te dinamik XFA yeniden sayfalama ve sayfa sayıları makalesi o canlı düzenin ne içerdiğini gösterir

Reddetme bilinçlidir. Akarak dizilen bir subform'un sabit koordinatları yoktur; herhangi bir "dönüştürme" konumları uydurmak zorunda kalırdı ve betikli bir form çoğu zaman statik bir AcroForm'un yeniden üretemeyeceği toplamlar hesaplar ya da görünürlük anahtarlar. Sessizce hesaplanmış bir toplam kaybetmiş, tamamlanmış görünen bir PDF üretmek, Report.ErrorText'teki net bir hatadan kötüdür. XFA 3.3 spesifikasyonu bu alt kümeden çok daha fazlasını tanımlar ve dönüştürücü öyle değilmiş gibi davranmaz

AcroForm revizyonunu yazarken ve doğrularken tuzaklar

Yazıcı kurulurken iki sorun ortaya çıktı ve ikisi de elle artımlı güncelleme üreten herkesi ilgilendirir. İlki: katalog, baytları değişmese bile — AcroForm başka yerde yeniden yazılan dolaylı bir nesne olduğundan — daima güncelleme kümesinin parçası olmalıdır. Seyrek güncelleme yazıcısı trailer /Root'unu taşıdığı nesnelerden türetir; kök nesnesi olmayan bir güncelleme, görüntüleyicilerin demirleyemediği bir revizyon üretir

İkincisi doğrulamayla ilgilidir. Dönüştürülen dosyayı PDFium sarmalayıcısı üzerinden yüklemek, belgenin bir /AcroForm'u olduğu anda form ortamı başlatmasını tetikler ve test kurulumundaki ev sahibi pdfium.dll'de FORM_GetTextDirection dışa aktarımı yoktu; tek bir alan kontrol edilmeden başlatma başarısız oluyordu. Geri okuma kontrolleri bu yüzden bileşen nesne okuyucusunu kullanır; o, etkin revizyonu form katmanına dokunmadan ayrıştırır

uses
  System.SysUtils, System.Classes, FPdfCompress;

function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
  Stream: TBytesStream;
  Objects: TPdfActiveObjects;
  Revision: TPdfSparseRevisionInfo;
  ErrorText: string;
begin
  Stream := TBytesStream.Create(Pdf);
  try
    if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
      Objects, Revision, ErrorText) then
      raise Exception.Create(ErrorText);
    // etkin katalog bir AcroForm'a referans vermeli; dönüştürücünün yazdığı
    // satır içi AcroForm /XFA girdisi taşımaz
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Appearance akışları bilinçli olarak basittir: metin ile seçim değerleri için 10 punto Helvetica; onay kutuları için konturlu bir kare; seçili radyo durumu için dolgulu bir iç kare. Dönüştürülen formu her yerde okunur kılarlar ama özgün XFA render'ının piksel kopyası değildirler; template'te tanımlı fontlar, renkler ve kenarlıklar taşınmaz. Bir form, tasarımcı çıktısıyla özdeş görünmek zorundaysa üretim hattını devirmeden önce onu bir kez XFA derlemesiyle render edip karşılaştırın

SSS

FlattenPdfXfaToAcroForm, PDFium'un V8 ya da XFA derlemesini gerektirir mi?

Hayır. FlattenPdfXfaToAcroForm paketleri ayrıştırır ve revizyonu PDFium Component koduyla yazar; standart pdfium.dll ile çalışır ve hiçbir XFA betiği çalıştırmaz

Dönüştürücü şifreli XFA belgelerini işleyebilir mi?

Doğrudan hayır. Şifreli bir kaynak hata ile reddedilir; belgeyi önce şifresini çözün ve çözülmüş kopyayı dönüştürün

Dönüştürülen alanlar hâlâ düzenlenebilir mi?

Evet. Çıktı, değerleri ile appearance akışlarıyla gerçek AcroForm metin, onay kutusu, radyo, seçim ve pushbutton alanları içerir; kullanıcılar formu AcroForm destekleyen herhangi bir görüntüleyicide doldurmaya devam edebilir

Statik XFA dönüştürmesi, XFA paket çıkarma ve V8 etkin XFA çalışma zamanının tamamı Delphi ve C++Builder için PDFium Component'in parçasıdır; bileşen Delphi, C++Builder ve Lazarus projeleri için PDFium'u sarmalar