PDFium Component مقدارهای ویرایششده فرم XFA را عیناً ذخیره میکند، در طول ذخیره و باز کردن دوباره، وقتی runtime یعنی pdfium.v8.dll مربوط به Windows V8 را اجرا کند که در v3.125.2 یا بعدتر شپ شده. runtimeهای قدیمیتر به مقدار فیلدها line feed اضافه میکردند، emoji را به یک کاراکتر BMP بیربط میبُریدند، ذخیرههای XFA تک-stream را بیسروصدا رد میکردند و میتوانستند یک نوشتن نهایی ناموفق را قورت بدهند. یک symptom مربوط به باز کردن دوباره اصلاً نقص کتابخانه نیست: فرم پویایی که subform ریشهاش restoreState="auto" ندارد layout خود را از template بازسازی میکند
باگریپورتهای این ماجرا همه شبیه هم بودند. مشتری یک فرم ادعای XFA را در یک viewer دلفی پر میکند، ذخیره میکند، دوباره باز میکند، و یک چیزی کمی جای دیگری است. جعبه کامنتهای خالی حالا یک خط خالی دارد، و بعد از یک ذخیره دوم دو تا. اسمی که با یک emoji تایپ شده با یک گلیف private-use برمیگردد. هیچکس خطا نمیگیرد، و همین است که این باگها را پرهزینه میکند: drift چند هفته بعد در خروجی somebody دیگر بیرون میزند
وقتی یک فرم XFA ذخیره و دوباره باز میشود چه چیزی خراب میشود؟
چهار نقص جداگانه در مسیر ذخیره بومی XFA باعث drift مقدارها میشد، و هر کدام پشت یک ذخیره موفق-به-نظر-رسنده قایم میشد. دو تاشان از serialization میآمدند، یکی از چیدمان ذخیرهسازی تک-stream، و یکی از خود PDF writer. جدول هر symptom را به علتش و به نسخهای که PDFium Component فیکسش کرد مپ میکند
| Symptom بعد از reopen | علت | فیکسشده در |
|---|---|---|
| فیلد خالی یک line feed دارد؛ مقدارها با هر ذخیره یک خط جدید میگیرند | هر دو نویسنده XFA بعد از تگهای شروع خط جدید چیدمانی درج میکردند | v3.125.2، pdfium.v8.dll |
| مقدار U+1F642 بهشکل U+F642 برمیگردد، یا emoji از پاکت فرم حذف میشود | برش wchar_t شانزدهبیتی در رمزگشایی؛ فیلتر surrogate در serializer فرم | v3.125.2، pdfium.v8.dll |
| ویرایشها در یک سند XFA تک-stream کلاً ناپدید شدهاند | ذخیره بومی چیدمان stream را رد میکرد، اما مقدار برگشتی نادیده گرفته میشد | v3.125.2؛ کامنتها و processing instructionها از v3.126.0 حفظ میشوند |
| فایل بریدهشده با اینکه ذخیره موفقیت گزارش کرده بود | نوشتن بافری نهایی بعد از اینکه نویسنده از قبل موفقیت گزارش کرده بود شکست خورد | runtime V8 در v3.125.2؛ pdfium.dll معمولی در v3.125.3 |
| فرم پویای سهصفحهای بهشکل دو صفحه باز میشود | subform ریشه مقدار restoreState="auto" را درخواست نمیکند | نویسندگی فرم، نه نقص کتابخانه |
نوشتههای قبلی نتیجه میگرفتند ویرایشهای فیلد XFA اصلاً با PDFium قابل پایدارسازی نیستند، که برای runtimeهای آن روزها درست بود. runtime جدیدتر V8 مقدارهای XFA را بهطور بومی ذخیره میکند، پس ویرایشی که در فرم زنده انجام شده بدون جراحی پاکت از سمت شما به پاکت datasets ذخیرهشده میرسد
کدام runtime یعنی PDFium مقدارهای XFA را ذخیره میکند؟
وفاداری ذخیره XFA به DLL بومی وابسته است نه به wrapper دلفی، پس اولین چک این است که پروسه شما واقعاً کدام runtime را load کرده. PDFium Component بهازای هر معماری دو build برای Windows شپ میکند: مقدار pdfium.dll معمولی که بدون V8 و XFA ساخته شده، و pdfium.v8.dll که موتور JavaScript و runtime فرم XFA را حمل میکند. فقط pdfium.v8.dll میتواند یک فرم XFA را اجرا کند، پس هر فیکس XFA این مقاله همانجا زندگی میکند، از کتابخانههای V8 بازسازیشده Win32 و Win64 در v3.125.2 به بعد
فیکس نوشتن-نهایی کد عمومی PDF writer است، پس برای اسناد معمولی هم مهم میشود. v3.125.3 کتابخانههای pdfium.dll معمولی را بازسازی کرد تا همان ترمیم را حمل کنند. سورس مشترک اثبات رفتار مشترک نیست: تا وقتی باینری بازسازی نشده، DLL قدیمی باگ قدیمی را نگه میدارد
تله دوم در loader بود. قبل از v3.125.2، ست کردن EnableV8Engine روی True باعث میشد binding نام پیشفرض یعنی pdfium.v8.dll را بردارد و یک مسیر کامل در LibraryName را نادیده بگیرد. برنامهای که به یک runtime تازه-deployشده اشاره میکرد میتوانست همچنان یک کپی قدیمیتر را از پوشه دیگری load کند. از v3.125.2 به بعد، یک LibraryName که درونش پوشه باشد در هر دو حالت موتور دقیقاً همان فایل را انتخاب میکند، و مسیر مفقود بهجای fallback به کتابخانه همراه دیگر شکست میخورد
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// یک مسیر در LibraryName همین فایل را پین میکند (v3.125.2 و بعدتر)؛
// اگر فایل نباشد، load کردن raise میکند بهجای fallback
{$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; // در راهاندازی شکست بخور، نه در اولین ذخیره
end;
بعد از باز کردن یک سند، مقدار TPdf.XFA به شما میگوید فایل XFA دارد و TPdf.XfaRuntimeAvailable میگوید DLL بارگذاریشده واقعاً میتواند اجرایش کند. اگر هم لازم است فرمهای استاتیک و پویا را تفکیک کنید، مقدار TPdf.FormType برمیگرداند ftXfaFull یا ftXfaForeground؛ مقاله تشخیص فرمهای XFA و استخراج پاکتهای XFA در دلفی همان سرک کشیدن را با جزئیات پوشش میدهد
چرا فیلدهای XFA ذخیرهشده line feed اضافه میگیرند؟
فیلدهای XFA ذخیرهشده line feed میگرفتند چون هر دو نویسنده XFA بومی، نویسنده المان XML عمومی و serializer پاکت فرم، خروجیشان را با یک خط جدید بعد از تگهای شروع قشنگ-چاپ میکردند. در بیشتر XML ها آن فاصله سفید تزئینی است. در داده XFA نیست: وقتی پاکت datasets دوباره parse میشود، متن بین <Comments> و </Comments> همان مقدار فیلد است، با خط جدید. پس یک فیلد خالی با یک LF منفرد دوباره باز میشد، و هر چرخه ذخیره-و-باز-کردن بعدی میتوانست یکی دیگر اضافه کند
ترمیم واضح یعنی کوتاه کردن مقدارها موقع load، غلط بود. کاربرها فاصلههای ابتدایی و انتهایی و متن چند-خطی عمدی داخل فیلدهای XFA تایپ میکنند، و یک بلاک آدرس یا یک کد با-عرض-ثابت باید بایت به بایت جان سالم به در ببرد. پس فیکس v3.125.2 فقط آن فاصله سفیدی را حذف میکند که خود serializer دور تگها سنتز کرده بود. مقدارهای کاربر و گرههای متنی موجود و بخشهای CDATA دستنخورده عبور میکنند، پس مقدار " indented" تورفتگیاش را نگه میدارد و فیلدی که عمداً خالی است خالی میماند
چرا یک emoji بهشکل یک کاراکتر متفاوت برمیگردد؟
یک emoji اشتباه برمیگشت چون مقدار wchar_t مربوط به Windows 16 بیت عرض دارد، و دو مسیر رمزگشایی یک مقدار اسکالر کامل Unicode را داخل یک wchar_t منفرد ذخیره میکردند. decoder مربوط به stream یعنی UTF-8 و parser ارجاعهای کاراکتر عددی مثل 🙂 هر دو همین کار را میکردند. مقدار U+1F642 یعنی صورت لبخند-ملایم در 16 بیت جا نمیشود، پس بیتهای بالا میافتادند و U+F642 ظاهر میشد: یک code point در Private Use Area که بیشتر فونتها آن را یک جعبه یا هیچ رندر میکنند
serializer فرم مشکل معکوس داشت. کاراکترها را یکییکی به اندازه یک wchar_t فیلتر میکرد، دو code unit یعنی surrogate را میدید که در انزوا نامعتبرند، و هر دو را میانداخت، پس emoji کلاً از پاکت فرم محو میشد. در v3.125.2 decoder هر مقدار اسکالر را کامل مصرف میکند و یک جفت surrogate درست تولید میکند. وقتی فقط یک جای خروجی مانده باشد، surrogate پایین را معوق نگه میدارد و تا وقتی آن واحد هنوز بافر است end-of-stream گزارش نمیکند. یک دنباله UTF-8 که بین بلوکهای خواندن بریده شود به خواندن بعدی منتقل میشود بهجای دور انداخته شدن. صادرکننده فرم حالا جفتهای surrogate معتبر را با هم نگه میدارد، و ارجاعهای کاراکتر عددی هم جفتهای درست تولید میکنند
داده تستی Latin-1 هیچکدام از اینها را نشان نمیدهد، پس هر تست round-trip مربوط به XFA حداقل به یک کاراکتر از صفحه مکمل نیاز دارد
XFA تک-stream و شکستهای ذخیره که هیچکس نمیدید
یک سند XFA تک-stream ویرایشهایش را از دست میداد چون helper ذخیره بومی آن چیدمان ذخیرهسازی را رد میکرد و فراخوانندهاش شکست را نادیده میگرفت. ISO 32000-1 §12.7.8 اجازه میدهد دریه /XFA دیکشنری فرم تعاملی یا یک آرایه از نام پاکتها و streamها باشد یا یک stream منفرد که کل سند XDP را حمل میکند. آرایههای پاکت حالت رایجاند، اما streamهای منفرد کاملاً قانونیاند، و ذخیره PDF انگار هیچ اتفاقی نیفتاده تمام میشد در حالی که داده فرم روی مقادیر قدیمیاش مانده بود
از v3.125.2 به بعد، runtime یعنی V8 زیرمجموعه پشتیبانیشده تک-stream را هندل میکند. اول هر دو پاکت زنده یعنی datasets و form را به یک ناحیه stage برده و اعتبارسنجی میکند، و فقط بعد از آن پاکتهای همخوان را در XDP اصلی جایگزین میکند. بقیه پاکتها و اعلانهای namespace ریشه حفظ میشوند. اگر staging شکست بخورد، stream یعنی XFA پایدار هرگز لمس نمیشود و سند علامت ویرایشش را نگه میدارد
کامنتهای XML و processing instructionها مراقبت اضافه میخواستند چون DOM داخلی XML آنها را میاندازد. در v3.125.2 حضورشان باعث میشد ذخیره کلاً شکست بخورد بهجای اینکه محتوا بیصدا از دست برود. v3.126.0 آنها را حفظ میکند: قبل از parse، هر کامنت یا processing instruction با یک marker ساختهشده از یک پیشوند سواپ میشود که هیچجای متن اصلی رخ نمیدهد. بعد از جایگزینی پاکتهای زنده، هر marker باید دقیقاً یک بار ظاهر شود قبل از اینکه token اصلی برگردد و stream نوشته شود. tokenهای بیرون پاکتهای جایگزینشده پس متن و ترتیب خودشان را نگه میدارند، از جمله tokenها در prolog و template و بقیه پاکتها
بعضی ورودیها همچنان عمداً رد میشوند، و هر رد یک شکست ذخیره صریح است:
- کامنتها یا processing instructionها داخل پاکتهای زنده یعنی
datasetsیاform، چون موقعیتهای اصلیشان را نمیشود به محتوای تازه-صادرشده مپ کرد - اعلانهای DTD و امضاهای XMLDSig، چون بازنویسی XDP نمیتواند یک امضای XML را معتبر نگه دارد
- رمزگذاری نامعتبر UTF-8 یا UTF-16 و تگهای ناقص و ارجاعهای کاراکتر نامعتبر و entityهای ناشناخته و processing instructionهای بدفرم، که بهجای ترمیم بیصدا رد میشوند
خروجی تک-stream یعنی UTF-8 است و مدل محتوای XML را حفظ میکند، نه چیدمان بایت اصلی یا اعلان رمزگذاری
نقص آخر زیر XFA بود. نویسنده فایل بومی خروجی را در بلوکهای 32 کیلوبایتی بافر میکند و بلوک ناقص نهایی را فقط در destructor اش flush میکرد، بعد از اینکه نویسنده سند از قبل موفقیت گزارش کرده بود. یک دیسک-پر یا خطای I/O روی آن بلوک آخر برای فراخواننده نامرئی بود. از v3.125.2 در runtime یعنی V8 و v3.125.3 در runtime معمولی، آن flush نهایی بخشی از نتیجه ذخیره است، و علامت ویرایش XFA فقط بعد از یک موفقیت واقعی پاک میشود. در سمت دلفی، مقدار TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean در فایلی موقت کنار هدف مینویسد و فقط وقتی ذخیره True برگرداند آن را سر جایش میبرد، پس یک ذخیره ناموفق فایل قبلی را سالم میگذارد
چرا یک فرم پویای XFA با صفحههای کمتر دوباره باز میشود؟
یک فرم پویای XFA وقتی با صفحههای کمتر باز میشود که subform ریشهاش restoreState="auto" اعلان نکرده باشد، و آن یک تصمیم نویسندگی فرم است نه نقص PDFium Component. در XFA 3.3، مقدار restoreState روی subform ریشه بهطور پیشفرض manual است. زیر manual، پردازنده XFA فقط state محدودی را از پاکت فرم ذخیرهشده برمیگرداند و بقیه را به اسکریپتهای نویسنده میسپارد. مقدار فیلدهای ذخیرهشده و شمارندههای instance مربوط به subform تکرارشونده همچنان برمیگردند، اما ویژگیهای هندسی که در زمان اجرا ست شدهاند نه
حالتی که این را لو داد فرمی سهصفحهای بود که اسکریپتش یک subform را به مقدار h="450pt" رشد میداد. پاکت فرم ذخیرهشده ارتفاع جدید و مقدارها و شمارندههای instance را داشت. اما موقع باز کردن دوباره، layout از ارتفاعهای template بازسازی میشد و فرم روی دو صفحه reflow میشد. runtime حق داشت: template هرگز بازسازی خودکار را درخواست نکرده بود. اعلانش روی subform ریشه باز شدن دوباره را فیکس میکند:
<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">
<!-- فیلدها؛ اسکریپتها ممکن است h را عوض کنند یا در زمان اجرا instance اضافه کنند -->
</subform>
</subform>
</template>
اگر مالک template نیستید، در viewer دورش پچ نزنید: فرمی که به حالت manual تکیه دارد انتظار دارد اسکریپتهای خودش state را بازسازی کنند. دوباره-صفحهبندی زنده موقع تایپ کاربر موضوع جداگانهای است، که در نحوه دنبال کردن تعداد صفحه و فیلدهای جابهجاشده dynamic XFA توسط PDFium Component پوشش داده شده
یک ذخیره XFA را در دلفی چطور تأیید کنید؟
تنها چک ذخیره XFA قابل-اعتماد، باز کردن فایل ذخیرهشده در یک instance تازه از TPdf و خواندن برگشتی داده ذخیرهشده است. مقدار TPdf.GetXfaDatasets پاکت datasets را همانطور که در سند ذخیره شده برمیگرداند نه مدل داده زنده XFA را، پس صداش قبل از ذخیره مقدارهای قدیمی را نشان میدهد. بعد از باز کردن دوباره، دقیقاً همان چیزی را نشان میدهد که نوشته شده. یک سند تک-stream پاکتهایی با نام جداگانه ندارد: PDFium کل XDP را بهعنوان یک پاکت با نام خالی گزارش میکند، پس GetXfaPacketByName('datasets') و GetXfaDatasets هیچ برنمیگردانند، و fallback کل stream را از طریق GetXfaFormPackets میخواند
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; // چیدمان آرایه-پاکت
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // تک-stream: یک پاکت بینام
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); // خروجی XDP ذخیرهشده UTF-8 است
finally
Pdf.Free;
end;
end;
روتین ذخیره بعد ویرایش معوق را commit میکند، نتیجه SaveAs را چک میکند و مقدار باز-بازشده را مقایسه میکند. مقدار TPdf.ClearFormFieldFocus فوکوس فرم را میکشد، که همان لحظهای است که PDFium بافر ویرایش فیلد فوکوسشده را commit میکند. مقدار TPdf.SetFocusedFormFieldText(const Value: WString): Boolean فیلد فوکوسشده را بهصورت برنامهنویسی پر میکند، اما به فوکوسای تکیه دارد که wrapper آن را از طریق FocusFormField دنبال میکند، که annotationهای widget را میپیماید. یک صفحه XFA پویا معمولاً هیچکدام را ندارد، پس آنجا متن معمولاً از طریق ورودی صفحهکلید در TPdfView میرسد، و تابع وقتی هیچ فیلد ردیابیشدهای فوکوس ندارد False برمیگرداند
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// پر کردن اسکریپتی اختیاری؛ False یعنی هیچ فیلد ردیابیشدهای فوکوس ندارد
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // بافر ویرایش را commit میکند
if not Pdf.SaveAs(FileName) then // شامل flush نهایی (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;
تست زیر-رشته را یک smoke test بگیرید. یک المان خالی ممکن است بهشکل <Tag/> سریال شود، ویژگیها میتوانند روی المانهای داده ظاهر شوند، و escape کردن فراتر از & و < یک انتخاب serializer است. برای چکهای production، XML باز-بازشده را با یک parser واقعی XML load کنید و گره متنی المان داده متصل را مقایسه کنید. چک را دو بار پشت سر هم هم اجرا کنید، چون نقص خط جدید فقط در نسل دوم شکل کاملش را نشان داد
مرجع سریع: چکلیست وفاداری ذخیره XFA
- مقدار
pdfium.v8.dllاز v3.125.2 یا بعدتر را برای فرمهای XFA deploy کنید، و v3.125.3 یا بعدتر را برایpdfium.dllمعمولی، تا فیکس نوشتن-نهایی در هر دو باشد - مقدار
LibraryNameرا به یک مسیر کامل اشاره بدهید وEnableV8Engineرا True کنید؛ مسیر مفقود بهجای load کردن کپی دیگری شکست میخورد - بعد از باز کردن سند مقدارهای
TPdf.XFAوTPdf.XfaRuntimeAvailableرا تأیید کنید - قبل از
SaveAsمقدارClearFormFieldFocusرا صدا بزنید تا فیلد فوکوسشده commit شود - هرگز نتیجه Boolean یعنی
SaveAsرا نادیده نگیرید؛ نتیجه False فایل قبلی را سر جایش میگذارد - با باز کردن در یک
TPdfجدید و خواندنGetXfaDatasetsتأیید کنید، با fallback بهGetXfaFormPacketsبرای XFA تک-stream - با مقدارهای خالی و فاصلههای ابتدایی و متن چند-خطی و
&و یک کاراکتر از صفحه مکمل تست کنید، در طول دو نسل ذخیره - برای DTDها و XMLDSig و کامنتها داخل پاکتهای زنده XFA تک-stream انتظار شکست ذخیره صریح را داشته باشید
- اگر یک فرم پویا هندسه زمان-اجرایش را موقع باز کردن دوباره از دست میدهد، قبل از مشکوک شدن به کتابخانه subform ریشه را برای
restoreState="auto"چک کنید
برای ساختار callback ای که runtime یعنی XFA از برنامه میزبان انتظار دارد، نسخه 2 مربوط به FPDF_FORMFILLINFO و ABI یعنی XFA در دلفی را ببینید. runtime یعنی V8 و wrapper مربوط به Delphi و C++Builder و کنترل viewer همگی بخشی از PDFium Component برای Delphi و C++Builder هستند، که هر دو runtime یعنی Windows را برای Win32 و Win64 شامل میشود