يعامل 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 () محلية هي تجاوز متعمد يحجب الأم، لا فجوة تُملأ من أعلى الشجرة
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 للصفحة، وقيمة الشقيق الناجي ومظهره، بعد إعادة كتابة كاملة وبعد تحديث تزايدي كليهما
// إزالة ابن مسمى واحد؛ ينجو شقيقه والأم المشتركة
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) وحقول التواقيع تعود إلى المسار الأقدم القائم على السلاسل وحدها
حين لا /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