Teknik Makale

Delphi'de Dinamik XFA Formları: HotPDF İşlemleri

HotPDF, Delphi içinde dinamik XFA formlarını TXFAWidgetRuntime aracılığıyla doldurur: her alan düzenlemesini tek bir işlem olarak ele alan, konaktan bağımsız bir widget katmanı — anlık görüntü, doğrulama, hesaplama, yeniden akış, ardından bütün olarak yayınlama ya da geri alma. Kendi VCL ya da FMX konağınızın içinde tek iş parçacıklı çalışır, Acrobat kurulu olmasını gerektirmez ve herhangi bir ayırma yapmadan önce her bütçeyi zorlar

Bu senaryo, devlet ya da sigortacılık işine belge yazılımı göndermiş herkese tanıdıktır. Bir hasar formu ya da vergi beyannamesi, sayfa içeriği tek bir "Please wait... if this message is not eventually replaced" uyarısı olan bir PDF olarak gelir ve her gerçek alan, yalnızca Adobe Acrobat ürününün işlediği bir XFA paketinde yaşar. Kullanıcılarınız onu uygulamanızın içinde doldurmak ister. Rasterleştirerek de kurtulamazsınız; çünkü form veri girildikçe satır büyütür ve üçüncü satırdan sonraki yerleşim, dosyayla gelen yerleşim değildir

Dinamik XFA neden hâlâ çözmeye değer bir sorun

Dinamik XFA kalıcıdır; çünkü konuşlandırılmış formlar, onları taşıyan biçimden daha uzun yaşar. ISO 32000-1 §12.7.8, XFA biçimini AcroForm sözlüğünde XDP paket akışı tutan bir /XFA girdisi olarak tanımlar; ISO 32000-2 ise tüm mekanizmayı kullanımdan kaldırır. Kullanımdan kaldırma onu yol haritasından sildi, sahadan değil; XFA 3.3 belirtimine göre yazılmış formlar hâlâ düzenleniyor ve hâlâ hukuken bağlayıcı. Statik XFA sıradan widget ek açıklamalarına indirgenebilir ve HotPDF, ApplyXFAAsAcroForm çağırdığınızda bunu yapar; ödünleşimler XFA formlarını AcroForm alanlarına düzleştirme konusunda ele alınıyor. Dinamik XFA farklı bir yapıdadır: occur aralıkları, büyüyebilen metin ve calculate betikleri, alan kümesini verinin bir işlevi haline getirir; bu yüzden kullanıcı yazmayı bitirene dek düzleştirilecek sabit bir ek açıklama listesi yoktur. TXFAWidgetRuntime tam da bu boşluğu doldurur: XFA DOM yapısını canlı tutar, kabul edilen her düzenlemeden sonra yerleşimi yeniden hesaplar ve konağınıza çizip isabet testi yapacağınız konumlandırılmış widget dizisini düz bir dizi olarak verir

Çalışma zamanı konak uygulamaya ne verir?

Size geometri ve durum verir; bir UI araç seti varsayan hiçbir şey vermez. TXFAWidgetRuntime, WidgetCount ve Widgets[I] üzerinden ID, Name, Kind, PageIndex, PDF punto cinsinden Bounds, Value, EditValue ile Focused, Editing, ReadOnly, Valid bayraklarını taşıyan TXFAWidgetState kayıtları sunar; boyama, imleç çizimi ve klavye yönlendirmesi ise sizin kodunuzda kalır. Widget kimliği kararlı ve sıralıdır: her widget, name[n] biçiminde bir ID alır; burada n, o alan adının yerleşim sırasındaki önceki geçiş sayısını sayar, böylece yinelenen bir alt formun ikinci satırı amount[1] olur. Yeniden kurulumdan sağ çıkan şey bu kimliktir ve FocusWidget, BeginEdit, DispatchEvent ile HitTest yöntemlerinin hepsinin konuştuğu dil budur. Bir THotPDF örneğinde zaten açık olan bir belge için CreateLoadedXFAWidgetRuntime, XDP paketlerini çıkarır, ilk sayfa kutusunu yerleşim sayfa boyutu olarak alır ve dosyada hiç XFA yoksa nil döndürür

var
  Pdf: THotPDF;
  Runtime: TXFAWidgetRuntime;
  WidgetID: AnsiString;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('claim-dynamic.pdf');
    Runtime := Pdf.CreateLoadedXFAWidgetRuntime;   // /XFA yoksa nil
    if Runtime = nil then
      Exit;
    try
      for I := 0 to Runtime.WidgetCount - 1 do
        Memo1.Lines.Add(Format('%s p%d [%.1f %.1f %.1f %.1f] = %s',
          [string(Runtime.Widgets[I].ID), Runtime.Widgets[I].PageIndex,
           Runtime.Widgets[I].Bounds.Left, Runtime.Widgets[I].Bounds.Top,
           Runtime.Widgets[I].Bounds.Right, Runtime.Widgets[I].Bounds.Bottom,
           string(Runtime.Widgets[I].Value)]));
      // sayfa uzayında isabet testi, en üstteki widget kazanır
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Bir alan işlendiğinde neyin atomik olması gerekir?

Düzenlemenin dokunabileceği her şey; bu da alan değerinden hayli fazlasıdır. CommitEdit, bir şey yazmadan önce CaptureSnapshot çağırır ve bu anlık görüntü dört şeyi kapsar: TXFADocument.SaveToBytes çıktısı olan serileştirilmiş XFA DOM, TXFAWidgetState etkileşim kayıtlarının tam dizisi, LastCalculationPasses ve LastReflowPasses sayaçları ile geçerli Warnings.Count değeri. Yalnızca düğüm değerlerini kaydetmek cazip bir kestirmedir ve yanlıştır; çünkü bir calculate betiği ya da çözümlenememiş bir bağlama, EnsureValueNode çağırarak düzenleme başladığında var olmayan veri düğümlerini somutlaştırabilir. Yalnızca değer geri yüklemesinin bunları kaldırma yolu yoktur; bu yüzden reddedilen bir düzenleme, datasets paketinde kalıcı yapısal artık bırakır. İşleme dizisinin kendisi katıdır: aday değeri yaz, düzenlenen alan için validate çalıştır, calculate işlemini sabit noktaya kadar çalıştır, ardından yerleşim kararlı olana dek yeniden akış yap. Herhangi bir aşamadaki herhangi bir hata FailAndRestore üzerinden geçer; bu da anlık görüntü baytlarını yeni bir TXFADocument içine geri yükler, widget listesini yeniden kurar, kayıtlı etkileşim durumlarını yeniden uygular, sayaçları sıfırlar ve Warnings listesini anlık görüntü uzunluğuna kırpar. LastDiagnostic hata durumunda nedeni tutar; geri yükleme kendisi hata fırlattığında ise patolojik durum olarak XFA transaction rollback failed değişmezini tutar

HotPDF bir XFA alan işlemesini tek bir işlem olarak ele alır; doğrulama, hesaplama ve yeniden akıştan önce serileştirilmiş DOM, her widget durumu, geçiş sayaçları ve uyarı sayısı yakalanır, ardından dördü birlikte yayınlanır ya da geri yüklenir
CommitEdit, bir şey yazmadan önce dört tür durumun anlık görüntüsünü alır; böylece başarısız bir validate, calculate ya da yeniden akış geride yapısal artık bırakmaz
function EditAmount(Runtime: TXFAWidgetRuntime;
  const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
  Current: UnicodeString;
begin
  Result := False;
  if not Runtime.BeginEdit(AWidgetID) then
    Exit;                                   // salt okunur ya da böyle bir widget yok
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // geçersiz aralık ya da bölünmüş vekil
    Exit;
  end;
  Result := Runtime.CommitEdit;             // ya hep ya hiç
  if not Result then
    // belge, widget listesi, sayaçlar ve uyarılar zaten düzenleme öncesi
    // duruma döndü; odaklı widget yalnızca geçersiz olarak işaretlenir
    ShowMessage(Runtime.LastDiagnostic);
end;

ReplaceSelection kendi notunu hak eder; çünkü bozuk girdinin reddedilmesinin en ucuz olduğu yer burasıdır. UTF-16 vekil çiftini bölen bir seçimi reddeder, eşleşmemiş yüksek ya da düşük vekil içeren yedek metni reddeder ve MaxValueChars değerinden uzun her sonucu reddeder. Bunu tuş vuruşu katmanında yakalamak, işlem mekanizmasının yarım yazılmış bir astral düzlem karakterini asla geri sarmak zorunda kalmaması demektir

Özel bir listeye yeniden kur, tek takasla yayınla

Widget yeniden kurulumu asla yarım bitmiş gözlemlenemez; bu yüzden RebuildWidgets tamamen ayrı, sahipli bir TObjectList kurar ve sonda tek bir atamayla yerine takar. Nedeni estetik değildir: TXFALayoutEngine.ComputeLayout, yeniden kurulum sürerken çalışır ve sağladığınız MeasureText işlevi üzerinden konak koduna geri çağrı yapar; widget sınırına ulaşıldığında EXFAWidgetRuntimeError fırlatabilir. Çalışma zamanı canlı listesini yerinde değiştirseydi, iki yol da konağın elinde kısmen eski kısmen yeni yerleşimden oluşan ve geri alınmak üzere olan bir belgeye DataNode işaretçileri taşıyan bir liste bırakırdı. Yeniden akış yakınsamasına sonra LayoutSignature karar verir: widget sayısı artı her ID, sayfa dizini ve dört ondalığa yuvarlanmış sınırlayıcı kutudan kurulan bir dizge. CommitEdit yeniden kurar, imzaları karşılaştırır ve ardışık iki imza eşleşene ya da geçiş bütçesi tükenene dek yineler. İmza hiç değişmediyse LastReflowPasses 0 kalır; yalnızca değer düzenlemesini formu gerçekten büyüten düzenlemeden böyle ayırt edersiniz. Etkileşim durumu her yeniden kurulum boyunca widget ID ile taşınır; böylece odak ve süren düzenleme bir satır eklenmesinden sağ çıkar

HotPDF XFA çalışma zamanı, yerleşim çalışırken ve konak ölçüm koduna geri çağrı yaparken widget listesini ayrı bir sahipli listeye yeniden kurar, ardından bitmiş listeyi konağın yarım gözlemleyemeyeceği tek bir atamayla yayınlar
Yeniden kurulum özel bir listede olur; çünkü ComputeLayout yol ortasında hata fırlatabilir ve iki ardışık yeniden akışın ne zaman yakınsadığına LayoutSignature karar verir

Bağlı bir alan neden yanlış kaydı okusun?

Çünkü betik veri bağlamı olmadan çalıştı. Açık bir <bind match="dataRef" ref="$record.actual"/> taşıyan alan ile aynı veri düğümünün adını taşıyan alan, tek bir değere işaret eden iki farklı widget olur; <occur max="2"/> içeren yinelenen bir alt form ise adı paylaşan ve yalnızca ait oldukları veri satırıyla ayrılan birden çok widget üretir. Doğrulama ve hesaplamayı belge köküne karşı değerlendirirseniz hepsi this değerini tüm datasets paketindeki ilk eşleşen düğüme çözümler; böylece ikinci satır sessizce birinci satırı doğrular. HotPDF bunu, yerleşim ürettiğinde çözümlenmiş DataNode değerini her widget girdisinde saklayarak ve ardından o düğümü HPDFXFAEvaluateFieldScript çağrılarının ikisinden de — xfskValidate ve xfskCalculate için — geçirerek önler. Aynı bağlam, bir hesaplama henüz var olmayan bir bağlamayı hedeflediğinde EnsureValueNode işlevinin hangi düğüme karşı oluşturulacağına da karar verir; hiçbir bağlama çözümlenemediğinde işlem, yanlış satıra yazmak yerine XFA calculation target is not bound ile temiz biçimde başarısız olur. Bu betiklerin arkasındaki FormCalc anlambilimi, AcroForm belgelerinin AcroForm format ve calculate betikleri konusunda anlatılan eylemlerden aldıklarına benzer; ancak buradaki çözümleme kuralları alan adı kapsamlı değil XFA kapsamlıdır

Bütçeler yan etkilerden önce denetlenir, sonra değil

Çalışma zamanındaki her sınır bir ön koşuldur; çünkü ayırma gerçekleştikten sonra uygulanan bütçe bütçe değildir. TXFAWidgetRuntimeOptions.Default, MaxWidgets için 10000, MaxValueChars için 1048576, MaxCalculationPasses için 16 ve MaxReflowPasses için 4 değeriyle gelir; varsayılan TXFAFormScriptOptions ise MaxOperations için 100000 ve MaxElapsedMilliseconds için 500 taşır. Altta XFA DOM kendi TXFADOMLimits değerlerini uygular: açılmış girdi ve çıktıda 128 MB üst sınırı, en çok 1024 birleştirilmiş paket, 1000000 düğüm ve 256 iç içelik derinliği. İki ayrıntı sayıların kendisinden daha önemlidir. Birincisi, betik bütçeleri betik başına değil işlem genelindedir: CommitEdit tek bir kalan işlem sayacı ve tek bir tekdüze son tarih tohumlar; her validate ve calculate çağrısı aynı sayacı azaltır ve yalnızca kalan milisaniyeleri alır, böylece iki yüz hesaplamalı alanı olan bir form tam 500 ms süreyi iki yüz kez harcayamaz. İkincisi, son tarih enjekte edilebilir bir MonotonicMilliseconds işlevinden gelir; geçen süre davranışını yoğun bir derleme aracısında yazı tura yerine test paketinde tekrar üretilebilir kılan da budur

HotPDF XFA çalışma zamanındaki bütçe katmanları: widget ve değer sınırlarından betik işlem ve süre sınırlarına, oradan XFA DOM üst sınırlarına kadar; bir işlemdeki her çağrının paylaştığı tek işlem sayacı ve tek son tarih
Betik bütçeleri betik başına değil işlem genelindedir; böylece iki yüz hesaplamalı alanın her biri yeni bir 500 ms talep edemez
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // varsayılan 10000
  Options.MaxCalculationPasses := 8;                       // varsayılan 16
  Options.MaxReflowPasses := 2;                            // varsayılan 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // tüm işlem
  Options.ScriptOptions.Limits.MaxElapsedMilliseconds := 200;
  Options.MeasureText :=
    function(const AText: UnicodeString; const AFont: TXFAFontSpec;
      AMaxWidth: Double): TXFATextExtent
    begin
      Result := MeasureWithHostCanvas(AText, AFont, AMaxWidth);
    end;
  Runtime := TXFAWidgetRuntime.Create(XDPBytes, 612, 792, Options);
  try
    Runtime.OnLayoutChanged :=
      procedure
      begin
        RepaintAllPages;   // yalnızca yeniden akış widget konumlarını gerçekten değiştirdiğinde tetiklenir
      end;
    // ... formu sür ...
  finally
    Runtime.Free;
  end;
end;

Çalışma zamanının durduğu yer ve bunu neden açıkça söylediği

Çalışma zamanı bilerek genel amaçlı bir XFA betik motoru değildir. DispatchEvent, enter ve exit etkinliklerini odağı taşıyarak yerel olarak işler; betik taşıyan diğer her etkinlikte ise rol yapmak yerine belirli ve kararlı bir tanıyla reddeder: addInstance, removeInstance ya da instanceManager sözcüklerini içeren betikler XFA runtime does not support event-driven instance mutation döndürür, .presence değerine dokunan betikler presence eşdeğerini döndürür ve geri kalan her şey XFA runtime does not support this event script döndürür. Üzerinde dallanabileceğiniz öngörülebilir bir ret, örnek dosyanızda çalışıp müşterininkinde ayrışan kısmi bir öykünmeden iyidir

İş parçacığı modeli aynı derecede nettir: bir çalışma zamanı örneği tek bir iş parçacığına aittir ve dahili kilitleme yoktur; çünkü yerleşim motoru konak ölçüm geri çağrılarına geri uzanır ve bunun etrafındaki bir kilit, yeniden çizim bekleyen bir kilitlenmedir. Alanların içindeki zengin içerik, kitaplığın geri kalanıyla aynı tutucu çizgiyi izler: exData yükleri XFA exData zengin metin ve köprüler konusunda anlatıldığı gibi işlenir; imza ve düğme widget türleri ReadOnly olarak döner, desteklenmeyen UI türleri ise veriyi sessizce kaybeden düzenlenebilir bir metin kutusu yerine xwkUnsupported olarak yüzeye çıkar

Bütün olarak bu, Delphi içinde dinamik XFA için uygulanabilir bir yanıttır: DOM yapısını canlı tutun, her düzenlemeyi ya tamamen yerleşen ya da geride hiçbir şey bırakmayan bir işlem yapın, her geçişi sınırlayın ve kapsam dışında olanı açıkça belirtin. Bunu bir hasar, vergi ya da sosyal yardım iş akışı için değerlendiriyorsanız, XFA çalışma zamanı HotPDF Delphi PDF component paketinin parçası olarak gelir; bu projelerin genelde birlikte ihtiyaç duyduğu AcroForm, düzleştirme ve işleme yollarıyla birlikte