مقاله فنی

چرا ویرایش‌های فیلد XFA در ذخیره در PDFium برای Delphi ناپدید می‌شوند

TPdf.SetFocusedFormFieldText در PDFium Component درون بافر ویرایش زنده‌ی فیلد فرم فعلاً کانون‌شده می‌نویسد، و برای یک فرم XFA آن بافر هرگز به بسته‌ی datasetsای که به دیسک serialize می‌شود نمی‌رسد — پس مقداری که یک کاربر تایپ می‌کند، و کد شما تأیید می‌کند پذیرفته شده، بی‌سروصدا دفعه‌ی بعدی که فایل باز می‌شود گم شده است. فیلدهای AcroForm این مسئله را ندارند: همان فراخوانی همان لحظه‌ای که کانون دور می‌شود به ورودی /V فیلد commit می‌شود. کاربری که یک فرم دریافتی XFA را پر می‌کند، ذخیره می‌کند، و دوباره باز می‌کند تا فیلد مبلغ را دوباره خالی ببیند، به یک اشکال رندر برنخورده — به لبه‌ی چیزی برخورده که خودِ موتور PDFium برای نوشتن داده‌ی فرم در معرض دید می‌گذارد

این سؤالی محدودتر از تشخیص یک فرم XFA در وهله‌ی اول است، یا اجراکردن جاوااسکریپتش: نه «آیا PDFium از XFA پشتیبانی می‌کند» و نه «چطور اسکریپت‌های AcroForm را اجرا کنم» بلکه به‌طور مشخص اینکه پس از اینکه SetFocusedFormFieldText موفقیت گزارش می‌دهد چه اتفاقی برای یک مقدار می‌افتد. نسخه‌ی کوتاه این است که AcroForm و XFA تا آنجا که به مسیر نوشتن PDFium مربوط می‌شود دو گویش از همان مدل فرم نیستند — آن‌ها دو مدل فرم با دو رابطه‌ی کاملاً متفاوت بین آنچه یک کاربر تایپ می‌کند و آنچه یک ذخیره واقعاً می‌گیرد هستند، و یکی‌گرفتن این دو چیزی است که یک فراخوانی API تک‌خطی را سه هفته پس از زنده‌شدن استقرار آزمایشی یک مشتری به یک تیکت پشتیبانی تبدیل می‌کند. مقاله‌ی جاوااسکریپت AcroForm فراخوانی تک‌خطی را نشان می‌دهد و نتیجه‌ی AcroForm-در-برابر-XFA را در یک کامنت کد بیان می‌کند؛ این یکی روی همان API می‌ماند و مسیر نوشتن داخلی، اثبات بسته‌ی-datasets که ویرایش XFA هرگز فرود نمی‌آید، چرا شکاف در خودِ PDFium می‌نشیند نه در binding از Delphi، و یک راه‌حل موقت وصله-XML-خودتان برای اسنادی که نیاز دارند ویرایش از یک ذخیره جان سالم به‌در ببرد را طی می‌کند

SetFocusedFormFieldText چطور یک مقدار فیلد را می‌نویسد؟

TPdf.SetFocusedFormFieldText با شبیه‌سازی یک ویرایش در سطح فشردن-کلید کار می‌کند، نه با فروکردن مستقیم یک مقدار درون مدل سند. داخلاً FORM_SelectAllText را فرا می‌خواند تا محتوای فعلی فیلد کانون‌شده را انتخاب کند، سپس FORM_ReplaceSelection را برای بازنویسی انتخاب با رشته‌ی جدید — همان دو عملیاتی که یک انتخاب-همه-و-تایپ صفحه‌کلید-محور ماشه می‌کشید. چون نوشتن از طریق مسیر ویرایش متن تعاملی PDFium عبور می‌کند نه دورش، هر فشردن‌کلید، اسکریپت format، یا calculate متصل به فیلد دقیقاً همان‌طور که برای یک انسان که تایپ می‌کند شلیک می‌شود، که چیزی است که API را برای پرکردن برنامه‌ای فرم در یک نمایشگری که جاوااسکریپت را زنده نگه می‌دارد مفید می‌کند. همتای سمت-خواندن FocusedFormFieldText است، پشتیبانی‌شده با FORM_GetFocusedText، و همان بافر زنده‌ای را که SetFocusedFormFieldText همین الان نوشته منعکس می‌کند

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

چرا AcroForm مقدار را نگه می‌دارد و XFA آن را گم می‌کند؟

فیلدهای متنی و combo از نوع AcroForm دوام می‌آورند چون محیط form-fill خودِ PDFium بافر ویرایش را برایتان commit می‌کند: همان لحظه‌ای که فیلد کانون را از دست می‌دهد، بافر درون ورودی /V فیلد نوشته می‌شود، همان کلیدی که هر خواننده‌ی PDF مطابق برای فهمیدن مقدار ذخیره‌شده‌ی یک فیلد به آن نگاه می‌کند. TPdf.ClearFormFieldFocus — که FORM_ForceToKillFocus را زیر پوسته فرا می‌خواند — آن commit را در تقاضا اجبار می‌کند، پس کدی که یک مقدار را برنامه‌ای تنظیم می‌کند مجبور نیست منتظر یک کلیک واقعی ماوس جای دیگری در UI بماند. بلافاصله پس از آن ذخیره کنید، و متن جدید بخشی از گراف شیء سند است پیش از اینکه TPdf.SaveAs اصلاً اجرا شود، چون /V یک ورودی واقعی در یک دیکشنری فیلد واقعی است، نه چیزی که بعداً پیچ شده

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

یک ویرایش فیلد XFA واقعاً کجا زندگی می‌کند؟

فیلدهای XFA چنین سیم‌کشی‌ای ندارند. متنی که یک کاربر تایپ می‌کند در یک بافر CPWL_Edit فرود می‌آید که متعلق به لایه‌ی رندر و تعامل XFA در PDFium است، و آن لایه هیچ مسیر کدی ندارد که بافر را به بسته‌ی datasets ذخیره‌شده در PDF برگرداند. TPdf.GetXfaDatasets این شکاف را قابل‌مشاهده می‌کند: آن را پیش و پس از یک ویرایش روی یک فیلد XFA فراخوانی کنید و بایت‌هایی که برمی‌گرداند یکسان هستند، چون متد بسته‌ی اصلی‌ای را می‌خواند که سند با آن باز شده، هرگز وضعیت زنده‌ی ویجتی که همین الان ویرایش کردید. هیچ چیزی درباره‌ی آن یک باگ کشینگ یا یک مسئله‌ی زمان‌بندی refresh نیست — بسته‌ی datasets روی دیسک و بافر ویرایش در حافظه صرفاً دو تکه‌ی متفاوت از وضعیت هستند که API عمومی PDFium هرگز آن‌ها را به هم وصل نمی‌کند

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

آیا این یک باگ در PDFium Component است یا یک محدودیت در PDFium؟

تکه‌ی گمشده درون خودِ PDFium می‌نشیند، نه در binding از Delphi رویش. API عمومی PDFium هیچ FPDF_SetXFAPacketای برای تزریق یک بسته‌ی به‌روزشده و هیچ FPDF_SaveAsXFAای برای درخواست از موتور XFA برای serialize‌کردن DOM فعلی‌اش دوباره به XML از نوع datasets پیش از یک ذخیره ندارد. FPDF_SaveAsCopy — exportی که پشت TPdf.SaveAs است — گراف شیء سندی را که PDFium از پیش دارد بیرون می‌نویسد؛ هیچ قلابی برای درخواست از موتور XFA برای flush‌کردن وضعیت زنده‌اش ابتدا ندارد، چون آن قلاب بالادست وجود ندارد. PDFium Component نمی‌تواند تطبیقی را که خودِ PDFium هرگز پیاده‌سازی نکرده اضافه کند، و عرضه‌ی یک سریالایزر DOM-به-XML خانگی که وضعیت داخلی XFA در PDFium را حدس می‌زند از شکاف صادقانه بدتر می‌بود: به‌نظر می‌رسید کار می‌کند تا نسخه‌ی بعدی PDFium چیزی را تغییر دهد که هیچ‌کس خارج از پروژه نمی‌تواند ببیند

این مرز در طول همان ممیزی v2.13.2 که SetFocusedFormFieldText را در وهله‌ی اول ساخت نمایان شد. FORM_ReplaceSelection برای نسخه‌ها در جدول import DLL بدون اینکه هرگز از کد Pascal فراخوانی شود بایند شده بود، و افزودن مسیر نوشتنی که در نهایت از آن استفاده کرد چیزی است که شکاف دوام را به‌اندازه‌ی کافی مشخص برای مستندکردن به‌جای نظری کرد. همان دور ممیزی یک شکاف بی‌ربط اما مرتبط-در-روحیه پیدا کرد: جاوااسکریپت AcroForm از v2.13.0 بی‌سروصدا غیرفعال شده بود چون پلتفرم JS فقط درون شاخه‌ی مقداردهی اولیه‌ی XFA سیم‌کشی شده بود، پس اسناد معمولی AcroForm با app.alert یا فیلدهای محاسبه‌شده اصلاً هرگز یک موتور اسکریپت دریافت نمی‌کردند. آن یکی قابل‌رفع بود — گسترش پلتفرم JS به هر سند صرف‌نظر از XFA — و در همان نسخه عرضه شد؛ شکاف دوام پوشش‌داده‌شده در اینجا قابل‌رفع نبود، به دلایل بالا. رفع اشکال جاوااسکریپت و رویدادهای host-veto پیرامون آن در اجرای جاوااسکریپت AcroForm با PDFium Component پوشش داده شده

در Delphi باید درباره‌اش چه کاری انجام دهید؟

برای اسناد AcroForm، رفع اشکال چیزی بیش از یک عادت خوب نیست: ClearFormFieldFocus را فرا بخوانید (یا کانون را به‌شکل دیگری دور کنید) پیش از SaveAs هر زمانی که یک مقدار برنامه‌ای تنظیم شده، به‌جای فرض‌کردن اینکه یک تعامل UI بعدی commit را برایتان ماشه می‌کشد. برای سندی که ممکن است AcroForm یا XFA باشد — که حالت رایج در یک نمایشگر عمومی‌منظور است — FormType یا بولی XFA را بررسی کنید پیش از اینکه به یک فراخواننده وعده دهید یک ذخیره می‌چسبد، و تشخیص فرم‌های XFA و استخراج بسته‌های XFA را برای مجموعه‌ی کامل کاوش‌ها بخوانید، از جمله حالت XFAF که در آن محتوای XFA روی ویجت‌های در غیر این‌صورت-معمولی AcroForm لایه‌بندی شده که واقعاً /V را رعایت می‌کنند

برای یک فرم واقعاً پویای XFA که مقادیر ویرایش‌شده باید از یک ذخیره جان سالم به‌در ببرند، بافر ویرایش تعاملی اصلاً ابزار درستی نیست. مسیر پایدار این است که GetXfaDatasets را به‌عنوان خط‌مبنای خودتان در نظر بگیرید، نه نتیجه‌تان: آن را یک‌بار وقتی سند باز می‌شود بخوانید، رکورد خودتان از آنچه کاربر فیلد-به-فیلد تغییر داده نگه دارید — دقیقاً همان مقادیری که UI شما از پیش دارد، چون PDFium آن‌ها را پس از واقعه به شما پس نخواهد داد — آن‌ها را خودتان درون XML خط‌مبنا وصله کنید، و خروجی خودتان را هدایت کنید. نوشتنی که از طریق XMLای که کد خودتان کنترل می‌کند عبور می‌کند از یک ذخیره‌ای که یک بافر CPWL_Edit هرگز نمی‌توانست جان سالم به‌در ببرد

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

یافتن شکاف پیش از اینکه یک مشتری آن را بیابد

TPdf.SaveAs صرف‌نظر از اینکه آیا یک مقدار فیلد XFA جان سالم به‌در برده یا نه True برمی‌گرداند، چون از دید PDFium، ذخیره واقعاً موفق شده — هر بایتی را که از آن خواسته شده بنویسد نوشته. این دقیقاً همان نوع نقصی می‌سازد که از یک smoke test رد می‌شود و به یک مشتری می‌رسد: هیچ چیزی throw نمی‌کند، هیچ چیزی لاگ نمی‌کند، فایل خوب باز می‌شود، فقط مقدار مشخص اشتباه است. یک تست رفت‌وبرگشتی که واقعاً فایل ذخیره‌شده را دوباره باز می‌کند و مقدار فیلد را مقایسه می‌کند — یا GetXfaDatasets را پیش و پس مقایسه می‌کند، مطابق مثال قبلی — به مجموعه‌ی رگرسیون هر نمایشگری تعلق دارد که به کاربران اجازه‌ی ویرایش محتوای XFA می‌دهد، نه فقط مسیرهای AcroForm که اتفاقاً به‌طور پیش‌فرض کار می‌کنند

هیچ‌کدام از این‌ها آن‌قدرها یک نقص برای ثبت در برابر PDFium Component نیست که یک مرز برای طراحی حول آن باشد: SetFocusedFormFieldText دقیقاً همان کاری را که نامش برای هر دو مدل فرم می‌گوید انجام می‌دهد، و تفاوت در نتیجه تمیز به آنچه AcroForm و XFA هرکدام آن بافر را در سمت PDFium به آن سیم می‌کنند برمی‌گردد. API، پریمیتیوهای کانون و ذخیره، و خواننده‌های بسته که در اینجا ارجاع داده شد بخشی از کامپوننت PDFium برای Delphi و C++Builder هستند