مقال تقني

نماذج PDF التفاعلية في دلفي: الإجراءات وجافا سكريبت

حقل نموذج PDF بحد ذاته هو مجرد مربع يحمل قيمة. ما يجعل النموذج يتصرف مثل تطبيق صغير هو الإجراء المرتبط به: نقرة تخفي قسماً، أو تسترجع القيم المحفوظة من ملف، أو تنتقل إلى الصفحة الأخيرة، أو تشغل برنامجاً نصياً (script) يجمع عموداً. لا يوجد أي من هذا في الحقل. بل يوجد في قاموس الإجراءات (action dictionary)، وينظم معيار ISO 32000-1 العائلة بأكملها في القسم 12.6. يستعرض هذا المقال الإجراءات التي يستخدمها برنامج دلفي في أغلب الأحيان ويوضح كيف يقوم PDF Library for Delphi بربط كل منها بحقل أو رابط

النموذج الذهني الذي يجدر الاحتفاظ به هو أن الحقل والإجراء هما كائنان منفصلان مرتبطان بمرجع (reference). يحمل التعليق التوضيحي للأداة (widget annotation) أو التعليق التوضيحي للرابط (link annotation) إجراءً في إدخال /A الخاص به. يسمي الإجراء الحقل الذي يعمل عليه بالعنوان، وليس بالفهرس (index)، لذلك فإن العنوان الذي تطلقه على الحقل هو المعرف الذي يستخدمه كل إجراء لاحق للعثور عليه. بمجرد أن يصبح هذا الفصل واضحاً، تتوقف واجهة برمجة التطبيقات (API) عن الظهور كمجموعة عشوائية من الاستدعاءات وتبدأ في الظهور كنمط واحد مطبق على أربعة أنواع من الأفعال

مخطط من PDF Library for Delphi لتعليق رابط PDF أو عنصر واجهة يشير إلى قاموس إجراء واحد يتفرع إلى أفعال الإجراءات named و hide و import-data و JavaScript، مع بطاقة import-data موسومة كمستثناة من PDF/A
قاموس إجراءات واحد خلف رابط أو ودجت يغذي أفعالاً أربعة مختلفة — حركة منفذ العرض، والرؤية، والاستيراد من القرص، والبرمجة النصية

الإجراءات المسماة (Named actions): التنقل بدون رقم صفحة

أبسط الإجراءات لا تحمل أي معلمات (parameters) على الإطلاق. يعرّف معيار ISO 32000-1 القسم 12.6.4.11، الجدول 194، الإجراءات المسماة (named actions): يفسر العارض (viewer) اسماً رمزياً في وقت التشغيل بدلاً من اتباع وجهة مخزنة. توجد أربعة أسماء مدعومة عالمياً، وهي بالضبط ما يتوقعه القارئ من شريط الأدوات: NextPage و PrevPage و FirstPage و LastPage. نظراً لأن الوجهة نسبية لأي صفحة يعرضها العارض حالياً، فإن زر "التالي" (Next) المبني بهذه الطريقة يعمل على كل صفحة دون أن تضطر إلى حساب الوجهة

في PDF Library for Delphi، يتم إرفاق إجراء مسمى بمستطيل نقطة فعالة (hotspot) على الصفحة الحالية. تحدد الوسيطتان (arguments) الصحيحتان الرابعة والخامسة الفعل (verb) والمظهر (appearance)

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// يرسم bit 0 من Options (القيمة 1) حدًا حول hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // انتقل إلى الصفحة الأخيرة

لا توجد وجهة للاحتفاظ بمزامنتها، وهذا هو الهدف الأساسي. يظل الإجراء المسمى سارياً عند إدراج صفحة أو حذفها لأنه لا يسمي صفحة في المقام الأول. قارن ذلك برابط انتقال صريح (go-to link)، والذي يخزن فهرس صفحة مستهدفة يجب عليك إعادة ترقيمه في اللحظة التي يكبر فيها المستند

إجراء الإخفاء (Hide action) وفخ المصفوفة (array) الخاص به

يقوم إجراء الإخفاء (Hide action)، وفقاً لمعيار ISO 32000-1 القسم 12.6.4.10، الجدول 196، بتبديل رؤية (visibility) حقل واحد أو أكثر. إنها الطريقة الأنظف لبناء سلوك إظهار وإخفاء بدون برمجة نصية، وهو ما تريده لرابط "إظهار التفاصيل" (Show details) أو لوحتين متنافستين (mutually exclusive) حيث يؤدي كشف إحداهما إلى إخفاء الأخرى. يحمل الإجراء هدفاً في إدخال /T الخاص به وقيمة منطقية /H تحدد الاتجاه: إخفاء عندما تكون القيمة true، وإظهار عندما تكون القيمة false

تكمن الدقة تماماً في كيفية ترميز هذا الهدف، وهو نوع من التفاصيل التي تنتج نموذجاً يعمل على جهازك ويفشل على جهاز العميل. عندما يسمي الإجراء حقلاً واحداً، تتم كتابة /T كسلسلة نصية واحدة. عندما يسمي عدة حقول، تتم كتابة /T كمصفوفة من السلاسل النصية. لا تعامل العوارض القديمة مصفوفة مكونة من عنصر واحد بنفس الطريقة التي تعامل بها سلسلة مجردة، لذلك يجب أن يتفرع الترميز بناءً على العدد: يجب إصدار اسم واحد كسلسلة، وليس كمصفوفة بطول واحد، إذا كنت تريد أن يستجيب لها النطاق الأوسع من القراء. يتخذ PDF Library for Delphi هذا القرار نيابة عنك. أنت تمرر أسماء الحقول مفصولة بفواصل، أو فواصل منقوطة، أو فواصل أسطر (line breaks)، ويصدر الكاتب سلسلة واحدة للاسم الواحد ومصفوفة لاسمين أو أكثر

مخطط من PDF Library for Delphi لقواعد ترميز إجراء Hide في PDF حيث يصبح اسم حقل واحد بالضبط سلسلة نصية /T بينما يصبح اسمان أو أكثر مصفوفة سلاسل، مع ملاحظات عن اتجاه علم الإخفاء وسلامة PDF/A وعناوين الحقول المؤهلة بالكامل
يتفرع الكاتب على عدد الحقول، فيُشحن اسم منفرد كسلسلة مجردة تحترمها القارئات الأقدم، بينما يصبح الاسمان فأكثر مصفوفة تُبدَّل معاً
// القيمة غير الصفرية لـ HideFlag تخفي الحقول المدرجة (/H true)؛ والصفر يُظهرها
// اسم واحد -> /T هو text string؛ اسمان أو أكثر -> /T هو array من strings
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

نظراً لأن الإجراء لا يشير إلى مورد خارجي، فإنه يظل متوافقاً مع PDF/A. الأسماء التي تمررها هي عناوين حقول مؤهلة بالكامل (fully qualified)، ولهذا السبب يجب توجيه الحقل الفرعي (child field) داخل مجموعة من خلال مساره المنقط الكامل بدلاً من اسمه الورقي (leaf name) المجرد

استيراد البيانات (ImportData): التعبئة المسبقة من FDF

حيثما يعيد إجراء الإخفاء ترتيب ما هو موجود بالفعل على الصفحة، فإن إجراء استيراد البيانات (import-data action) يجلب قيماً من خارجها. يعرّفه معيار ISO 32000-1 القسم 12.6.4.8، الجدول 198، بأنه إجراء يملأ نموذج AcroForm من ملف تنسيق بيانات النماذج (Forms Data Format) على القرص. هذا هو الإجراء الذي يقف خلف عنصر تحكم "إعادة تحميل بيانات العينة" (Reload sample data) أو "إعادة التعيين إلى الإعدادات الافتراضية" (Reset to defaults)، حيث يتم إرفاق ملف FDF بجوار ملف PDF ويحتوي على قيم الحقول الأساسية. يحاكي الاستدعاء الاستدعاءات الأخرى، حيث يأخذ مستطيل النقطة الفعالة، والمسار إلى FDF، وقناع بت للمظهر (appearance bitmask): Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). لا يحتاج الملف إلى أن يكون موجوداً عند إنشاء ملف PDF، ولكن يجب أن يكون موجوداً عندما ينقر المستخدم، وتتم إعادة كتابة أي خطوط مائلة عكسية (backslashes) في المسار إلى صيغة الخط المائل القياسية لـ PDF نيابة عنك

يستحق أحد القيود ذكره بوضوح لأنه غالباً ما يكون مفاجئاً. يشير إجراء استيراد البيانات إلى ملف خارجي، لذا فهو غير مسموح به في PDF/A. عندما يكون المستند في وضع PDF/A، يرجع الاستدعاء صفراً ولا يضيف أي شيء بدلاً من إنتاج ملف يفشل في التحقق من الصحة. إذا كان مسارك (pipeline) يستهدف المخرجات الأرشيفية، فيجب أن تتم التعبئة المسبقة في وقت الإنشاء عن طريق كتابة قيم الحقول مباشرة، وليس عن طريق تأجيلها حتى يتم النقر عليها

جافا سكريبت (JavaScript): الحزم العامة (global packages) والبرامج النصية لكل إجراء (per-action scripts)

بالنسبة للمنطق الذي يتجاوز الإظهار، والإخفاء، والاستيراد، تصل عائلة الإجراءات إلى جافا سكريبت (JavaScript) على مستوى المستند. هناك مكانان متميزان يمكن أن يتواجد فيهما البرنامج النصي، والفرق بينهما مهم. يتم تخزين حزمة جافا سكريبت على مستوى المستند مرة واحدة للملف بأكمله وتعمل عند فتح المستند، مما يجعلها الموطن المناسب لتعريفات الوظائف (functions) والحالة المشتركة (shared state). يتم إرفاق برنامج نصي لكل إجراء برابط أو حقل واحد ويعمل فقط عند تنشيط هذا الكائن، مما يجعله الموطن المناسب للسطر الواحد الذي يستدعي وظيفة تم تعريفها مسبقاً في الحزمة

يُعرض PDF Library for Delphi كليهما. يقوم AddGlobalJavaScript بتخزين حزمة مسماة على مستوى المستند؛ إعادة استخدام اسم يستبدل ما كان مخزناً تحته. يقوم AddLinkToJavaScript بإرفاق برنامج نصي بنقطة فعالة بحيث تؤدي النقرة إلى تنفيذه

مخطط من PDF Library for Delphi لنموذج JavaScript في PDF بطبقتين حيث تعرّف حزمة عامة على مستوى المستند الدالة recalcTotal عند فتح المستند ويحمل كل رابط نصًا برمجيًا من سطر واحد لكل إجراء يستدعيها عند النقر
عرّف recalcTotal مرة واحدة في حزمة مستوى المستند ودع كل نقطة ساخنة قابلة للنقر تبلغه باستدعاء من سطر واحد لكل إجراء
// حزمة على مستوى المستند: عرّف function قابلة لإعادة الاستخدام مرة واحدة
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// script لكل action على link: استدعِ function المشتركة فقط
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

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

الحقول، والحقول الفرعية، وتجميد النتيجة (freezing the result)

تحتاج الإجراءات إلى حقول للعمل عليها، لذا من المفيد أن نرى كيف يتم إنشاء الحقل. يقوم NewFormField بإنشاء حقل على الصفحة الحالية ويُرجع فهرسه؛ يحدد النوع الصحيح (integer type) الصنف، حيث 1 للنص (Text)، و 2 لزر الدفع (Pushbutton)، و 3 لمربع الاختيار (Checkbox)، و 4 لزر الراديو (Radiobutton)، و 5 للاختيار (Choice)، و 6 للتوقيع (Signature)، و 7 للوالد (Parent) الذي يمتلك فروعاً (children) ولكنه لا يرسم أي شيء بنفسه. العنوان الذي تمرره لا يمكن أن يحتوي على نقطة، لأن النقطة هي الفاصل (separator) في الأسماء المؤهلة بالكامل التي تستخدمها الإجراءات لمخاطبة الفروع

يتم بناء مجموعات أزرار الراديو (Radio groups) والنماذج الهرمية عن طريق إعطاء حقل والد (parent field) فروعاً. يضيف NewChildFormField فرعاً تحت والد مسمى، وفي حالات أزرار الراديو والاختيار، يضيف AddFormFieldSub الخيارات الفردية ويعيد فهرساً مؤقتاً تستخدمه لوضع كل واحد منها. عندما تنتهي المرحلة التفاعلية وتريد تجميد حقل بحيث يصبح مظهره الحالي محتوى صفحة دائماً، يقوم FlattenFormField برسم الحقل على الصفحة وإزالته من النموذج. بعد التسوية (flatten)، تنتقل فهارس الحقول اللاحقة لأسفل بمقدار واحد، وهو الشيء الوحيد الذي يجب تذكره إذا قمت بتسوية عدة حقول في حلقة (loop)

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // حقل نصي ستستهدفه action Hide بواسطة title الخاص به
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // اربط link من نوع Hide وlink للتنقل بهذه الصفحة
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Last page

    // script على مستوى المستند متاح لكل event في الملف
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // جمّد الحقل إذا لم يعد output قابلًا للتحرير
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

استدعاء التسوية معطل (commented out) عن قصد. اتركه خارجاً وسيشحن المستند كنموذج حي تنطلق إجراءاته في القارئ. قم بتمكينه وسوف يتم تصيير (rendered) الحقل إلى علامات ثابتة، وهو ما تريده عندما يكتمل النموذج ويجب أن تنتقل النتيجة كسجل ثابت. نفس الحقل، ونفس الكود، ومستندان مختلفان تماماً بناءً على ما إذا كنت ستجمده أم لا

اختيار الفعل المناسب

تنقسم الإجراءات الأربعة بشكل نظيف حسب ما تلمسه. يقوم الإجراء المسمى بتحريك إطار العرض (viewport) ولا يحتاج إلى حقل. يغير إجراء الإخفاء (Hide action) الرؤية (visibility) ويحتاج إلى عناوين حقول، مع التعامل مع ترميز السلسلة مقابل المصفوفة (string-versus-array encoding) نيابة عنك. يصل إجراء استيراد البيانات إلى ملف على القرص وبالتالي فهو غير مسموح به في PDF/A. يقوم إجراء جافا سكريبت بتشغيل منطق تعسفي ومن الأفضل تقسيمه بين حزمة عامة من الوظائف (functions) واستدعاءات صغيرة لكل إجراء. استخدم أبسط إجراء ينجز المهمة: إجراء الإخفاء أكثر قابلية للنقل من برنامج نصي يقوم بتعيين علامة مخفية (hidden flag)، والإجراء المسمى أكثر متانة من وجهة صفحة مخزنة لأنه لا يوجد رقم يجب الحفاظ عليه

من هنا، يكمل موضوعان متجاوران الصورة. إذا كان النموذج جزءاً من مستند يمكن الوصول إليه (accessible document)، فإن شجرة البنية التي يتجول فيها قراء الشاشة مغطاة في مقالنا حول PDF الموسوم وبنية إمكانية الوصول (tagged PDF and accessibility structure). عندما يتعين قفل النموذج المكتمل وتوقيعه، يتم وصف سير العمل في نظرة عامة على طاولة عمل الامتثال والتوقيع (compliance and signing workbench). تعتمد الثلاثة جميعها على نفس المحرك، والذي يتم شحنه كـ مكتبة PDF لدلفي جنباً إلى جنب مع واجهات برمجة تطبيقات (APIs) الإنشاء والنموذج والتوقيع المغطاة في مكان آخر من هذه المدونة