PDFlibPas נותנת למפתחי Delphi ו-C++Builder שלושה סוגי פעולה עבור ניווט שעוזב את העמוד הנוכחי מאחור: GoToR (Go To Remote) פותח עמוד ספציפי בקובץ PDF אחר, GoToE (Go To Embedded) פותח קובץ PDF משובץ בתוך המסמך הנוכחי, ו-Launch מריץ תוכנית חיצונית או פותח קובץ דרך מעטפת מערכת ההפעלה. שלושתם חיים ב-ISO 32000-1 סעיף 12.6.4, סעיף סוגי-הפעולה שגם מגדיר את פעולת ה-GoTo היומיומית, וכל אחד נושא מלכודת משלו עבור הבלתי-זהיר: מספר עמוד שאומר משהו שונה בהתאם לאיזו קריאה בונה אותו, יעד שהוא שם ולא נתיב-קובץ, וזוג פרמטרי מחרוזת שנראים זהים אבל משרתים שני מציגים שונים
שום דבר מזה אינו תיאורטי. חבילת עזר טכני — מדריך ראשי, PDF מפרטים שמפיץ מעדכן בלוח-הזמנים שלו עצמו, כלי כיול שמותקן לצד שניהם — נשענת בדיוק על סוג החיווט הבין-מסמכי הזה: הפניה שחייבת לנחות על עמוד 5 של קובץ המפרטים, גיליון נתונים ששווה לשלוח בתוך המדריך ולא לצידו, קישור שמעביר ישר לכלי הכיול. המאמר הזה הוא תמונת-ראי של קריאת פעולות סימנייה והערה בחזרה מ-PDF קיים: החלק ההוא מכסה צריכת פעולת GoToR, Launch, או GoToE שמפיק אחר כבר כתב לתוך קובץ; זה מכסה בניית אותם שלושה סוגי-פעולה מאפס, כולל כללי-ברמת-שדה ש-PDFlibPas אוכפת לפני שהיא מבצעת בייט אחד
שלוש דרכים שבהן פעולת PDF יכולה לעזוב את העמוד הנוכחי
PDFlibPas מפרידה ניווט מקומי מכל דבר אחר במפתח /S של הפעולה, ו-GoToR, GoToE, ו-Launch הם שלושת תת-הסוגים שהיעד שלהם יושב מחוץ לעמוד הנוכחי: GoToR תחת ISO 32000-1 סעיף 12.6.4.3, GoToE תחת סעיף 12.6.4.4, ו-Launch תחת סעיף 12.6.4.5, כולם בתוך סעיף 12.6.4 הרחב יותר, סוגי-פעולה, שגם מגדיר את פעולת ה-GoTo היומיומית. יעד של פעולת GoTo פשוטה נוקב בשם אובייקט-עמוד שכבר קיים בתוך המסמך, כך ש-PDFlibPas יכולה לאמת אותו מיד; GoToR ו-GoToE לא יכולות לעשות זאת באותו אופן, שכן הקובץ החיצוני אולי אפילו לא קיים על המכונה הזו וספירת-העמודים של קובץ משובץ אינה משהו שהמסמך המארח עוקב אחריו, כך ששניהם נושאים הפניה בלתי-נפתרת במקום קישור קשיח — מפרט קובץ בתוספת יעד עבור GoToR, שם קובץ-משובץ בתוספת עמוד יעד עבור GoToE — בעוד Launch משמיטה את מושג היעד לגמרי וסתם נוקבת במשהו שמערכת ההפעלה צריכה להריץ או לפתוח. הפיצול הזה מופיע כשתי משפחות קריאה בצד-הכתיבה: בונים ברמה-גבוהה, חד-שלביים כמו AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF, ו-AddLinkToLocalFile יוצרים הערת-קישור נקודת-חמה-בעמוד והפעולה שלה יחד, מכסים את רוב הפריסות האמיתיות — שורת טקסט או אייקון שקורא לוחץ עליו — בעוד קובעים ברמה-נמוכה-יותר כמו SetActionRemoteDestinationEx, SetActionLaunchOptions, ומקביליהם AddActionNext* מצמידים או מחליפים פעולה על משהו שכבר יש לך ידית אליו: סימנייה קיימת, טריגר שדה-טופס, או אירוע מחזור-חיים ברמת-מסמך או ברמת-עמוד. שתי המשפחות מסתיימות בכתיבת אותן צורות מילון בדיוק; ההבדל הוא איפה אתה עומד כשאתה קורא להן, וכפי שהחלק הבא מכסה, מה מספר עמוד אומר כשאתה כן
איך אתה בונה קישור GoToR שפותח עמוד בקובץ PDF אחר?
פעולת GoToR זקוקה לשני דברים — מפרט קובץ ויעד בתוך הקובץ ההוא — ו-PDFlibPas חושפת שתי קריאות שונות עבור מסירת החלק השני, כל אחת עם מוסכמת מספור-עמוד משלה. AddLinkToFile ו-AddLinkToFileEx, בוני הנקודה-החמה-בעמוד ברמה-גבוהה, מאמתים את הארגומנט Page או DestPage שלהם כגדול מאפס, אותו מספור מבוסס-1 ש-PDFlibPas משתמשת בו בכל מקום אחר, כולל SelectPage. SetActionRemoteDestinationEx, הקובע ברמה-נמוכה-יותר שמשמש להצמדה או החלפה של פעולת GoToR על משהו שכבר יש לך ידית אליו, במקום זאת מאמת את DestPage כגדול-או-שווה-לאפס וכותב אותו ישר לתוך מערך היעד המפורש של הפעולה ללא התאמה: הוא רוצה את אינדקס-העמוד הגולמי, מבוסס-האפס, של מסמך-היעד, המספור ש-ISO 32000-1 מגדיר עבור יעד מפורש מרוחק. קרא לקובע ברמה-נמוכה עם אותו מספר שהיית מוסר לבונה ברמה-גבוהה והקישור נפתח עמוד אחד מוקדם מדי
var
Lib: TPDFlib;
ActionID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('manual.pdf', '') = 1 then
begin
Lib.SelectPage(12);
// Page is 1-based here, same as SelectPage above: this opens
// the fifth page of specs.pdf.
Lib.AddLinkToFile(72, 700, 200, 16, 'specs.pdf', 5, 0, 0, 0);
// A later maintenance pass repoints the same link at a
// reorganized file. SetActionRemoteDestinationEx edits the
// action directly, and DestPage here is the zero-based index
// PDF itself uses for a remote explicit destination -- "the
// fifth page" is now 4, not 5.
ActionID := Lib.GetAnnotActionID(1);
Lib.SetActionRemoteDestinationEx(ActionID, 'specs-2026.pdf',
4, Ord(dkFit), 0, 0, 0, 0, 0, 0, -1);
end;
finally
Lib.Free;
end;
end;
שאר הארגומנטים של SetActionRemoteDestinationEx מילוליים באותה מידה. ValueMask הוא סט-סיביות — 1 עבור שמאל, 2 עבור עליון, 4 עבור ימין, 8 עבור תחתון, 16 עבור זום — ו-PDFlibPas בודקת אותו מול DestType לפני שהיא כותבת משהו: יעד dkFitR חייב לספק בדיוק 15 (כל ארבעת הקצוות, ללא זום), dkFit ו-dkFitB חייבים לספק 0, ו-dkFitH/dkFitV מקבלים רק את הקואורדינטה הרלוונטית האחת שלהם. סיביות שאתה משאיר בלתי-מוגדרות בתוך מסכה אחרת-תקפה לא מושמטות מהמערך; הן נכתבות כ-null מפורש של PDF, ש-ISO 32000-1 מתייחס אליו כ"שמור על כל ערך שהמציג כבר מחזיק" עבור הקואורדינטה ההיא — דרך לגיטימית לומר "קפוץ לעמוד הזה, השאר את הזום כמו שהוא" ולא פספוס. הזום עצמו שמור כשבר מהערך שאתה מוסר, כך שקריאה שמבקשת 150 אחוז מוסרת למערך ערך שמור של 1.5, וטווח הקלט התקף הוא 0 עד 6400
איך אתה מקשר ל-PDF שמשובץ בתוך המסמך שלך עצמו?
AddLinkToEmbeddedPDF בונה את פעולת ה-GoToE, וארגומנט היעד שלה, EmbeddedFileName, הוא שם ולא נתיב: הוא חייב להתאים למחרוזת ה-Title שכבר נמסרה ל-EmbedFile כשהצירוף נעשה, משום ששם הכותרת ההוא המפתח המילולי ש-PDFlibPas שומרת בעץ-השם /EmbeddedFiles של המסמך, ו-GoToE נפתרת על ידי חיפוש השם ההוא, לא על ידי נגיעה חוזרת במערכת הקבצים. הפונקציה רק בודקת ש-EmbeddedFileName אינו ריק ו-TargetPage הוא לפחות 1 — מסור שם שאף פעם לא באמת שובץ והקריאה עדיין מחזירה הצלחה, הפעולה עדיין נכתבת, והקישור פשוט נכשל בפתרון עבור כל קורא שלוחץ עליו
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.NewPage;
// The Title argument becomes the key PDFlibPas stores in the
// document's EmbeddedFiles name tree -- that string, not
// "datasheet.pdf", is the target GoToE resolves against.
if Lib.EmbedFile('Datasheet', 'datasheet.pdf', 'application/pdf') = 1 then
Lib.AddLinkToEmbeddedPDF(72, 700, 200, 16, 'Datasheet', 3, 0, 0);
Lib.SaveToFile('manual.pdf');
finally
Lib.Free;
end;
end;
שתי רצפות-גרסה נערמות כאן, לא אחת. EmbedFile זקוקה ל-PDF 1.4 עבור עץ-השם /EmbeddedFiles, ו-AddLinkToEmbeddedPDF בנפרד מעלה את הרצפה ל-PDF 1.6 עבור סוג הפעולה GoToE עצמו, כך שהמינימום האפקטיבי עבור כל מסמך שמשתמש בתכונה הזו הוא 1.6, לא 1.4. שים לב גם ש-TargetPage כאן מבוסס-1, מוסכמת PDFlibPas הרגילה — ניגוד מכוון ל-DestPage מבוסס-האפס שהחלק הקודם הרגע כיסה, ותזכורת שאיזו סכימת מספר-עמוד חלה תלויה בסוג-הפעולה והקריאה הספציפית, לא בכלל-גורף אחד. מילון-היעד של הפעולה יכול גם לשאת רשומת /R של C עבור ילד או P עבור הורה, תומך בשרשרת דו-קפיצתית לתוך קובץ משובץ או בחזרה החוצה למכולה שלו, אף על פי ש-AddLinkToEmbeddedPDF אף פעם לא בונה אלא את כיוון-הילד, שכן זה זה שהגיוני ממסמך שמבצע את השיבוץ ולא נשבץ
פעולות Launch: FileName אחד, שני יעדי מחרוזת שאינם ניתנים-להחלפה
SetActionLaunchOptions כותבת את יעד-הקובץ של פעולת Launch לשני מפתחות שונים מארגומנט FileName יחיד, ושני המפתחות מחזיקים שני סוגי מחרוזת שונים. מפתח ה-/F ברמה-העליונה מקבל מילון מפרט-קובץ, נבנה דרך אותה המרת-נתיב ש-PDFlibPas משתמשת בה עבור GoToR, שהיא הצורה הניידת ש-ISO 32000-1 סעיף 7.11.3 מגדיר עבור מילון מפרט-קובץ. תת-המילון /Win, כאשר PDFlibPas כותבת אחד, מקבל את מפתח ה-/F שלו עצמו מוגדר לערך FileName הגולמי בדיוק כפי שנמסר, ללא המרה בכלל, משום ש-/Win /F מתועד ב-ISO 32000-1 סעיף 12.6.4.5 כמחרוזת נתיב Windows פשוטה שנועדה רק למציג Windows לקרוא. מסור נתיב נייד, שכבר הומר, ומצפה ששני המפתחות יסתיימו זהים והעותק /Win יישא כל מה שמסרת לפונקציה, ללא נגיעה
var
Lib: TPDFlib;
ActionID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('manual.pdf', '') = 1 then
begin
Lib.SelectPage(1);
Lib.AddLinkToLocalFile(72, 660, 220, 16, 'calibrate.exe', 0);
ActionID := Lib.GetAnnotActionID(1);
// Operation 0 leaves this as a normal open -- pass 1 to ask a
// Windows viewer to print instead. Parameters and
// DefaultDirectory only ever reach /Win /P and /Win /D, never
// the top-level /F.
Lib.SetActionLaunchOptions(ActionID, 'calibrate.exe',
'/silent /profile:default', 'C:\Tools\Calibration', 0, -1);
end;
finally
Lib.Free;
end;
end;
התייחס ל-Launch כפעולה בעלת-החיכוך-הגבוה ביותר מבין השלושה, שכן כל מטרתה היא הרצת תוכנית או פתיחת קובץ מחוץ ל-sandbox של ה-PDF, וכל מציג מרכזי מתייחס אליה בהתאם. Adobe Acrobat's Enhanced Security חוסמת או שואלת על פעולות Launch כברירת מחדל אלא אם היעד יושב במיקום נאמן במפורש, ורוב פריסות Acrobat ארגוניות משאירות את ההגנה ההיא מופעלת. פעולת Launch במסמך שנמסר לציבור אינה, לכן, טריגר אמין: תכנן שהיא תיחסם, תישאל, או תתעלם בשקט על ידי כל מציג שפותח את הקובץ, ושמור אותה עבור סביבות סגורות שבהן אתה גם שולט בהגדרות-האמון של המציג — קיוסק פנימי, פריסה תאגידית מבוקרת, מסמך שאף פעם לא עוזב מכונה שאתה מנהל
שער ה-PDF/A: למה קריאות GoToR ו-Launch יכולות להחזיר אפס
SetActionRemoteDestinationEx ו-SetActionLaunchOptions שתיהן מסרבות לחלוטין כאשר מסמך היעד נמצא בכל מצב תאימות PDF/A: שתיהן בודקות את מצב ה-PDF/A של המסמך כתנאי הראשון שלהן ויוצאות עם תוצאה של 0 לפני שהן נוגעות בפעולה, ללא חריגה שהועלתה. זה מכוון. ההגבלות של PDF/A על פעולות אינטראקטיביות שוללות את Launch ספציפית, שכן מתן לקובץ ארכיוני את היכולת להריץ תוכנית שרירותית הוא בדיוק סוג ההתנהגות תלוית-הסביבה שפורמטי ארכוב לטווח-ארוך קיימים כדי למנוע, ו-PDFlibPas מיישמת את אותו שער שמרני על קובע ה-go-to המרוחק באותו נתיב קוד. התוצאה המעשית קלה לפספוס במהלך פיתוח: אותה קריאה בדיוק שעובדת על PDF רגיל תתקמפל, תרוץ, ובשקט לא תעשה כלום על מסמך שנטען עם רמת-תאימות PDF/A מוגדרת, כך שבדוק את ערך ההחזרה במקום להניח הצלחה — 0 כאן אינו שגיאת-קלט-פגום, זו הספרייה שמסרבת לבקשה שמתנגשת עם הצהרת-התאימות של המסמך עצמו
איפה GoToR, GoToE, ו-Launch משתלבים בזרימת עבודה גדולה יותר של PDFlibPas
שלושת סוגי-הפעולה במאמר הזה לא כולם מגיעים לאותם מקומות. המאמר הנלווה על טריגרי פעולת מחזור-חיים של מסמך ועמוד מכסה את SetDocumentAction ו-SetPageAction, שיכולות להצמיד פעולת GoToR או Launch לטריגר כמו WillClose דרך הקבועים המשותפים PDF_ACTION_BUILDER_REMOTE_DESTINATION ו-PDF_ACTION_BUILDER_LAUNCH — אותו בונה שגם מכסה טריגר URI או JavaScript פשוט. ל-GoToE אין קבוע כזה ואין נתיב לתוך הבונה הגנרי ההוא בכלל; AddLinkToEmbeddedPDF היא הדרך היחידה ש-PDFlibPas בונה אחת, מה שהופך אותה בהחלט לפעולת נקודה-חמה-בעמוד, אף פעם לא טריגר ברמת-מסמך או ברמת-עמוד. במקום שבו GoToR ו-Launch כן מגיעות לבונה הגנרי, הפשרה היא שליטה: הוא בונה GoToR שמצביעה רק על יעד מרוחק בשם, ופעולת Launch עם רק שם קובץ ופרמטרים, בעוד המיעון המפורש עמוד-וסוג-fit ואפשרויות ה-launch הספציפיות-ל-Windows המכוסות במאמר הזה מגיעות רק דרך SetActionRemoteDestinationEx ו-SetActionLaunchOptions ישירות
תכונת-בטיחות אחת שווה לדעת לפני בניית כלי-תחזוקה סביב הקובעים האלה. SetActionRemoteDestinationEx ו-SetActionLaunchOptions בונות את כל פעולת-ההחלפה במילון-טיוטה קודם, ורק מוחקות ומעתיקות את המפתחות /F, /D או /Win, ו-/NewWindow אל הפעולה החיה ברגע שהעותק-הטיוטה ההוא מאומת — כך שקריאה שנכשלת באימות, בין אם מ-ValueMask מחוץ-לטווח או FileName ריק, משאירה את הפעולה המקורית, וכל שרשרת /Next שכבר תלויה בה, לגמרי ללא נגיעה במקום נכתבת-מעל-חצי. זה חשוב משום שפעולות GoToR ו-Launch שתיהן יכולות לשבת בתוך שרשרת /Next שנבנתה עם AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, או ה-AddActionNextEx הכללי יותר, נותנות לטריגר בודד להפעיל רשומת יומן JavaScript ואז קפיצה מרוחקת ברצף. בניית GoToR, GoToE, ו-Launch כמתואר כאן היא חלק מPDFlibPas, ספריית ה-PDF הילידית עבור Delphi ו-C++Builder