Technical Article

نماذج PDF التفاعلية في Delphi: الإجراءات وJavaScript

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

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

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

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

في PDFlibPas، يتم إرفاق إجراء مسمى بمستطيل منطقة نشطة (hotspot) في الصفحة الحالية. يحدد المعاملان الصحيحان الرابع والخامس الفعل والمظهر.

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the 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);   // jump to last page

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

إجراء الإخفاء وفخ المصفوفة الخاص به

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

تكمن الدقة تماماً في كيفية ترميز هذا الهدف، وهو نوع التفاصيل التي تجعل النموذج يعمل على جهازك ويفشل لدى العميل. عندما يشير الإجراء إلى حقل واحد، يُكتب /T كسلسلة نصية واحدة. وعندما يشير إلى عدة حقول، يُكتب /T كمصفوفة من السلاسل النصية. لا تعامل برامج العرض القديمة المصفوفة أحادية العنصر بنفس الطريقة التي تعامل بها سلسلة مجردة، لذا يجب أن يتفرع الترميز بناءً على العدد: يجب إرسال الاسم المفرد كسلسلة، وليس كمصفوفة بطول واحد، لكي تحترمه أوسع مجموعة من القراء. يتخذ PDFlibPas هذا القرار بالنيابة عنك. يمكنك تمرير أسماء الحقول مفصولة بفاصلة، أو فاصلة منقوطة، أو فواصل أسطر، ويقوم الكاتب بإخراج سلسلة نصية واحدة للاسم الواحد ومصفوفة لاسمين أو أكثر.

// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

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

ImportData: التعبئة المسبقة من FDF

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

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

JavaScript: الحزم العامة والنصوص البرمجية الخاصة بكل إجراء

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

يكشف PDFlibPas كلاهما. يقوم AddGlobalJavaScript بتخزين حزمة مسماة على مستوى المستند، ويؤدي إعادة استخدام الاسم إلى استبدال كل ما تم تخزينه تحته. بينما يقوم AddLinkToJavaScript بإرفاق نص برمجي بمنطقة نشطة بحيث يؤدي النقر عليها إلى تنفيذه.

// Document-level package: define a reusable function once.
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);' +
  '}');

// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

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

الحقول، والحقول الفرعية، وتجميد النتيجة

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

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

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

    // A text field the Hide action will target by its title.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Wire a Hide link and a navigation link to this page.
    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

    // A document-level script available to every event in the file.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Freeze the field if the output should no longer be editable.
    // Pdf.FlattenFormField(FldShip);

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

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

اختيار الفعل الصحيح

تنقسم الإجراءات الأربعة بوضوح حسب ما تلمسه. يقوم إجراء مسمى بنقل منفذ العرض (viewport) ولا يحتاج إلى حقل. ويغير إجراء الإخفاء (Hide) الرؤية ويحتاج إلى عناوين الحقول، مع معالجة ترميز السلسلة مقابل المصفوفة نيابة عنك. ويصل إجراء استيراد البيانات إلى ملف على القرص وبالتالي فهو محظور في PDF/A. ويقوم إجراء JavaScript بتشغيل منطق عشوائي ويفضل تقسيمه بين حزمة عامة من الدوال واستدعاءات صغيرة لكل إجراء. اختر الأبسط الذي يؤدي الغرض: إجراء الإخفاء أكثر قابلية للنقل من نص برمجي يعين علامة إخفاء، والإجراء المسمى أكثر متانة من وجهة صفحة مخزنة لأنه لا يوجد رقم للمحافظة عليه.

من هنا، يكتمل المشهد بموضوعين مجاورين. إذا كان النموذج جزءاً من مستند يسهل الوصول إليه، فإن شجرة البنية التي تقرأها قارئات الشاشة مغطاة في مقالنا حول مستندات PDF الموسومة وبنية إمكانية الوصول. وعندما يتعين قفل النموذج المكتمل وتوقيعه، يتم وصف سير العمل في دليل workbench للامتثال والتوقيع. تعتمد جميعها على نفس المحرك، الذي يتم شحنه باسم مكتبة PDF لـ Delphi إلى جانب واجهات برمجة التطبيقات للإنشاء والنماذج والتوقيع المغطاة في مكان آخر من هذه المدونة.