مقال تقني

بناء حقول وإجراءات AcroForm باستخدام HotPDF في Delphi

إجراء AcroForm عبارة عن قاموس مرفق بأداة (widget) تخبر العارض بما يجب فعله عند حدوث شيء ما لتلك الأداة. انقر فوق زر ويقرأ العارض قاموس الإجراءات الخاص به: يفتح إجراء URI عنوان ويب، ويدير إجراء JavaScript برنامجاً نصياً (script)، ويرسل إجراء SubmitForm قيم الحقول المجمعة إلى نقطة نهاية (endpoint)، ويمسح إجراء ResetForm القيم ويعيدها إلى الإعدادات الافتراضية. الإجراء عبارة عن بيانات، وليس سلوكاً مخبوزاً في الملف. يحدد ISO 32000-1 §12.6 شكل القاموس؛ ويوفر العارض المحرك الذي يفسره. هذا الانقسام مهم لأن الإجراء المكتوب بشكل مثالي في ملف PDF لا يزال لا يفعل شيئاً إذا لم يكن لدى القارئ على الطرف الآخر محرك له، والكثير من الحزن المتعلق بـ AcroForm يعود إلى تلك الفجوة وليس إلى حقل مشوه

يكتب HotPDF تلك القواميس مباشرة من Delphi و C++Builder، جنباً إلى جنب مع أدوات الحقول التي تتدلى منها. هناك هيكلان قيد اللعب لكل نموذج تفاعلي: الأداة التي يراها المستخدم على الصفحة، والحقل بالإضافة إلى آلية الإجراء تحتها التي تحمل البيانات والأسلاك. يتم تحريرها بشكل مستقل، ويمكن أن يكون أي منهما خاطئاً بينما يبدو الآخر على ما يرام. تعمل الأقسام أدناه من خلال تسمية الحقول، وإجراءات الأزرار نفسها، و JavaScript على مستوى الحقل، وفئة العيب الذي ينجو من الفحص البصري لأنه يعيش بالكامل في الهيكل الثاني

أسماء الحقول هي مفاتيح توجيه، وليست تسميات توضيحية

يحمل كل حقل AcroForm اسماً مؤهلاً بالكامل. يجعل ISO 32000-1 §12.7.3 هذا الاسم، وليس التسمية التوضيحية المرئية، المفتاح الذي تنتقل تحته قيمة الحقل عند تصدير النموذج أو إرساله. يميل المطورون القادمون من تصميم VCL إلى التعامل مع اسم عنصر التحكم كمعرف كود خاص، وهو ليس كذلك هنا. إنه تنسيق السلك (wire format)

الشيء الأول الذي يترتب على ذلك هو أن حقلين لهما نفس الاسم المؤهل بالكامل ليسا حقلين. يعاملهما PDF كتعليقين توضيحيين (annotations) لأداة واحدة لحقل واحد، يشتركان في قيمة واحدة، لذا فإن الكتابة في أحدهما تحدث الآخر على الفور. هذا هو بالضبط ما تريده عندما يجب أن يتكرر اسم العميل في كل صفحة من صفحات العقد. إنه خطأ (bug) عندما تعيد حلقة الإنشاء استخدام 'Field1' عبر ثلاث صفحات عن طريق الخطأ. لا يوجد فحص بصري يلتقط الحالة الثانية. لا تزال كل صفحة ترسم المربع الخاص بها، ولا يظهر الارتباط إلا بمجرد أن يبدأ شخص ما في الكتابة

تبني الأسماء المنقطة مثل applicant.email تسلسلاً هرمياً. تجمع العقدة الأصلية applicant أبناءها، وهو ما يتيح لإعادة التعيين أو الإرسال استهداف جزء فقط من النموذج. تسمية الحقول بهذه الطريقة منذ البداية لا تكلف شيئاً، وتؤتي ثمارها في المرة الأولى التي يطلب فيها النظام المتلقي كتلة مقدم الطلب (applicant block) فقط

أزرار الاختيار (Radio buttons) لها قاعدة خاصة بها. يجب أن تشترك الأزرار التي يجب تبديلها معاً في اسم مجموعة. في HotPDF، استدعاءات AddRadioButton التي تمرر نفس اسم المجموعة تربط أدواتها بحقل أصل واحد، وتحدد قيمة التصدير لكل زر ('basic' أو 'full') الخيار المختار. أعط كل زر اسماً مميزاً وستحصل على صف من مفاتيح التشغيل/الإيقاف المستقلة بدلاً من مجموعة واحدة يستبعد كل منها الآخر (mutually exclusive)، والتي تُعرض بشكل متطابق وتتصرف بشكل خاطئ

إنشاء مجموعة الحقول صفحة بصفحة

يضع HotPDF الحقول من خلال طرق THPDFPage، لذا فإن كل حقل ينتمي إلى كائن الصفحة الذي قام بإنشائه. فخ التسلسل الذي يجب الانتباه إليه هو AddPage. إنه يعيد توجيه CurrentPage إلى الصفحة الجديدة في اللحظة التي يعود فيها، لذلك فإن أي استدعاء للحقل بعد ذلك يهبط في الصفحة الجديدة حتى عندما يكون الحقل منطقياً ينتمي إلى الصفحة التي تركتها للتو. قم بإنهاء كل صفحة، المحتوى المرسوم والحقول معاً، قبل استدعاء AddPage

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Page 1: applicant block
  Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 700, 300, 722));
  Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 660, 300, 682));
  Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 620, 70, 640), False);
  Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 580, 70, 600), True);
  Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 580, 110, 600), False);
  Pdf.CurrentPage.AddComboBox('plan', 'Standard',
    ['Basic', 'Standard', 'Premium'], Rect(50, 540, 200, 565));

  Pdf.AddPage;  // CurrentPage now points at page 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;

تستخدم الإحداثيات اتفاقية PDF، مع كون الأصل في الزاوية اليسرى السفلية من الصفحة. هذا هو نفس الأصل الذي يستخدمه TextOut للنص المرسوم، لذا فإن Rect(50, 100, 200, 120) يجلس بالقرب من الجزء السفلي من صفحة Letter، وليس الجزء العلوي. يضع VCL حرف Y في الأعلى وينمو للأسفل، لذا فإن جدول التخطيط (layout table) المنقول مباشرة يخرج معكوساً عمودياً، ويتم قلب كل حقل إلى الطرف الخاطئ من الصفحة. قم بإجراء التحويل مرة واحدة في مساعد مشترك (shared helper) بدلاً من كل موقع استدعاء، ويصلح تصحيح واحد النموذج بأكمله

ربط الأزرار بـ URI و JavaScript وإجراءات الإرسال

زر الضغط (push button) يكون خاملاً حتى يتم إرفاق إجراء به. يبرز HotPDF أنواع الإجراءات من ISO 32000-1 §12.6.4 من خلال تعداد THPDFButtonAction (الذي يحتوي على baURI، baJavaScript، baSubmitURL، baResetForm، baHide، baShow، baNamed)، ويوفر طريقتين تنشئان الزر وتربطان الإجراء الخاص به في استدعاء واحد

// Open a help page in the system browser
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);

// Run viewer-side JavaScript
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);

// Submit as XFDF and keep empty fields in the payload
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 620, 420, 650),
  [sffXFDF, sffIncludeNoValueFields]);

تستحق علامات الإرسال (submit flags) تفكيراً أكثر مما تحصل عليه عادة. يأخذ AddPushButtonWithSubmitAction مجموعة THPDFSubmitFormFlags، وتنتج المجموعة الفارغة إرسال (post) عادياً بترميز url-encoded، وهو التنسيق الذي تقبله العديد من نقاط النهاية (endpoints) النموذجية وترفضه العديد من نقاط نهاية الإنتاج. إضافة sffXFDF تبدل الحمولة (payload) إلى XFDF. يغير sffGetMethod فعل HTTP. تحتفظ sffIncludeNoValueFields بالحقول الفارغة في الحمولة بدلاً من إسقاطها بصمت، وهو ما يهم في اللحظة التي يميز فيها المستهلك "غائب" (absent) عن "فارغ" (blank). مجموعة العلامات هي جزء من عقد الواجهة (interface contract) الخاص بك مع نقطة النهاية المتلقية، لذا قم بتسويتها مع الفريق الذي يحلل الإرسال، وليس بعد الدفعة الأولى المرفوضة

JavaScript على مستوى الحقل: ضغطة المفتاح، والتنسيق، والتحقق

النقرات على الأزرار ليست المكان الوحيد الذي تعيش فيه الإجراءات. يرفق HotPDF أيضاً JavaScript بالأحداث الخاصة بكل حقل والتي تطلقها العارضات القادرة على تشغيل البرامج النصية (script-capable viewers) أثناء إدخال المستخدم للبيانات. هناك ثلاثة مشغلات (triggers)، وتطلق في نقاط مختلفة من دورة حياة الإدخال. يعمل إجراء ضغطة المفتاح (keystroke action) مع وصول كل حرف، ومرة أخرى عند التنفيذ (commit). يعيد إجراء التنسيق (format action) كتابة القيمة المعروضة بعد تنفيذ تغيير، للعرض البحت. يحصل إجراء التحقق (validate action) على الكلمة الأخيرة، ويقبل أو يرفض القيمة المنفذة قبل أن تصبح قيمة الحقل

// Reject committed values that are not plausible email addresses
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Display US phone numbers as (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Refuse applicants under 18 at commit time
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');

تعيين event.rc = false داخل برنامج نصي لضغطة المفتاح أو التحقق يخبر العارض برفض الإدخال. المشكلة هي أن أياً من هذا لا يعمل ما لم يقم العارض بشحن محرك JavaScript. يمتلك Acrobat وعدد قليل من منتجات سطح المكتب واحداً. لا تمتلك معظم القارئات المحمولة، والعارضات المضمنة في المتصفح، وخطوط أنابيب الطباعة واحداً، وتقوم بإسقاط البرامج النصية على الأرض دون شكوى. لذا فإن البرامج النصية للحقول تعمل على تحسين جودة البيانات للمجموعة الفرعية من المستخدمين الذين يقوم القارئ الخاص بهم بتشغيلها، وهذا كل ما يفعلونه. إنها ليست حدوداً أمنية. لا يزال يتعين التحقق من كل قيمة مقدمة على الخادم بمجرد وصولها، لأنك لا تستطيع أن تفترض أن العميل قد تحقق من أي شيء

العيوب التي تتجاوز المراجعة البصرية

أصعب عيوب AcroForm في التقاطها هي تلك التي تعيش في بنية البيانات بدلاً من العرض (rendering)، لأن فتح الملف والنظر إليه لا يخبرك بأي شيء. يظهر أربعة منها بشكل متكرر بما يكفي ليكونوا جديرين بالتسمية، ولكل منهم اختبار ميكانيكي يجده قبل الإصدار

  • انحراف قيمة التصدير (Export value drift). مربع اختيار تم إنشاؤه كـ AddCheckBox('consent', 'Yes', ...) يرسل Yes. المستهلك الذي يطابق على Y يرفض كل تقديم بينما تبدو الصفحة مثالية. املأ النموذج، وصدره كـ XFDF من Acrobat، وقارن القيم (diff) مقابل المخطط (schema) الذي يتوقعه المستهلك فعلياً
  • عكس القيمة العرضي (Accidental value mirroring). يندمج حقلان يشتركان في اسم مؤهل بالكامل في حقل واحد. يظهر العرض في وقت إدخال البيانات وليس في وقت الإنشاء، لذا فإن الاختبار هو الكتابة في النموذج، وليس عرضه والنظر إلى النتيجة بالعين
  • قيم مجمعة (Combo values) خارج قائمة الخيارات. عندما لا تكون القيمة الحالية الممررة إلى AddComboBox إحدى الخيارات المدرجة، تختلف العارضات حول ما إذا كان يجب إظهارها، أو إفراغها، أو وضع علامة عليها. احتفظ بالافتراضي داخل القائمة وسيختفي الاختلاف
  • الحقول لا تزال قابلة للتحرير بعد إغلاق سير العمل. لا يحتوي HotPDF على استدعاء لتسوية المظهر (appearance-flattening) لحقول AcroForm. الطريقة المدعومة لتجميد نموذج مكتمل هي إنشاء الحقول بعلامة ffReadOnly، مما يبقي القيمة مرئية من خلال تيار المظهر الخاص بالحقل (appearance stream) بينما يرفض التعديلات. يبقى الحقل كائن نموذج حي، وهو ما تتوقع أدوات التجميع والتوقيع اللاحقة (downstream) العثور عليه

يستحق سلوك واحد من جانب العارض ملاحظة تراجع (regression note) على الرغم من عدم وجود تغيير في التعليمات البرمجية يعالجه. يمكن لعمليات نشر Acrobat للمؤسسات تعطيل JavaScript أو تقييد أهداف الإرسال حسب السياسة، لذا فإن الإجراء الذي نجح من خلال كل بناء تطوير يمكن أن يجلس ميتاً على سطح مكتب عميل مقفل. خطط لآلية احتياطية (fallback) مرئية للحالة التي لا يفعل فيها الزر شيئاً، حتى لو كانت هذه الآلية الاحتياطية مجرد تعليمات مطبوعة تخبر المستخدم بما يجب عليه فعله بدلاً من ذلك

أين يتصل عمل النموذج ببقية المستند

حقل التوقيع هو بحد ذاته نوع حقل AcroForm. النموذج الذي سيتم اعتماده أو التوقيع عليه لاحقاً من الأفضل أن يحتفظ بهذا الحقل أثناء الإنشاء بدلاً من ترقيعه فيه لاحقاً، وأسباب مستوى البايت (byte-level) لذلك موجودة في المقال المرافق حول التوقيعات الرقمية وتوقيع PAdES باستخدام HotPDF. المدخلات التي تصل كحزم XFA بدلاً من AcroForm الأصلية تمثل حالة مختلفة: تسوية (flattening) XFA إلى حقول AcroForm هي سير عملها الخاص مع نموذج الخسارة الخاص بها، لأن تقنيتي النموذج لا يمكن أن تتعايشا في ملف واحد

تعد طرق الحقل والإجراء والمشغل الموضحة هنا جزءاً من واجهة برمجة تطبيقات مكون HotPDF القياسي لـ Delphi و C++Builder؛ تربط صفحة المنتج المرجع الكامل، بما في ذلك الأحمال الزائدة لعلامة الحقل (field-flag overloads) وتعداد علامة الإرسال (submit-flag) الكامل