مقاله فنی

رانتایم فرم XFA پویا در Delphi: تراکنش‌های HotPDF

HotPDF فرم‌های XFA پویا را در Delphi از طریق TXFAWidgetRuntime پر می‌کند، یک لایه ویجت مستقل از میزبان که هر ویرایش فیلد را به‌شکل یک تراکنش در نظر می‌گیرد: snapshot، اعتبارسنجی، محاسبه، reflow و بعد انتشار یا بازگشت کامل. این رانتایم تک‌نخی داخل میزبان VCL یا FMX خودتان اجرا می‌شود، به نصب Acrobat نیازی ندارد و هر بودجه‌ای را پیش از تخصیص هر چیزی اعمال می‌کند

این سناریو برای هر کسی که نرم‌افزار سند به کارهای دولتی یا بیمه عرضه کرده آشناست. یک فرم ادعا یا اظهارنامه مالیاتی به‌شکل PDF می‌رسد که محتوای صفحه‌اش یک اعلان «Please wait... if this message is not eventually replaced» است و همه فیلدهای واقعی در یک بسته XFA زندگی می‌کنند که فقط Adobe Acrobat رندرش می‌کند. کاربران شما می‌خواهند داخل برنامه شما پرش کنند. با rasterise کردن هم بیرون نمی‌روید، چون فرم با ورود داده ردیف اضافه می‌کند و چیدمان بعد از ردیف سوم همان چیدمانی نیست که در فایل منتشر شده

چرا XFA پویا هنوز مشکلی است که ارزش حل کردن دارد

XFA پویا دوام می‌آورد چون فرم‌های مستقر از فرمت حامل‌شان بیشتر زندگی می‌کنند. ISO 32000-1 §12.7.8 XFA را به‌شکل یک مدخل /XFA روی دیکشنری AcroForm توصیف می‌کند که یک جریان بسته XDP نگه می‌دارد، و ISO 32000-2 کل سازوکار را منسوخ اعلام کرده؛ منسوخ‌سازی آن را از نقشه راه برد، نه از میدان، و فرم‌های نوشته‌شده روی مشخصات XFA 3.3 هنوز صادر می‌شوند و هنوز از نظر حقوقی الزام‌آورند. XFA ایستا قابل تقلیل به حاشیه‌نویسی ویجت عادی است و HotPDF وقتی ApplyXFAAsAcroForm را صدا می‌زنید همین کار را می‌کند، با ملاحظاتی که در تخت کردن فرم‌های XFA به فیلدهای AcroForm پوشش داده شده. XFA پویا حیوان متفاوتی است: بازه‌های occur، متن رشدپذیر و اسکریپت‌های calculate مجموعه فیلدها را تابعی از داده می‌کنند، پس تا کاربر تایپش تمام نشده فهرست حاشیه‌نویسی ثابتی وجود ندارد که به آن تخت شود. این همان شکافی است که TXFAWidgetRuntime پر می‌کند، با زنده نگه داشتن DOM ای XFA، محاسبه مجدد چیدمان بعد از هر ویرایش پذیرفته‌شده و دادن یک آرایه تخت از ویجت‌های مکان‌دار به میزبان شما برای رسم و hit-test

‏رانتایم چه چیزی به برنامه میزبان می‌دهد؟

هندسه و حالت را می‌دهد و هیچ چیزِ فرض‌کننده یک ابزارک UI ندارد. TXFAWidgetRuntime مقادیر WidgetCount و Widgets[I] را به‌شکل رکوردهای TXFAWidgetState عرضه می‌کند که ID، Name، Kind، PageIndex، Bounds بر حسب نقطه PDF، Value، EditValue و پرچم‌های Focused، Editing، ReadOnly، Valid را حمل می‌کنند، در حالی که نقاشی، کشیدن caret و مسیریابی صفحه‌کلید در کد شما می‌ماند. هویت ویجت پایدار و ترتیبی است: هر ویجت یک ID به شکل name[n] می‌گیرد، که در آن n حضورهای قبلی آن نام فیلد را در ترتیب چیدمان می‌شمارد، پس ردیف دوم یک subform تکرارشونده amount[1] است. همان هویتی است که از بازسازی جان سالم می‌برد و همان چیزی است که FocusWidget، BeginEdit، DispatchEvent و HitTest همه با آن حرف می‌زنند. برای سندی که از قبل در یک نمونه THotPDF باز است، CreateLoadedXFAWidgetRuntime بسته‌های XDP را استخراج می‌کند، جعبه صفحه اول را به‌عنوان اندازه صفحه چیدمان می‌گیرد و وقتی فایل اصلاً XFA ندارد nil برمی‌گرداند

var
  Pdf: THotPDF;
  Runtime: TXFAWidgetRuntime;
  WidgetID: AnsiString;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('claim-dynamic.pdf');
    Runtime := Pdf.CreateLoadedXFAWidgetRuntime;   // nil وقتی هیچ /XFA وجود ندارد
    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)]));
      // hit test در فضای صفحه، بالاترین ویجت می‌برد
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

وقتی یک فیلد کامیت می‌شود چه چیزی باید اتمیک باشد؟

هر چیزی که ویرایش می‌تواند به آن دست بزند، که به‌مراتب بیشتر از مقدار فیلد است. CommitEdit پیش از نوشتن هر چیزی CaptureSnapshot را صدا می‌زند و آن snapshot چهار چیز را پوشش می‌دهد: DOM ای XFA سریال‌شده از TXFADocument.SaveToBytes، آرایه کامل رکوردهای تعامل TXFAWidgetState، شمارنده‌های LastCalculationPasses و LastReflowPasses و Warnings.Count فعلی. ذخیره تنها مقادیر گره میان‌بُر وسوسه‌کننده‌ای است و غلط است، چون یک اسکریپت calculate یا یک binding حل‌نشده می‌تواند EnsureValueNode را صدا بزند و گره‌های داده‌ای را مادی کند که هنگام شروع ویرایش وجود نداشتند؛ بازیابی فقط-مقدار راهی برای حذفشان ندارد، پس یک ویرایش ردشده پسماند ساختاری دائمی در بسته datasets می‌گذاشت. خود توالی کامیت سخت‌گیرانه است — نوشتن مقدار کاندید، اجرای validate برای فیلد ویرایش‌شده، اجرای calculate تا نقطه ثابت، سپس reflow تا چیدمان پایدار شود — و هر شکستی در هر مرحله از FailAndRestore عبور می‌کند که بایت‌های snapshot را در یک TXFADocument تازه لود می‌کند، فهرست ویجت‌ها را بازسازی می‌کند، حالت‌های تعامل ثبت‌شده را دوباره اعمال می‌کند، شمارنده‌ها را صفر می‌کند و Warnings را به طول snapshot آن برمی‌گرداند. LastDiagnostic در شکست دلیل را نگه می‌دارد و در حالت مرضی‌ای که خود بازیابی استثنا بدهد عبارت تحت‌اللفظی XFA transaction rollback failed را

HotPDF کامیت فیلد XFA را یک تراکنش در نظر می‌گیرد: DOM سریال‌شده، حالت هر ویجت، شمارنده‌های گذر و شمارش هشدارها را پیش از اعتبارسنجی، محاسبه و reflow ثبت می‌کند و بعد هر چهار را با هم منتشر یا بازیابی می‌کند
‏CommitEdit پیش از نوشتن هر چیزی چهار نوع حالت را snapshot می‌کند، پس یک validate، calculate یا reflow ناموفق هیچ پسماند ساختاری باقی نمی‌گذارد
function EditAmount(Runtime: TXFAWidgetRuntime;
  const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
  Current: UnicodeString;
begin
  Result := False;
  if not Runtime.BeginEdit(AWidgetID) then
    Exit;                                   // فقط‌خواندنی، یا چنین ویجتی وجود ندارد
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // بازه بد، یا surrogate جدا شده
    Exit;
  end;
  Result := Runtime.CommitEdit;             // همه یا هیچ
  if not Result then
    // سند، ویجت‌ها، شمارنده‌ها و هشدارها از قبل به حالت پیش از ویرایش
    // برگشته‌اند؛ ویجت فوکوس‌شده صرفاً نامعتبر علامت می‌خورد
    ShowMessage(Runtime.LastDiagnostic);
end;

ReplaceSelection شایسته یادداشت خودش است، چون جایی است که ورودی بدشکل ارزان‌ترین جا رد می‌شود. انتخابی که یک جفت UTF-16 surrogate را می‌شکند را رد می‌کند، متن جایگزین حاوی surrogate بالا یا پایین جفت‌نشده را رد می‌کند و هر نتیجه‌ای بلندتر از MaxValueChars را رد می‌کند. گرفتنش در لایه کلید یعنی سازوکار تراکنش هرگز مجبور به باز شدن یک کاراکتر نیم‌نوشته از صفحه فرا نمی‌شود

بازسازی در یک فهرست خصوصی، انتشار در یک جابه‌جایی

بازسازی ویجت هرگز نباید نیمه‌تمام قابل مشاهده باشد، پس RebuildWidgets یک TObjectList مالکیت‌دار کاملاً جدا می‌سازد و در انتها با یک انتساب تنها آن را جا می‌کند. دلیل زیبایی‌شناسی نیست: TXFALayoutEngine.ComputeLayout در حین جریان بازسازی اجرا می‌شود و از طریق تابع MeasureText که شما داده‌اید به کد میزبان برمی‌گردد، و وقتی حد ویجت برسد می‌تواند EXFAWidgetRuntimeError پرتاب کند. اگر رانتایم فهرست زنده‌اش را در‌جا جهش می‌داد، هر مسیر میزبان را با فهرستی رها می‌کرد که نیمی چیدمان قدیم و نیمی جدید بود، با اشاره‌گرهای DataNode به سندی که در آستانه rollback است. همگرایی reflow سپس توسط LayoutSignature تعیین می‌شود، رشته‌ای ساخته‌شده از تعداد ویجت به‌علاوه هر ID، ایندکس صفحه و کادر مرزی گرد‌شده به چهار رقم اعشار: CommitEdit بازسازی می‌کند، امضاها را مقایسه می‌کند و تکرار می‌کند تا دو امضای متوالی مطابقت کنند یا بودجه گذر تمام شود. وقتی امضا اصلاً عوض نشد، LastReflowPasses در 0 می‌ماند؛ همین راه شماست که ویرایش فقط-مقدار را از ویرایشی که واقعاً فرم را بزرگ کرد تفکیک کنید، و حالت تعامل با هر بازسازی توسط ویجت ID حمل می‌شود، پس فوکوس و ویرایش در جریان از یک درج ردیف جان سالم می‌برند

‏رانتایم ای XFA در HotPDF فهرست ویجت‌هایش را در یک فهرست مالکیت‌دار جدا بازسازی می‌کند در حالی که چیدمان اجرا می‌شود و به کد اندازه‌گیری میزبان برمی‌گردد، سپس فهرست تمام‌شده را با یک انتساب تنها منتشر می‌کند که میزبان نمی‌تواند نیمه‌تمام ببیند
بازسازی در یک فهرست خصوصی اتفاق می‌افتد چون ComputeLayout می‌تواند وسط راه استثنا بدهد، و LayoutSignature تعیین می‌کند دو reflow متوالی کجا همگرا شده‌اند

چرا یک فیلد bound رکورد اشتباه را می‌خواند؟

چون اسکریپت بدون زمینه داده اجرا شد. یک فیلد با <bind match="dataRef" ref="$record.actual"/> صریح و یک فیلد هم‌نام با آن گره داده، دو ویجت متفاوت‌اند که به یک مقدار اشاره می‌کنند، و یک subform تکرارشونده با <occur max="2"/> چند ویجت تولید می‌کند که نام مشترک دارند و فقط در ردیف داده متعلق بهشان تفاوت دارند؛ اعتبارسنجی و محاسبه را روی ریشه سند بسنجید و هر کدام this را به اولین گره منطبق در کل بسته datasets حل می‌کند، پس ردیف دو به‌طور بی‌صدا ردیف یک را اعتبارسنجی می‌کند. HotPDF این را با ذخیره DataNode حل‌شده روی هر مدخل ویجت هنگام تولید چیدمان پرهیز می‌کند، سپس آن گره را از میان هر دو فراخوان HPDFXFAEvaluateFieldScript برای xfskValidate و xfskCalculate می‌فرستد. همین زمینه تعیین می‌کند EnsureValueNode وقتی محاسبه‌ای روی binding‌ای که هنوز وجود ندارد هدف می‌گیرد علیه کدام گره بسازد، و وقتی هیچ binding‌ای حل نمی‌شود کامیت به‌طور تمیز با XFA calculation target is not bound شکست می‌خورد نه اینکه در ردیف اشتباه بنویسد. معناشناسی FormCalc پشت آن اسکریپت‌ها پژواک همان چیزی است که اسناد AcroForm از actionهای توصیف‌شده در فرمت AcroForm و اسکریپت‌های calculate می‌گیرند، اما قواعد حل این‌جا به دامنه XFA محدود است نه نام فیلد

بودجه‌ها قبل از اثرات جانبی بررسی می‌شوند نه بعد

هر حدی در رانتایم یک پیش‌شرط است، چون بودجه‌ای که بعد از اتفاق افتادن تخصیص اعمال شود بودجه نیست. TXFAWidgetRuntimeOptions.Default مقادیر MaxWidgets را در 10000، MaxValueChars را در 1048576، MaxCalculationPasses را در 16 و MaxReflowPasses را در 4 منتشر می‌کند، و پیش‌فرض‌های TXFAFormScriptOptions مقادیر MaxOperations را در 100000 و MaxElapsedMilliseconds را در 500 حمل می‌کنند. زیر آن‌ها DOM ای XFA حد‌های خودش یعنی TXFADOMLimits را اعمال می‌کند: سقف 128 مگابایتی روی ورودی و خروجی فشرده‌زدایی‌شده، حداکثر 1024 بسته دوخته‌شده، 1000000 گره و عمق تودرتویی 256. دو جزئیات از خود اعداد مهم‌ترند. اول، بودجه اسکریپت‌ها به سطح کل تراکنش است نه به ازای هر اسکریپت: CommitEdit یک شمارنده عملیات باقی‌مانده و یک مهلت یکنوا بذر می‌کند و هر فراخوان validate و calculate از همان شمارنده برداشت می‌کند و فقط میلی‌ثانیه‌های باقی‌مانده را می‌گیرد، پس فرمی با دویست فیلد محاسبه‌گر نمی‌تواند کل 500 میلی‌ثانیه را دویست بار خرج کند. دوم، مهلت از تابع تزریق‌پذیر MonotonicMilliseconds می‌آید، که همین باعث می‌شود رفتار زمان سپری‌شده در مجموعه تست بازتولیدپذیر باشد به‌جای شیر یا خط روی یک build agent شلوغ

لایه‌های بودجه در رانتایم ای XFA در HotPDF، از حد‌های ویجت و مقدار تا حد‌های عملیات و زمان اسکریپت و سرانجام سقف‌های DOM ای XFA، با یک شمارنده عملیات و یک مهلت مشترک میان همه فراخوان‌های یک تراکنش
بودجه اسکریپت‌ها به سطح کل تراکنش است نه به ازای هر اسکریپت، پس دویست فیلد محاسبه‌گر نمی‌توانند هر کدام 500 میلی‌ثانیه تازه ادعا کنند
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // پیش‌فرض 10000
  Options.MaxCalculationPasses := 8;                       // پیش‌فرض 16
  Options.MaxReflowPasses := 2;                            // پیش‌فرض 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // کل تراکنش
  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;   // فقط وقتی reflow واقعاً ویجت‌ها را جابه‌جا کرد صدا می‌خورد
      end;
    // ... فرم را هدایت کنید ...
  finally
    Runtime.Free;
  end;
end;

‏رانتایم کجا متوقف می‌شود و چرا بلند می‌گوید

‏رانتایم آگاهانه یک موتور اسکریپت‌نویسی XFA عمومی نیست. DispatchEvent فعالیت‌های enter و exit را به‌طور بومی با جابه‌جایی فوکوس مدیریت می‌کند و برای هر فعالیت دیگری که اسکریپت حمل کند با یک دیاگنوستیک مشخص و پایدار امتناع می‌کند به‌جای تظاهر: اسکریپت‌هایی که addInstance، removeInstance یا instanceManager را ذکر می‌کنند XFA runtime does not support event-driven instance mutation برمی‌گردانند، اسکریپت‌هایی که به .presence دست می‌زنند معادل presence آن را برمی‌گردانند و بقیه XFA runtime does not support this event script برمی‌گردانند. یک امتناع قابل پیش‌بینی که رویش شاخه بزنید بهتر از شبیه‌سازی جزئی‌ای است که روی فایل نمونه شما کار می‌کند و روی فایل مشتری واگرا می‌شود

مدل نخ‌کشی به همان اندازه بی‌پرده است: یک نمونه رانتایم متعلق به یک نخ است، بدون قفل داخلی، چون موتور چیدمان به کال‌بک اندازه‌گیری میزبان برمی‌گردد و یک قفل دور آن انتظار مرگ برای یک repaint است. محتوای غنی داخل فیلدها همان خط محافظه‌کارانه بقیه کتابخانه را دنبال می‌کند، جایی که بارهای exData همان‌طور که در متن غنی و لینک‌های XFA exData توضیح داده شده مدیریت می‌شوند، و ویجت‌های امضا و دکمه به‌شکل ReadOnly برمی‌گردند در حالی که انواع UI پشتیبانی‌نشده به‌شکل xwkUnsupported ظاهر می‌شوند نه به‌شکل جعبه متن قابل ویرایشی که بی‌صدا داده از دست بدهد

در کنار هم، این یک پاسخ کارا برای XFA پویا در Delphi است: DOM را زنده نگه دارید، هر ویرایش را تراکنشی کنید که یا کامل فرود می‌آید یا هیچ چیز باقی نمی‌گذارد، هر گذر را محدود کنید و درباره چیزی که خارج از محدوده است صریح باشید. اگر برای گردش کار ادعا، مالیات یا مزایا ارزیابی‌اش می‌کنید، رانتایم ای XFA بخشی از کامپوننت HotPDF Delphi PDF منتشر می‌شود، کنار مسیرهای AcroForm، تخت‌سازی و رندری که این پروژه‌ها معمولاً در نهایت با هم به آن‌ها نیاز پیدا می‌کنند