מאמר טכני

מודל האובייקטים הלוגי של PDF: סוגים, הפניות ומבנה

קובץ PDF הוא, ביסודו, אוסף של אובייקטים המצביעים זה על זה. קלפו את הדחיסה, את הנהלת החשבונות של ההפניות הצולבות, ואת היסטי הבתים, ומה שיישאר הוא גרף: סט קטן של ערכים מוקלדים (typed values), מחווטים יחד על ידי הפניות, ששורשם באובייקט יחיד שהקורא יודע כיצד למצוא. כל מה ש-PDF יכול לבטא, מפסקה של טקסט ועד גופן מוטמע או חתימה דיגיטלית, בנוי משמונה סוגי אובייקטים פרימיטיביים והכלל המאפשר לאובייקט אחד להתייחס לאחר. למדו אותם, ושאר הפורמט נקרא כחיבור (composition) ולא כתעלומה

זוהי השכבה הלוגית של PDF, המוגדרת בסעיף 7.3 של ISO 32000-1, והיא יושבת רמה אחת מעל הפריסה הפיזית של הקובץ (הכותרת, הגוף, טבלת ההפניות הצולבות וה-trailer, שהוא נושא בפני עצמו בסקירה הטכנית של מבנה קובץ PDF). המודל הלוגי הוא המשמעות של הבתים הללו לאחר שנותחו. מציג קורא את הקובץ אחורה כדי למצוא את ה-trailer, עוקב אחריו לשורש, ומשם המסמך מתגלה כאובייקטים המפנים לאובייקטים. זה החלק שאתם מסיקים עליו כאשר אתם מדבגים עמוד פגום, כותבים מנתח, או סומכים על ספרייה שתרכיב מסמך

שמונה סוגי אובייקטים, ולא שום דבר אחר

PDF מגדיר בדיוק שמונה סוגי אובייקטים בסיסיים. כל ערך במסמך הוא אחד מהם, וזה מה ששומר על הפורמט נוח לניהול (tractable) למרות היקפו

בוליאנים (Booleans) הם מילות המפתח true ו-false. הם מדליקים ומכבים דגלים, כגון האם הערה מודפסת

מספרים (Numbers) באים בשני טעמים שהמפרט מתייחס אליהם כסוג אחד: שלמים (integers) כמו 42 וממשיים (reals) כמו 3.14 או -0.002. ל-PDF אין סימון מעריך (exponent notation), אז לעולם לא תראו 1e6 בקובץ תואם. קואורדינטות, גדלי גופנים וזוויות סיבוב הם כולם מספרים

מחרוזות (Strings) מחזיקות רצפים של בתים, כתובים בסוגריים, (Hello), או בסוגריים משולשים כהקסדצימליים, <48656C6C6F>. שני הסימונים מקודדים תוכן זהה; הקס (hex) הוא פתח המילוט לבתים בעייתיים בתוך סוגריים. מחרוזות נושאות טקסט, אבל הן בתים בראש ובראשונה, מה שמשנה ברגע שאתם מטפלים בכל דבר מעבר ל-ASCII

שמות (Names) הם אסימונים אטומיים (atomic tokens) המוצגים על ידי לוכסן: /Type, /Pages, /MediaBox. שם אינו מחרוזת; זהו מזהה (identifier), המשמש כמפתח מילון או כערך מונה (enumerated value), ושני שמות שווים רק אם הם תואמים בית מול בית. הלוכסן הוא תחביר, לא חלק מהשם. זה מכשיל מצטרפים חדשים שמתייחסים ל-/Times-Roman ולמחרוזת (Times-Roman) כניתנים להחלפה; הפורמט לא

מערכים (Arrays) הם רשימות הטרוגניות מסודרות בסוגריים מרובעים: [0 0 612 792] הוא מלבן עמוד, ומערך יכול לערבב סוגים בחופשיות, כולל הפניות לאובייקטים אחרים. מילונים (Dictionaries) הם סוס העבודה. כתובים בין << ל->>, מילון ממפה מפתחות שם (name keys) לערכים מכל סוג, וכמעט כל מבנה בעל משמעות ב-PDF, עמוד, קטלוג, גופן, הערה, הוא מילון עם מפתח /Type המצהיר מה הוא

זרמים (Streams) הם מילונים עם זנב של בתים גולמיים (raw bytes) בין מילות המפתח stream ו-endstream. המילון מתאר את הבתים (אורכם, וכל מסנן כמו FlateDecode שדוחס אותם), והבתים נושאים את המטען הגדול: הוראות תוכן העמוד, תוכניות גופנים מוטמעות, תמונות. זרם הוא המקום שבו PDF שם כל דבר גדול מדי או בינארי מדי מכדי לשבת בשורה (inline)

הסוג השמיני הוא האובייקט חסר הערך (null object), מילת המפתח null. זהו ערך אמיתי, שונה מהיעדרו של מפתח. רשומת מילון המוגדרת ל-null מקבלת יחס כאילו אינה קיימת, והפניה שנפתרת לאובייקט שאינו קיים מניבה גם null ולא שגיאה. ההתנהגות הסלחנית הזו היא מכוונת: היא מאפשרת לקובץ פגום לרדת ברמה (degrade) במקום לסרב להיפתח. אין סוג תשיעי; כל מה ש-PDF מבטא נובע מהאופן שבו השמונה הללו משתלבים

ערכים ישירים, אובייקטים עקיפים והפניות

כל אחד משמונת הסוגים הללו יכול להופיע בשתי דרכים. אובייקט ישיר (direct) נכתב במקום, כמו ה-612 בתוך מערך MediaBox. אובייקט עקיף (indirect) מקבל זהות כדי שאובייקטים אחרים יוכלו להצביע עליו: שני מספרים שלמים, מספר אובייקט ומספר דור (generation), העוטפים את ההגדרה ב-obj ו-endobj:

12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj

זהו אובייקט 12, דור 0, מילון גופנים. בכל מקום אחר בקובץ, אובייקט אחר מתייחס אליו עם הפניה עקיפה (indirect reference): אותם שני מספרים ואחריהם מילת המפתח R, 12 0 R. ההפניה היא מצביע (pointer). כאשר מילון משאבים של עמוד אומר /Font << /F1 12 0 R >>, הוא מציין את אובייקט 12 כגופן מאחורי שם המשאב /F1, מבלי להעתיק את הגדרת הגופן לתוך העמוד

מספר הדור קיים עבור מחיקות ושימוש חוזר. כאשר אובייקט מתפנה והמשבצת שלו מנוצלת שוב, הדור עולה ב-1 (increments) כך ש-12 0 R ישן לא יכול להיפתר לדייר החדש של משבצת 12. קבצים שנכתבו זה עתה הם כמעט כולם דור 0, אך קובץ שנערך בכבדות יכול לשאת מספרים גבוהים יותר, ומנתח שמתעלם מהדור יקרא בסופו של דבר את האובייקט הלא נכון

העקיפות (Indirection) היא מה שהופך את ה-PDF ליעיל וניתן לעריכה. גופן, תמונה או מרחב צבעים אחד יכול להיות מוגדר פעם אחת ולהיות מופנה ממאה עמודים. שינוי קטן יכול להיתוסף לסוף (appended) כמהדורה חדשה המחליפה אובייקט בודד במקום לשכתב את הקובץ. טבלת ההפניות הצולבות היא האינדקס שהופך מספר אובייקט להיסט בתים, כך שהקורא קופץ ישר ל-12 0 obj מבלי לסרוק, אבל זו אופטימיזציה פיזית. מבחינה לוגית, כל מה שאתם צריכים לדעת הוא ש-12 0 R פירושו "האובייקט המזוהה כ-12 0."

הקטלוג: היכן שכל מסמך מתחיל

פתרון הפניות חייב להתחיל איפשהו, והאיפשהו הזה הוא רשומת ה-/Root של ה-trailer, המצביעה על קטלוג המסמך (document catalog): שורש גרף האובייקטים, מילון עם /Type /Catalog. הקורא מגיע אליו ראשון משום שה-trailer נמצא ראשון, ומשם כל שאר חלקי המסמך נגישים על ידי מעקב אחר הפניות

הקטלוג נושא רק שתי רשומות חובה ממש: ה-/Type שלו, ו-/Pages, הפניה עקיפה לשורש עץ העמודים (page tree). השאר הן אופציונליות ומתארות התנהגות ברמת המסמך כולו במקום תוכן: /Outlines מצביע על עץ הסימניות, /Names מחזיק עצי שמות עם מפתח לפי מחרוזת, /Metadata מפנה לזרם מטא-נתונים של XMP, ו-/PageMode ו-/PageLayout מציעים למציג כיצד לפתוח את המסמך. אף אחד מאלה אינו נחוץ כדי לרנדר עמוד; הם מגדירים את החוויה סביב העמודים. מבני הסימניות, המטא-נתונים וההערות התלויים על הקטלוג נידונים במאמר על מטא-נתונים, סימניות והערות ב-PDF

התרשים להלן מראה היכן גוף האובייקט יושב בתוך הקובץ המקיף. הקטלוג ועץ העמודים חיים בתוך גוף זה כאובייקטים עקיפים רגילים; הכותרת, טבלת ההפניות הצולבות וה-trailer סביבם הם הפיגומים הפיזיים המאפשרים לקורא לאתר אותם

תרשים של ארבעת החלקים הפיזיים של קובץ PDF: כותרת גרסה, גוף המחזיק את אובייקטי המסמך כולל הקטלוג ועץ העמודים, טבלת הפניות צולבות של היסטי אובייקטים, ו-trailer המצביע על השורש

עץ העמודים: היררכיה מאוזנת של עמודים

מ-/Pages המסמך מתפצל לעץ העמודים, שם משתלמת הבחירה של PDF בגרף על פני רשימה שטוחה. עמודים אינם נשמרים כרצף פשוט; הם תלויים מעץ שהצמתים הפנימיים שלו הם צמתי עץ עמודים (/Type /Pages) ושהעלים שלו הם אובייקטי עמוד (/Type /Page). צומת פנימי מפרט את ילדיו במערך /Kids ומתעד, ב-/Count, כמה עמודי עלה חיים תחתיו. כל צומת מלבד השורש נושא הפניית /Parent חזרה למעלה, כך שהעץ הולך בשני הכיוונים

2 0 obj                                  % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % a leaf page
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

כאן אובייקט 2 הוא השורש, עם שלושה עמודים תחתיו: עמוד העלה 3, ועוד שניים שנגישים דרך צומת פנימי 4. ספירת ה-/Count של השורש, שהיא 3, חייבת להיות שווה לסך העלים שמתחתיו, וספירה שאינה תואמת למבנה בפועל היא דרך נפוצה שבה קובץ שנערך ידנית משתבש. מטרת העץ היא מקומיות של גישה (locality of access). קורא שפותח עמוד 900 במסמך בן אלף עמודים אינו הולך על 900 אובייקטים; הוא יורד דרך קומץ צמתים, מכיוון שעץ בנוי היטב נשאר רדוד ומאוזן. בניית עץ כזה ביד היא מספיק מסובכת כדי שיהיה שווה לראות אותה מקצה לקצה, מה שהמדריך על בניית מסמך PDF מאפס עושה

העץ מצדיק את קיומו בפעם השנייה באמצעות הורשה (inheritance). חופן תכונות של עמוד, /Resources, /MediaBox, /CropBox, ו-/Rotate, יכולות להיות מוגדרות על צומת פנימי ולהישמט מהעמודים הבודדים, אשר אז יורשים את ערכו של האב הקדמון הקרוב ביותר. הגדירו /MediaBox פעם אחת על השורש וכל עלה יקבל את אותו גודל עמוד מבלי לחזור עליו; עמוד שצריך להיות שונה מצהיר על גודל משלו. זה המקום היחיד במודל האובייקטים שבו משמעותו של ערך תלויה במיקומו של אובייקט בעץ, לא רק בתוכן שלו עצמו

מה בעצם מחזיק עמוד עלה

אובייקט עמוד (page object) הוא נקודת החיבור בין המודל המבני לתוכן הנראה לעין. רשומת ה-/Contents שלו מפנה לזרם תוכן אחד או יותר, אופרטורי הציור שמציירים טקסט וגרפיקה על העמוד. מילון ה-/Resources שלו משיים את הגופנים, התמונות, ומרחבי הצבע שאופרטורים אלו מסתמכים עליהם, כל רשומה היא הפניה עקיפה לאובייקט המשותף למספר עמודים. ה-/MediaBox נותן את מלבן העמוד בנקודות (1/72 אינץ'), ורשומות כמו /Rotate ו-/CropBox מכווננות כיצד הוא מוצג

חלוקת העבודה הזו היא המודל כולו בזעיר אנפין (in miniature). מילון העמוד הוא מבנה: רשומות מוקלדות (typed entries) והפניות שאומרות מהו העמוד ועם מה הוא מצייר. זרם התוכן הוא הוראות: גוש נפרד ודחיס שאומר כיצד לצייר. הגופן שמאחורי /F1 הוא משאב משותף, המוגדר פעם אחת ומופנה בכל מקום בו משתמשים בו. מילון, זרם והפניה משתפים פעולה כדי לרנדר עמוד אחד, ואותן תבניות מתרחבות לכל המסמך. אופרטורי זרם התוכן בתוך אותו גוש (blob) מכוסים בנפרד עבור טקסט וגופנים ועבור גרפיקה ואלמנטים ויזואליים

מדוע מודל זה שווה הכרה

רוב המפתחים פוגשים את מודל האובייקטים רק כאשר משהו נשבר: עמוד מרונדר ריק כי הפניית ה-/Contents שלו תלויה (dangles), טקסט יוצא כקופסאות כי משאב גופן מעולם לא הוטמע, כלי מדווח על /Count שלא תואם לעמודים שהוא יכול למצוא. כל אחד מאלה הוא הצהרה על הגרף, וקריאת הגרף ישירות עדיפה על ניחוש. שמונת הסוגים וכלל ההפניה הם אוצר מילים קטן מספיק כדי להחזיק בראש, וברגע שאתם רואים PDF כאובייקטים המצביעים על אובייקטים, קבצים פגומים מפסיקים להיות אטומים (opaque)

עם זאת, כתיבת המודל ביד היא נדיר מאוד שזו הבחירה הנכונה מעבר ללמידה. שמירה על היסטי הפניות צולבות, מספרי דורות, ספירות עץ עמודים ואורכי זרמים עקביים לאורך עריכות היא סוג הנהלת החשבונות שספרייה קיימת כדי לטפל בה. בייצור, ספריית פיתוח PDF בשלה מנהלת את גרף האובייקטים תוך השארתכם לחשוב במונחים של עמודים ותוכן. הכרת המודל עדיין משתלמת: אתם מבינים מה הספרייה בונה מתחת, ומדוע