مقال تقني

قيم حقول AcroForm الموروثة وإعادات ضبطها في Delphi

يعامل HotPDF Delphi Component ‏/FT و /Ff و /V و /DV على حقل AcroForm محمّل بوصفها خصائص قابلة للوراثة، تُحل بتجول سلسلة /Parent. ومنذ v2.754.3 و v2.754.4 يبقى الابن المسمى الذي يأتي نوعه من أمه قابلاً للعنونة فردياً، و RemoveFormField تترك أشقاءه وشأنهم، و ResetLoadedFormField تنسخ الافتراضي الموروث بنوع كائن PDF الأصلي. وقبل ذلك كان عدد مفاجئ من النماذج العادية يُقرأ قراءة خاطئة

النموذج الذي يفضح كل هذا ليس غريباً. أداة تأليف تبني عقدة مجموعة group تحمل /FT /Ch وأعلام الحقل وقائمة الخيارات مرة واحدة، وتعلق تحتها ابنين مسميين a و b، كل منهما قاموس حقل وودجة مدموج لا يحمل سوى /T و /Parent و /Rect و /V خاصته. تلك طريقة مشروعة تماماً لمشاركة الخصائص، وهي بالضبط الحالة التي وسمها قسم الحدود في ضبط قيم حقول النماذج في PDF محمّل باستخدام Delphi بوصفها غير معالجة: موازنة الأزرار كانت تنظر إلى /FT المحلي وحده. هذه المقالة تلتقط من حيث توقفت تلك، مغطية كيف تصنف شجرة الحقول، وكيف تقرأ القيم الموروثة، وما يُسمح لإعادة ضبط حقل واحد أن يكتبه

أي مدخلات AcroForm يمكن لحقل أن يورثها عن أمه؟

توسم §12.7.3.1 من ISO 32000-1 (الجدول 220) ‏/FT و /Ff و /V و /DV بوصفها قابلة للوراثة، ويفعل الجدول 229 في §12.7.4.3 الشيء نفسه مع /MaxLen لحقل نصي، فأي قارئ ينظر إلى القاموس المحلي وحده سيبلّغ عن نوع خطأ وأعلام خاطئة وقيمة فارغة لابن سليم تماماً. تمرر HotPDF كل تلك القراءات عبر محلل داخلي واحد، HPDFLoadedInheritedFieldObject، يفحص القاموس بحثاً عن المفتاح، ويحل مرجعاً غير مباشر إن وجد واحداً، وإلا يتبع /Parent بحد أقصى 128 مستوى، لأن الملفات المشوهة يمكن أن تبني دورات /Parent لا علاقة لها بـ /Kids. والجالبات العامة تجلس فوقه: GetFormFieldType و GetFormFieldValue و GetLoadedFormFieldFlags و IsFormFieldRequired و IsFormFieldNoExport و GetLoadedFormFieldMaxLength و GetLoadedFormFieldDefaultValue ومساعدتا الخيارات GetLoadedFormFieldOptionCount و GetLoadedFormFieldOptions، اللتان تلتقطان أيضاً مصفوفة /Opt مخزنة على الأم. وقاعدة في المحلل سهلة الخطأ: يتوقف التجول عند أول قاموس يحوي المفتاح، حتى لو كانت القيمة هناك سلسلة فارغة. ‏/V () محلية هي تجاوز متعمد يحجب الأم، لا فجوة تُملأ من أعلى الشجرة

مخطط خصائص AcroForm الموروثة في HotPDF: عقدة مجموعة تحمل /FT و /Ff و /Opt مرة واحدة بينما يحمل الابنان المسميان group.a و group.b فقط /T و /Parent و /Rect و /V محلية، مبيناً HPDFLoadedInheritedFieldObject تجول على /Parent حتى 128 مستوى حيث يفوز أول قاموس يحوي المفتاح وتحجب القيمة المحلية الفارغة الأم
تحل HotPDF ‏/FT و /Ff و /V و /DV و /Opt عبر محلل واحد يتجول على الأمهات، فيبقى الابن المسمى قابلاً للعنونة بينما تتجاوز القيمة المحلية الفارغة عمداً كل ما تحمله المجموعة فوقها
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 ينظر إلى الأبناء بدل الأم: الابن هو حقل ابن إذا امتلك /T خاصة، أو امتلك /Kids خاصة، أو لم يكن قاموس /Subtype /Widget أصلاً. وفقط حين لا يكفل أي ابن تكون العقدة طرفية، بأبنائها معاملين ودجاتها التوضيحية

الحالتان الحديتان اللتان صاغتا تلك القاعدة كلتاهما من القواميس المدموجة، التي يجيزها §12.7.3.1 حين يكون للحقل ودجة واحدة. القاموس المدموج المسمى يحمل /Subtype /Widget وما زال حقل ابن، فالنوع الفرعي وحده لا يستطيع إرساله إلى قائمة الودجات المجهولة للأم؛ يفوز /T. ويحدث العكس أيضاً: بعض المنتجين يكررون /FT للأم على كل ودجة مجهولة، فلا يمكن استخدام /FT دليلاً على أن ودجة تبدأ حقلاً جديداً. التصنيف مشترك بين ذاكرة العلاقات و FormFieldExists و RemoveFormField، وكل تجول منها يسجل الآن القواميس التي زارها ويتجاوز 128 مستوى. ملف انحدار مجموعته تسرد نفسها مرتين، /Kids [5 0 R 5 0 R 6 0 R 7 0 R]، ما زال يبلّغ عن حقلين بالضبط بدل التكرار إلى ما لا نهاية أو عدّ العقدة نفسها مرتين

كيف يتجنب RemoveFormField حذف الحقول الشقيقة؟

تحذف RemoveFormField الآن الابن الذي تسميه وحده، لأن الاكتشاف والحذف اتفقا أخيراً على ماهية الحقل الطرفي. وذلك الاتفاق أهم مما يبدو. الحمل الزائد بالاسم يحل فهرساً عبر ذاكرة العلاقات ثم يعد الحقول الطرفية في تجوال ثانٍ فوق /AcroForm /Fields. وبعد إصلاح الذاكرة لترى group.a و group.b، كان تجوال الحذف غير المصلح سيعامل group بوصفها حقلاً طرفياً واحداً، وكان الفهرس 0 سيزيل الأم مع كل شقيق وكل ودجاتها. يستخدم تجوال الحذف الآن اختبار HPDFLoadedFieldHasChildFields نفسه والمجموعة المزارة نفسها، ويجمع الودجات التوضيحية للابن المحذوف وحده، ويقتطعها من /Annots لكل صفحة، ولا يزيل الأم إلا حين تنتهي مصفوفة /Kids فيه فارغة. ويفحص الانحدار المواضع الثلاثة كلها التي سيظهر فيها خطأ: /Kids للأم، و /Annots للصفحة، وقيمة الشقيق الناجي ومظهره، بعد إعادة كتابة كاملة وبعد تحديث تزايدي كليهما

مخطط نجو الشقيق في HotPDF RemoveFormField: يعيد تجوال الحذف استخدام HPDFLoadedFieldHasChildFields والمجموعة المزارة من الاكتشاف، ويقتطع الابن المسمى group.a وحده من AcroForm /Fields ومن /Annots للصفحة، ويبقي الأم المشتركة ما دامت مصفوفة /Kids فيه تحمل group.b الناجي
يستقر الاكتشاف والحذف أخيراً على تعريف واحد للحقل الطرفي، فإزالة ابن مسمى واحد تترك قيمة شقيقه ومظهره سليمين بعد إعادة كتابة كاملة أو تحديث تزايدي
// إزالة ابن مسمى واحد؛ ينجو شقيقه والأم المشتركة
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// النوع والأعلام والخيارات ما زالت تُحل عبر الأم
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

ماذا يكتب ResetLoadedFormField حين يكون الافتراضي موروثاً؟

تكتب ResetLoadedFormField ‏/V محلية نسخة جديدة من /DV الموروث بنوع كائن PDF نفسه، وتتحقق من الافتراضي كله قبل لمس الحقل. نوع الكائن مهم لأن الجالبات العددية تسطّح كل شيء إلى نص. افتراضي مربع اختيار اسم مثل /Yes، وافتراضي صندوق قائمة متعدد التحديد مصفوفة سلاسل، وافتراضي نصي قد يكون سلسلة UTF-16 سداسية؛ نسخ أي منها عبر GetLoadedFormFieldDefaultValue سيحول الاسم إلى سلسلة والمصفوفة إلى سلسلة فارغة والسلسلة السداسية إلى أرقامها الحرفية. لذا تتفرع إعادة الضبط على النوع الموروث: حقول النص والاختيار تحصل على كائن سلسلة جديد يحفظ علم IsHexadecimal، وحقول الاختيار بافتراض مصفوفي تحصل على مصفوفة جديدة من سلاسل جديدة، والأزرار غير الدفعية تحصل على كائن اسم جديد. والنسخ، بدل الإشارة إلى كائنات الأم، مقصود: ‏/V يتشارك مصفوفة /DV للأم أو رقم كائنها سيغير الافتراضي في المرة القادمة التي يحرر فيها أحد القيمة. وافتراض من النوع الخطأ، أو مصفوفة اختيار تحوي ما ليس سلاسل، يرفع استثناء ويترك /V و /I كما كانا تماماً. وأزرار الدفع التي لا قيمة لها (الجدول 226، بت 17) وحقول التواقيع تعود إلى المسار الأقدم القائم على السلاسل وحدها

مخطط إعادة الضبط المكتوبة النوع في HotPDF: تتفرع ResetLoadedFormField على نوع كائن /DV الموروث، فتكتب كائن اسم جديداً لمربع اختيار، ومصفوفة جديدة من سلاسل جديدة لاختيار متعدد التحديد، وسلسلة تحفظ IsHexadecimal لنص سداسي، وسلسلة فارغة أو /Off حين لا /DV موجود، وترفع استثناء دون لمس /V أو /I عند عدم تطابق النوع
النسخ بدل الإشارة إلى كائنات الأم يمنع تحريراً لاحقاً للقيمة من تغيير الافتراضي بصمت، وأزرار الدفع وحقول التواقيع تعود إلى المسار الأقدم القائم على السلاسل وحدها

حين لا /DV موجود في أي مكان أعلى السلسلة، تحافظ الطريقة على عقد الإزالة بكتابة سلسلة فارغة محلية، أو /Off لمربع اختيار أو حقل راديو. حذف /V المحلي سيبدو أنظف وهو خطأ: قد تحمل الأم قيمة حالية، وإزالة تجاوز الابن ستعيد تلك القيمة بصمت. وهذا أيضاً سبب كون إعادة ضبط حقل واحد ليست إجراء ResetForm في §12.7.5.3، الذي يشغله العارض على مجموعة حقول حين ينقر المستخدم زراً، كما في بناء حقول AcroForm وإجراءاتها مع HotPDF. ‏ResetLoadedFormField عملية تحرير على حقل محمّل واحد، بقاعدتها الخاصة لحالة لا افتراضي، وتسجل الحقل عبر NoteLoadedFormFieldDirty حتى يرى إعادة الحساب التزايدي التغيير

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // الأم تحمل /DV [(b) (r)] على صندوق قائمة MultiSelect: يحصل group.a
    // على /V [(b) (r)] خاصته و /I [0 2] جديدة؛ والأم بلا مساس
    Pdf.ResetLoadedFormField(Field.Index);
    // الجالبات العددية لا تستطيع تمثيل الافتراضي المصفوفي
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // فارغ
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

إبقاء /V و /I و /AS متوافقة

إعادة الضبط صحيحة فقط إذا تبع فهرس التحديد وحالة المظهر القيمة، فتختم ResetLoadedFormField بالموازنَين نفسيهما في SetFormFieldValue. تقبل HPDFReconcileChoiceSelection الآن قيمة مصفوفية: تحذف /I المحلية دون تعديلها، وتطابق كل قيمة على نصف التصدير لكل مدخل /Opt، وتكتب /I مرتبة جديدة واحدة، فإعادة الضبط إلى [(b) (r)] مقابل خيارات b و g و r تعطي /I [0 2]. وتطلب ReconcileLoadedButtonAppearanceStates الآن النوع الموروث، فمربع اختيار ابن تسكنه /FT /Btn على الأم يحصل أخيراً على /AS مضبوطة. وعلى جانب الكتابة تخزن SetFormFieldValue و SetLoadedFormFieldDefaultValue كائن اسم لزر غير دفعي موروث حتى حين لا يحمل الابن مدخلاً محلياً يُنسخ منه النوع. وحين تعيد EnsureLoadedFieldAppearanceStream بناء مظاهر الأزرار تكتب /AS /Off ما لم تطابق القيمة حالة التشغيل، وتمنح كل تدفق حالة /Type /XObject و /Subtype /Form و /BBox صحيحة؛ وقبل v2.754.4 كان تجديد المظهر بعد إعادة ضبط قد يعيد تشغيل المربع قبل حفظ الملف

حدود تستحق المعرفة قبل أن تبني على هذا

تبقى الجالبات العددية عدداً. يعيد GetFormFieldValue و GetLoadedFormFieldDefaultValue سلسلة فارغة لقيمة مصفوفية، ويحولان الأعداد والقيم المنطقية إلى 42 أو true، ويبلّغان عن سلسلة مشفرة سداسياً بتهجئتها السداسية. دورة /Parent تنهي التجوال دون استثناء، فحقل ضاع نوعه في دورة يبلّغ lfftUnknown وأعلام 0 بدل الفشل. و SetFormFieldValue و ResetLoadedFormField تكتبان دائماً الابن الذي تخاطبه ولا تروّجان قيمة أبداً إلى الأم المشتركة، وهو الصواب للأبناء المستقلين لكنه يعني أن مجموعات الراديو تخاطب عبر الحقل الذي يملك التحديد. وكل استدعاء يوثق حقلاً واحداً على حدة؛ لا شيء هنا يجعل دفعة إعادات ضبط عملية ذرية

حل الخصائص الموروثة والتصنيف الموحد لشجرة الحقول وإعادة الضبط المكتوبة النوع الموصوفة هنا جزء من واجهة النماذج المحمّلة في HotPDF Delphi Component لـ Delphi و C++Builder، إلى جانب إنشاء الحقول المغطى في إضافة حقول AcroForm إلى PDF محمّل في Delphi