يمنح PDFlibPas، مكتبة PDF الأصلية لـDelphi وC++Builder، مستند PDF مكانين منفصلين لتعليق سلوك تلقائي: أفعال دورة حياة على مستوى المستند مثل WillClose، وWillSave، وDidSave، وWillPrint، وDidPrint، مخزَّنة في قاموس /AA الخاص بالدليل، وأفعال دورة حياة على مستوى الصفحة — Open وClose — مخزَّنة في قاموس /AA الخاص بكائن الصفحة نفسه بدلًا من ذلك. الخلط بين الحاويتين هو الطريقة الأكثر شيوعًا لجعل فعل دورة حياة لا يفعل شيئًا بصمت
الحالات المُحفِّزة عادية. يريد فريق مالية قالب كشف يختم طابعًا زمنيًا للطباعة ويسجّل من طبعه في اللحظة التي تبدأ فيها الطباعة فعليًا، لا عند مجرد فتح الملف. يحتاج سير عمل كثيف النماذج دفع قيم الحقول إلى خادم تلقائيًا قبل السماح لعميل PDF القارئ بإغلاق النافذة، بحيث لا تعني علامة تبويب مُغلَقة أبدًا تعديلًا مفقودًا. يريد تقرير متعدد الصفحات لافتة خاصة بصفحة تظهر فقط بينما تلك الصفحة على الشاشة. يعرض PDF فعليًا طبقة ثالثة تحت المستند والصفحة لهذا النوع من السلوك — أفعال مرفقة بحقل نموذج فردي أو إدخال /A الخاص برابط، موضوع مقالة مرافقة حول أفعال النموذج التفاعلية وجافاسكريبت — لكن هذه المقالة تبقى عند الطبقتين فوقها: المستند بأكمله، وصفحة واحدة
أي مُحفِّزات تعيش على /AA الخاص بدليل المستند؟
تعيش خمسة مُحفِّزات على قاموس /AA الخاص بالدليل، ويُطلَق كل واحد منها لحدث يؤثر على المستند بأكمله، لا صفحة واحدة. يسرد ISO 32000-1 §12.6.3 (أحداث المُحفِّز) مفاتيح مستوى المستند كـWC، وWS، وDS، وWP، وDP — الأسماء الحرفية من حرفين المكتوبة في قاموس /AA — لـWillClose، وWillSave، وDidSave، وWillPrint، وDidPrint على التوالي، ويعكس PDFlibPas تلك المجموعة بالضبط في تعداد TPDFlibDocumentActionTrigger: datWillClose، وdatWillSave، وdatDidSave، وdatWillPrint، وdatDidPrint. SetDocumentAction هي نقطة الدخول الواحدة التي تُرفق أيًّا من الخمسة، ومعامل ActionKind الذي تأخذه هو واحد من عشرة ثوابت PDF_ACTION_BUILDER_* مشتركة عبر كل استدعاء بناء فعل في المكتبة، من رابط عادي إلى سكربت إلى قفزة وجهة. ما يفعله فعل GoTo، أو ملف بعيد، أو ملف مضمَّن، أو Launch فعليًا بمجرد تحفيزه سؤال مختلف عن أين يُرفق، وهذا موضوع مقالة مرافقة حول أفعال GoTo، والملف البعيد، والمضمَّن، وLaunch — هذه تبقى عند سؤال الحاوية، الدليل أو الصفحة، بدلًا من سؤال نوع الفعل
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.AddStandardFont(4);
Lib.DrawText(40, 700, 'Quarterly statement');
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save', '', 0, 0);
Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
Lib.SaveToFile('statement.pdf');
finally
Lib.Free;
end;
end;
كيف يختلف مُحفِّز على مستوى الصفحة عن آخر على مستوى المستند؟
يُطلَق مُحفِّز على مستوى الصفحة فقط لكائن الصفحة الواحد المرفق به، ويخزّنه PDFlibPas في قاموس /AA الخاص بتلك الصفحة نفسها بدلًا من الدليل. لا يوجد سوى مُحفِّزي صفحة، Open وClose، يقابلان مفتاحي O وC اللذين يعرّفهما ISO 32000-1 لقاموس الأفعال الإضافية الخاص بالصفحة، ويعرضهما PDFlibPas كـpatOpen وpatClose عبر SetPageAction، التي تُرفق بأيًّا كانت الصفحة المحددة حاليًا عبر SelectPage — تفصيل يهم أول مرة تجتاز فيها مستندًا متوقعًا أن استدعاءً واحدًا سينطبق في كل مكان، لأنه لا يفعل ذلك أبدًا. إرفاق أي من نوعي المُحفِّز يرفع أيضًا الحد الأدنى لإصدار PDF للملف، وتطلب الحاويتان أرضيتين مختلفتين: يرفع PDFlibPas المستند إلى PDF 1.4 على الأقل في أول مرة يكتب فيها إدخال /AA للدليل، وإلى PDF 1.5 على الأقل في أول مرة يكتب فيها إدخال /AA لصفحة، بصرف النظر عن أي نوع فعل يجلس بداخله. هذا متطلب على مستوى الحاوية مُضاف فوق أيًّا كان ما يحتاجه الفعل نفسه بمفرده، بحيث لا يزال فعل رابط عادي كان سيتطلب فقط PDF 1.1 بمفرده يسحب الملف بأكمله إلى PDF 1.5 بمجرد لفّه في مُحفِّز فتح صفحة
Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
'https://example.com/analytics/page-3-closed', '', 0, 0);
قراءة أفعال دورة الحياة وحذفها
تُعيد كل من GetDocumentActionInfo وGetPageActionInfo سجل TPDFlibActionInfo، ويعود حقل Kind بـakNone كلما لم يكن لذلك المُحفِّز شيء مرفَق، لذا تحقق من Kind قبل الثقة بأي حقل آخر في السجل — URI، وJavaScript، وFileName والبقية ذات معنى فقط لنوع الفعل الواحد الذي يبلّغ عنه Kind فعليًا، بما أن شكل السجل نفسه يُعاد استخدامه عبر كل نوع فعل يمكن للباني إنتاجه. تمسح كل من RemoveDocumentAction وRemovePageAction مُحفِّزًا واحدًا وتبلّغ بـ1 عندما تجد شيئًا لحذفه، و0 عندما يكون المُحفِّز فارغًا بالفعل؛ وعندما يكون الإدخال المحذوف آخر واحد متبقٍ في قاموس /AA، يحذف PDFlibPas /AA الفارغة الآن نفسها بدلًا من ترك حاوية متدلية بلا معنى وراءها على الدليل أو الصفحة
var
Info: TPDFlibActionInfo;
begin
Info := Lib.GetDocumentActionInfo(datWillSave);
if Info.Kind = akURI then
WriteLn('WillSave calls out to: ', string(Info.URI));
if Lib.RemoveDocumentAction(datWillSave) = 1 then
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save-v2', '', 0, 0);
end;
هل يسمح PDF/A بأفعال دورة الحياة على الإطلاق؟
لا. يرفض توافق PDF/A حاوية الأفعال الإضافية بأكملها، لا أنواع الأفعال التي تبدو محفوفة بالمخاطر فقط، لأن ISO 19005 يقيّد نموذج الفعل التفاعلي في PDF على افتراض أن ملفًا أرشيفيًا يجب أن يُرسَم بالطريقة نفسها بعد عقود من الآن، دون الاعتماد على محرك سكربتات أو اتصال شبكي قد لا يكون موجودًا بحلول ذلك الوقت. تتحقق SetLifecycleAction، الباني المشترك خلف كل من SetDocumentAction وSetPageAction، من PDFAMode قبل أن تنظر إلى ActionKind على الإطلاق، بحيث يُلتقط فعل رابط يفتح فقط صفحة ويب للشركة أو فعل مسمّى يعني فقط الانتقال إلى الصفحة التالية في الشبكة نفسها كفعل خطير، لا شيء كان مراجع أمني ليعلّمه عادة، مُحجَّب على أي حال، لأن القيد بنيوي لا حالة بحالة. الخطر العملي أن الرفض صامت: تُعيد كل من SetDocumentAction وSetPageAction القيمة 0 دون إطلاق استثناء، بحيث يشحن موقع استدعاء لا يتحقق أبدًا من القيمة المُعادة مستندًا يفتقد بصمت المُحفِّز الذي كان يُفترض أن يحمله
Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
'', '', 0, 0) = 0 then
// rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
WriteLn('lifecycle action not attached');
عدم تناظر واحد يستحق تذكره. لا تتحقق RemoveDocumentAction وRemovePageAction أبدًا من PDFAMode، بحيث يعمل تحميل ملف يحمل بالفعل أفعال دورة حياة غير متوافقة وحذفها في الطريق إلى حفظ متوافق مع PDF/A تمامًا كما هو متوقع — مسار الكتابة فقط، إرفاق مُحفِّز جديد، هو المُقيَّد بنمط التوافق
أين تلائم الطباعة-عند-الفتح دون مُحفِّز WillOpen؟
لا يملك قاموس /AA الخاص بالدليل إدخال WillOpen على الإطلاق، بالتصميم — يعرّف /AA على مستوى المستند في ISO 32000-1 خمسة مفاتيح بالضبط، WillClose، وWillSave، وDidSave، وWillPrint، وDidPrint، ولا شيء في تلك القائمة يُطلَق لمجرد فتح ملف. يعيش خطاف وقت الفتح في إدخال دليل منفصل، /OpenAction، الذي يعرضه PDFlibPas عبر عائلته الخاصة من الاستدعاءات، SetOpenActionJavaScript، وSetOpenActionDestination، وSetOpenActionNamedDestination من ضمنها، ولا يلمس أي منها قاموس /AA أو تعداد TPDFlibDocumentActionTrigger على الإطلاق. لكن الآليتين تتركبان، وهذا عادة ما يحتاجه قالب طباعة-عند-الفتح فعليًا: ابنِ القالب بحيث يبدأ /OpenAction الخاص به مهمة الطباعة، عادة فعل جافاسكريبت يستدعي أمر الطباعة الخاص بالعارض نفسه، والطباعة نفسها هي ما يمنح WillPrint وDidPrint شيئًا يعملان عليه — طابع زمني يُختم قبل تجميع الصفحات، إدخال تدقيق يُكتب بمجرد انتهائها
ما مدى موثوقية هذه المُحفِّزات عبر عارضات PDF؟
لا يشغّلها كل عارض، حتى خارج PDF/A، لذا عامل فعل دورة حياة كطلب لا ضمان. يشغّل Acrobat ومعظم القارئات الكاملة لسطح المكتب المجموعة بأكملها بأمانة، لكن حصة كبيرة من استهلاك PDF الواقعي لا تلمس أبدًا قاموس أفعال إضافية على الإطلاق: عارضات مضمَّنة في المتصفح، ومعظم قارئات الجوال، وشبه كل خط أنابيب رسم من جانب الخادم أو استخراج نص إما يتجاهل /AA كليًا أو يحترم فقط شريحة ضيقة منه، مع أداء WillPrint وDidPrint عادة الأسوأ بما أن التحويل بلا رأس (headless) ليس لديه عملية طباعة ليتصل بها. إذا كان فعل إرسال نموذج WillClose المسار الوحيد الذي يلتقط بيانات النموذج، فهو ليس مسارًا موثوقًا — اقرنه بزر إرسال صريح، وعامل المُحفِّز التلقائي كراحة للقارئات التي تصادف دعمها له
مُحفِّزات المستند، والصفحة، والحقل ثلاث طبقات من آلية قاموس الأفعال الكامنة نفسها، وبمجرد وضوح الحاوية، يصبح الباقي اختيار ثابت ActionKind الصحيح والتحقق من رمز الإرجاع. مُحفِّزات دورة الحياة هذه، جنبًا إلى جنب مع واجهة برمجة باني الأفعال الأوسع التي تلمسها هذه المقالة، تُشحن كجزء من مكتبة PDFlibPas لـDelphi القياسية، مع مرجع المُحفِّز ونوع الفعل الكامل في توثيق المنتج