PDFlibPas, ספריית ה-PDF הילידית של Delphi ו-C++Builder, נותנת למסמך PDF שני מקומות נפרדים לתלות בהם התנהגות אוטומטית: פעולות מחזור-חיים ברמת-מסמך כמו WillClose, WillSave, DidSave, WillPrint ו-DidPrint, שמורות במילון ה-/AA של הקטלוג, ופעולות מחזור-חיים ברמת-עמוד — Open ו-Close — שמורות במילון ה-/AA של אובייקט העמוד עצמו במקום זאת. בלבול בין שתי המכולות הוא הדרך הנפוצה ביותר שבה פעולת מחזור-חיים בשקט לא עושה כלום
המקרים המניעים רגילים. צוות כספים רוצה תבנית דוח שמטביעה חותמת-זמן הדפסה ורושמת מי הדפיס אותה ברגע שההדפסה בפועל מתחילה, לא כשהקובץ סתם נפתח. זרימת עבודה כבדת-טפסים זקוקה לערכי שדה שנדחפים לשרת אוטומטית לפני שלקוח ה-PDF של הקורא מורשה לסגור את החלון, כך שלשונית סגורה לעולם לא תאמר עריכה אבודה. דוח רב-עמודי רוצה כרזה ספציפית-לעמוד שמופיעה רק בעוד העמוד ההוא על המסך. PDF בפועל מציע שכבה שלישית מתחת למסמך ועמוד עבור סוג ההתנהגות הזה — פעולות שמצומדות לרשומת /A של שדה טופס בודד או קישור, נושא מאמר נלווה על פעולות טופס אינטראקטיביות ו-JavaScript — אבל המאמר הזה נשאר בשתי השכבות שמעליה: המסמך כולו, ועמוד יחיד
אילו טריגרים חיים על ה-/AA של הקטלוג?
חמישה טריגרים חיים במילון ה-/AA של הקטלוג, וכל אחד מהם מופעל עבור אירוע שמשפיע על המסמך כולו, לא עמוד בודד. ISO 32000-1 סעיף 12.6.3 (Trigger Events) מפרט את המפתחות ברמת-המסמך כ-WC, WS, DS, WP ו-DP — השמות המילוליים בני-שני-האותיות שנכתבים לתוך מילון ה-/AA — עבור WillClose, WillSave, DidSave, WillPrint ו-DidPrint בהתאמה, ו-PDFlibPas משקפת את הקבוצה ההיא בדיוק בטיפוס-המנייה TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction היא נקודת הכניסה היחידה שמצמידה כל אחת מהחמש, והפרמטר ActionKind שהיא מקבלת הוא אחד מעשרה קבועי PDF_ACTION_BUILDER_* משותפים בכל קריאת בונה-פעולה בספרייה, מ-URI פשוט לסקריפט לקפיצת יעד. מה ש-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 של עמוד, ללא קשר לאיזה סוג-פעולה יושב בפנים. זו דרישה ברמת-מכולה ששוכבת מעל כל מה שהפעולה עצמה זקוקה לו בזכות עצמה, כך שפעולת URI ערומה שהייתה זקוקה רק ל-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, כך שפעולת URI שסתם פותחת דף אינטרנט חברתי או פעולת Named ששוקלת רק ללכת לעמוד הבא נלכדת באותה רשת כמו אחת מסוכנת — שום דבר שסוקר אבטחה בדרך כלל היה מסמן, נחסם בכל זאת, משום שההגבלה מבנית ולא לפי-מקרה. הסכנה המעשית היא שהדחייה שקטה: 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, ושום דבר ברשימה ההיא לא מופעל אך ורק משום שקובץ נפתח. ה-hook של זמן-פתיחה חי ברשומת קטלוג נפרדת, /OpenAction, ש-PDFlibPas חושפת דרך משפחת קריאות משלה, SetOpenActionJavaScript, SetOpenActionDestination ו-SetOpenActionNamedDestination ביניהן, אף אחת מהן לא נוגעת במילון ה-/AA או בטיפוס-המנייה TPDFlibDocumentActionTrigger בכלל. שני המנגנונים כן מרכיבים יחד, עם זאת, וזה בדרך כלל מה שתבנית הדפסה-בפתיחה בפועל זקוקה לו: בנה את התבנית כך שה-/OpenAction שלה מתחיל את עבודת ההדפסה, בדרך כלל פעולת JavaScript שקוראת לפקודת ההדפסה של המציג עצמו, וההדפסה עצמה היא מה שנותן ל-WillPrint ו-DidPrint משהו לרוץ מולו — חותמת-זמן שמוטבעת לפני שהעמודים נשלחים ל-spool, רשומת ביקורת שנכתבת ברגע שהם מסתיימים
עד כמה הטריגרים האלה אמינים על פני מציגי PDF?
לא כל מציג מריץ אותם, אפילו מחוץ ל-PDF/A, כך שהתייחס לפעולת מחזור-חיים כבקשה ולא ערבות. Acrobat ורוב הקוראים המלאים לשולחן-עבודה מבצעים את הקבוצה כולה בנאמנות, אבל חלק גדול מצריכת PDF אמיתית בעולם אף פעם לא נוגעת במילון פעולות-נוספות בכלל: מציגים משובצי-דפדפן, רוב הקוראים הניידים, וכמעט כל צינור עיבוד או חילוץ-טקסט בצד-שרת או מתעלמים מ-/AA לחלוטין או מכבדים רק פרוסה צרה ממנו, כאשר WillPrint ו-DidPrint בדרך כלל מסתדרים הכי גרוע שכן להמרה חסרת-ראש אין פעולת-הדפסה שהם יכולים להתחבר אליה. אם פעולת שליחת-טופס ב-WillClose היא הנתיב היחיד שלוכד נתוני טופס, זה לא נתיב אמין — צמד אותה עם כפתור-שליחה מפורש, והתייחס לטריגר האוטומטי כנוחות עבור הקוראים שבמקרה תומכים בו
טריגרי מסמך, עמוד, ושדה הם שלוש שכבות של אותו מנגנון מילון-פעולה בסיסי, וברגע שהמכולה ברורה, השאר הוא בחירת קבוע ה-ActionKind הנכון ובדיקת קוד ההחזרה. הטריגרים האלה של מחזור-חיים, לצד ה-API הרחב יותר של בונה-הפעולה שהמאמר הזה נוגע בו, נשלחים כחלק מספריית ה-PDF Delphi PDFlibPas הסטנדרטית, עם מסמך העזר המלא לטריגר וסוג-פעולה בתיעוד המוצר