مقاله فنی

مقدارهای ارثی فیلدهای AcroForm و ریست آنها در Delphi

HotPDF Delphi Component مقدارهای /FT و /Ff و /V و /DV را روی یک فیلد AcroForm بارگذاری‌شده ویژگی‌های ارثی می‌گیرد که با پیمودن زنجیرهٔ /Parent resolve می‌شوند. از v2.754.3 و v2.754.4 به بعد، فرزند نام‌داری که نوعش از والدش می‌آید همچنان به‌صورت فردی قابل‌آدرس می‌ماند، RemoveFormField کار به خواهر و برادرهایش ندارد و ResetLoadedFormField پیش‌فرض ارثی را با همان نوع object اصلی PDF کپی می‌کند. پیش از آن، تعداد شگفت‌انگیزی از فرم‌های کاملاً معمولی غلط خوانده می‌شدند

فرمی که همهٔ این‌ها را لو می‌دهد هیچ اگزوتیکی نیست. یک ابزار authoring گره گروهی group می‌سازد که /FT /Ch و فلگ‌های فیلد و فهرست گزینه‌ها را یک بار حمل می‌کند، و زیرش دو فرزند نام‌دار a و b آویزان می‌کند، هر کدام یک dictionary ادغام‌شدهٔ فیلد-به‌علاوهٔ-ویجت با چیزی جز /T و /Parent و /Rect و /V خودش. این یک راه کاملاً قانونی برای به اشتراک گذاشتن ویژگی‌هاست و دقیقاً همان موردی است که بخش Limits در تنظیم مقدار فیلدهای فرم در یک PDF بارگذاری‌شده با Delphi به‌عنوان پوشش‌نداده علامت زده بود: سازگار کردن دکمه فقط به /FT محلی نگاه می‌کرد. این مقاله از همان‌جا که آن یکی ایستاد دوباره برمی‌دارد: درخت فیلد چطور دسته‌بندی می‌شود، مقدارهای ارثی چطور خوانده می‌شوند و ریست یک فیلد تکی مجاز است چه بنویسد

یک فیلد کدام درایه‌های AcroForm را می‌تواند از والدش به ارث ببرد؟

ISO 32000-1 §12.7.3.1، جدول 220، مقدارهای /FT و /Ff و /V و /DV را ارثی علامت می‌زند و جدول 229 در §12.7.4.3 همین کار را برای /MaxLen یک فیلد متنی می‌کند، پس هر readerی که فقط به dictionary محلی نگاه کند برای یک فرزند کاملاً معتبر، نوع غلط و فلگ‌های غلط و مقدار خالی گزارش می‌دهد. HotPDF همهٔ این خواندن‌ها را از یک resolver داخلی واحد عبور می‌دهد، یعنی HPDFLoadedInheritedFieldObject، که dictionary را برای کلید بررسی می‌کند، اگر ارجاع غیرمستقیمی پیدا شد resolve می‌کند و در غیر این صورت تا حداکثر 128 سطح دنبال /Parent می‌رود، چون فایل‌های بدفرم می‌توانند چرخه‌های /Parent بسازند که هیچ ربطی به /Kids ندارند. getterهای عمومی روی همین سوارند: GetFormFieldType و GetFormFieldValue و GetLoadedFormFieldFlags و IsFormFieldRequired و IsFormFieldNoExport و GetLoadedFormFieldMaxLength و GetLoadedFormFieldDefaultValue و هلپرهای گزینه یعنی GetLoadedFormFieldOptionCount و GetLoadedFormFieldOptions که آرایهٔ /Opt ذخیره‌شده روی والد را هم برمی‌چینند. یک قاعده در resolver است که راحت می‌شود غلطش فهمید: پیمایش در اولین dictionaryی که کلید را دارد متوقف می‌شود، حتی اگر مقدار آنجا یک رشتهٔ خالی باشد. یک /V () محلی یک override عمدی است که والد را ماسک می‌کند، نه شکافی که از بالاتر در درخت پر شود

نمودار ویژگی‌های ارثی AcroForm در HotPDF: گره گروهی /FT و /Ff و /Opt را یک بار حمل می‌کند در حالی که فرزندان نام‌دار group.a و group.b فقط /T و /Parent و /Rect و یک /V محلی دارند، و نشان می‌دهد HPDFLoadedInheritedFieldObject تا 128 سطح دنبال /Parent می‌رود، جایی که اولین dictionary حامل کلید برنده است و یک مقدار محلی خالی والد را ماسک می‌کند
HotPDF مقدارهای /FT و /Ff و /V و /DV و /Opt را از یک resolver والدپیما عبور می‌دهد، پس فرزند نام‌دار قابل‌آدرس می‌ماند در حالی که یک مقدار خالی محلی، عمداً هرچه گروه بالای سرش حمل می‌کند را override می‌کند
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // 'group' حامل /FT /Ch و /Ff 131078 و /Opt است؛ فرزند
    // 'group.b' فقط /T و /Parent و /Rect و /V خودش را دارد
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (بیت 18) + NoExport (بیت 3) + Required (بیت 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // همان /V محلی
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

چرا /FT محلی آزمون غلطی برای فیلد انتهایی است؟

چون یک والد می‌تواند نوع را تأمین کند و در عین حال صاحب فیلدهای فرزند نام‌دار باشد، پس حضور /FT هیچ چیزی دربارهٔ اینکه درخت فیلد کجا تمام می‌شود نمی‌گوید. پیمایش قدیمی هر گره‌ای که /FT خودش را داشت یا /Kids نداشت انتهایی اعلام می‌کرد. در فرم بالا، group هم /FT /Ch دارد هم /Kids، پس به‌عنوان یک فیلد به نام group با دو ویجت ثبت می‌شد و نام‌های کاملاً واجدشرایط group.a و group.b به‌سادگی محو می‌شدند. GetFormFieldCount مقدار 1 برمی‌گرداند، جستجو با نام فرزند شکست می‌خورد و SetFormFieldValue فقط می‌توانست روی والد مشترک بنویسد. آزمون جایگزین، یعنی HPDFLoadedFieldHasChildFields، به‌جای والد به kids نگاه می‌کند: یک kid وقتی فرزند فیلد است که یا /T خودش را داشته باشد، یا /Kids خودش را، یا اصلاً dictionaryی با /Subtype /Widget نباشد. فقط وقتی هیچ kidای واجد شرایط نباشد گره انتهایی است و kidsهایش به‌عنوان annotationهای ویجتش حساب می‌شوند

دو حالت مرزی که این قاعده را شکل دادند هر دو از dictionaryهای ادغام‌شده می‌آیند، که §12.7.3.1 وقتی یک فیلد یک ویجت دارد اجازه‌شان می‌دهد. یک dictionary ادغام‌شدهٔ نام‌دار /Subtype /Widget حمل می‌کند و باز هم یک فیلد فرزند است، پس subtype به‌تنهایی نمی‌تواند آن را به فهرست ویجت‌های ناشناس والد بفرستد؛ /T حرف آخر را می‌زند. برعکسش هم اتفاق می‌افتد: بعضی تولیدکننده‌ها /FT والد را روی هر ویجت ناشناس تکرار می‌کنند، پس /FT نمی‌تواند دلیل این باشد که یک ویجت فیلد تازه‌ای را شروع می‌کند. این دسته‌بندی بین cache روابط و FormFieldExists و RemoveFormField مشترک است و هر یک از این پیمایش‌ها حالا dictionaryهایی را که قبلاً دیده ثبت می‌کند و از 128 سطح می‌گذرد. یک فایل رگرسیون که گروهش خودش را دوبار فهرست می‌کند، یعنی /Kids [5 0 R 5 0 R 6 0 R 7 0 R]، همچنان دقیقاً دو فیلد گزارش می‌کند به‌جای آنکه تا ابد بازگشت کند یا همان گره را دوبار بشمارد

RemoveFormField چطور از حذف فیلدهای خواهر و برادر جلوگیری می‌کند؟

RemoveFormField حالا فقط فرزندی را حذف می‌کند که نامش را می‌بری، چون کشف و حذف سرانجام سر تعریف فیلد انتهایی به توافق رسیدند. این توافق بیشتر از ظاهرش مهم است. overload نام‌محور یک ایندکس را از طریق cache رابطه resolve می‌کند و بعد در یک پیمایش دوم روی /AcroForm /Fields فیلدهای انتهایی را می‌شمارد. وقتی cache اصلاح شد تا group.a و group.b را ببیند، یک پیمایش حذفِ اصلاح‌نشده همچنان group را یک فیلد انتهایی تکی می‌گرفت و ایندکس 0 والد را همراه با هر خواهر و برادری و همهٔ ویجت‌هایشان حذف می‌کرد. پیمایش حذف حالا از همان آزمون HPDFLoadedFieldHasChildFields و همان مجموعهٔ بازدیدشده‌ها استفاده می‌کند، فقط annotationهای ویجتِ فرزند حذف‌شده را جمع می‌کند، آن‌ها را از /Annots هر صفحه می‌زداید و والد را فقط وقتی حذف می‌کند که آرایهٔ /Kids اش در نهایت خالی شود. رگرسیون هر سه جایی را که یک اشتباه خودش را نشان می‌داد بررسی می‌کند: /Kids والد، /Annots صفحه، و مقدار و ظاهر خواهر و برادر زنده‌مانده، هم بعد از بازنویسی کامل و هم بعد از یک به‌روزرسانی افزایشی

نمودار بقای خواهر و برادر در RemoveFormField در HotPDF: پیمایش حذف از HPDFLoadedFieldHasChildFields و مجموعهٔ بازدیدشده‌های کشف دوباره استفاده می‌کند، فقط فرزند نام‌دار group.a را از /Fields مربوط به AcroForm و /Annots صفحه می‌زداید و والد مشترک را تا وقتی آرایهٔ /Kids اش هنوز group.b زنده‌مانده را دارد نگه می‌دارد
کشف و حذف سرانجام سر تعریف فیلد انتهایی به توافق رسیدند، پس حذف یک فرزند نام‌دار، مقدار و ظاهر خواهر و برادرش را بعد از بازنویسی کامل یا یک به‌روزرسانی افزایشی دست‌نخورده نگه می‌دارد
// حذف یک فرزند نام‌دار؛ خواهر و برادرش و والد مشترک زنده می‌مانند
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// نوع و فلگ‌ها و گزینه‌ها همچنان از طریق والد resolve می‌شوند
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

ResetLoadedFormField وقتی پیش‌فرض ارثی است چه می‌نویسد؟

ResetLoadedFormField یک /V محلی می‌نویسد که کپی تازهٔ /DV ارثی با همان نوع object است، و کل پیش‌فرض را قبل از دست زدن به فیلد اعتبارسنجی می‌کند. نوع object مهم است چون getterهای اسکالر همه چیز را به متن تخت می‌کنند. پیش‌فرض یک چک‌باکس یک name مثل /Yes است، پیش‌فرض یک list box چند-انتخابی آرایه‌ای از رشته‌هاست و یک پیش‌فرض متنی ممکن است رشتهٔ UTF-16 هگزادسیمال باشد؛ کپی کردن هر کدام از این‌ها از طریق GetLoadedFormFieldDefaultValue name را به رشته، آرایه را به رشتهٔ خالی و رشتهٔ hex را به ارقام تحت‌اللفظی‌اش تبدیل می‌کرد. پس reset بر اساس نوع ارثی شاخه می‌زند: فیلدهای متنی و انتخاب یک object رشتهٔ تازه می‌گیرند که فلگ IsHexadecimal را نگه می‌دارد، فیلدهای انتخاب با پیش‌فرض آرایه‌ای یک آرایهٔ تازه از رشته‌های تازه می‌گیرند و دکمه‌های غیر-pushbutton یک object name تازه. کپی کردن، به‌جای اشاره کردن به objectهای والد، عمدی است: یک /V که آرایهٔ /DV والد یا شمارهٔ object آن را شریک می‌شد، دفعهٔ بعد که کسی مقدار را ویرایش کند پیش‌فرض را عوض می‌کرد. یک پیش‌فرض با نوع غلط، یا یک آرایهٔ انتخاب که چیزی جز رشته داشته باشد، استثنا می‌دهد و /V و /I را دقیقاً مثل قبل رها می‌کند. دکمه‌های pushbutton که مقدار ندارند (جدول 226، بیت 17) و فیلدهای امضا به مسیر قدیمی فقط-رشته برمی‌گردند

نمودار reset نوع‌دار در HotPDF: ResetLoadedFormField بر اساس نوع object ارثی /DV شاخه می‌زند، برای چک‌باکس یک object name تازه می‌نویسد، برای انتخاب چند-انتخابی یک آرایهٔ تازه از رشته‌های تازه، برای متن hex رشته‌ای که IsHexadecimal را نگه می‌دارد، وقتی /DV وجود ندارد یک رشتهٔ خالی یا /Off، و در ناهمخوانی نوع بدون دست زدن به /V یا /I استثنا می‌دهد
کپی کردن به‌جای اشاره کردن به objectهای والد باعث می‌شود یک ویرایش مقدار بعدی پیش‌فرض را بی‌سروصدا عوض نکند، و دکمه‌های pushbutton و فیلدهای امضا به مسیر قدیمی فقط-رشته برمی‌گردند

وقتی هیچ /DV ای در هیچ‌جای زنجیره وجود ندارد، متد قرارداد پاک کردنش را با نوشتن یک رشتهٔ خالی محلی، یا /Off برای چک‌باکس یا رادیو، نگه می‌دارد. حذف کردن /V محلی تمیزتر به نظر می‌رسد و غلط است: والد ممکن است مقدار جاری داشته باشد و برداشتن override فرزند بی‌سروصدا آن مقدار را برمی‌گرداند. همین دلیلش است که ریست یک فیلد تکی، اکشن ResetForm در §12.7.5.3 نیست که viewer هنگام کلیک کاربر روی دکمه‌ای روی مجموعه‌ای از فیلدها اجرایش می‌کند، همان‌طور که در ساخت فیلدهای AcroForm و اکشن‌ها با HotPDF توضیح داده شد. ResetLoadedFormField یک عملیات ویرایش روی یک فیلد بارگذاری‌شده است، با قاعدهٔ خودش برای حالت بدون پیش‌فرض، و فیلد را از طریق NoteLoadedFormFieldDirty ثبت می‌کند تا محاسبهٔ مجدد افزایشی تغییر را ببیند

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // والد روی یک list box با MultiSelect دارای /DV [(b) (r)] است: group.a
    // /V [(b) (r)] خودش و یک /I تازهٔ [0 2] می‌گیرد؛ والد دست‌نخورده می‌ماند
    Pdf.ResetLoadedFormField(Field.Index);
    // getterهای اسکالر نمی‌توانند پیش‌فرض آرایه‌ای را بازنمایی کنند
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // خالی
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

هم‌قدم نگه داشتن /V و /I و /AS

یک reset فقط وقتی درست است که شاخص انتخاب و وضعیت ظاهر دنبال مقدار بیایند، پس ResetLoadedFormField با همان دو سازگارکنندهٔ SetFormFieldValue تمام می‌شود. HPDFReconcileChoiceSelection حالا مقدار آرایه‌ای را می‌پذیرد: /I محلی را بدون تغییر دادنش حذف می‌کند، هر مقدار را با نیمهٔ export هر درایهٔ /Opt تطبیق می‌دهد و یک /I مرتب تازه می‌نویسد، پس reset به [(b) (r)] در برابر گزینه‌های b و g و r نتیجه‌اش /I [0 2] می‌شود. ReconcileLoadedButtonAppearanceStates حالا نوع ارثی را می‌خواهد، پس چک‌باکسی که /FT /Btn اش روی والد است بالاخره /AS اش ست می‌شود. در سمت نوشتن، SetFormFieldValue و SetLoadedFormFieldDefaultValue حتی وقتی فرزند درایهٔ محلی برای کپی گرفتن نوع ندارد، برای یک دکمهٔ غیر-pushbutton ارثی یک object name ذخیره می‌کنند. و وقتی EnsureLoadedFieldAppearanceStream ظاهرهای دکمه را دوباره می‌سازد، مگر اینکه مقدار با حالت روشن بخواند /AS /Off می‌نویسد و به هر state stream یک /Type /XObject و /Subtype /Form و /BBox درست می‌دهد؛ پیش از v2.754.4، بازتولید ظاهر بعد از یک reset می‌توانست چک‌باکس را دوباره قبل از ذخیرهٔ فایل تیک بزند

محدودیت‌هایی که قبل از ساختن روی این پایه باید بدانی

getterهای اسکالر اسکالر می‌مانند. GetFormFieldValue و GetLoadedFormFieldDefaultValue برای یک مقدار آرایه‌ای رشتهٔ خالی برمی‌گردانند، اعداد و بولی‌ها را به 42 یا true رشته می‌کنند و یک رشتهٔ hex-کدشده را با املای هگزادسیمالش گزارش می‌کنند. یک چرخهٔ /Parent پیمایش را بدون استثنا تمام می‌کند، پس فیلدی که نوعش در یک چرخه گم شده lfftUnknown و فلگ‌های 0 گزارش می‌کند نه اینکه شکست بخورد. SetFormFieldValue و ResetLoadedFormField همیشه فرزندی را می‌نویسند که آدرسش داده‌ای و هرگز مقداری را به والد مشترک ارتقا نمی‌دهند، که برای فرزندان مستقل درست است اما یعنی گروه‌های رادیو باید از طریق فیلدی خطاب شوند که انتخاب را مالک است. و هر فراخوانی یک فیلد را به‌تنهایی ثبت می‌کند؛ هیچ‌چیز در اینجا یک دستهٔ resetها را تراکنشی نمی‌کند

resolve کردن ویژگی‌های ارثی، دسته‌بندی یکپارچهٔ درخت فیلد و reset نوع‌داری که اینجا توصیف شد بخشی از API فرم بارگذاری‌شده در HotPDF Delphi Component برای Delphi و C++Builder است، در کنار ساخت فیلدهایی که در افزودن فیلدهای AcroForm به یک PDF بارگذاری‌شده در Delphi پوشش داده شده