הסירו את תיאורי העמודים ותישארו עם שכבה דקה של מבנה שאף אחד לא מדפיס אך כל קורא, מפתח אינדקסים, ומערכת ארכיונים תלויים בה. אובייקט עמוד אינו יודע דבר על הפרק אליו הוא שייך, המחבר שכתב אותו, או הערת השוליים שמקשרת למקום אחר. ידע זה קיים רמה אחת למעלה, בשלושה מבנים המחוברים לקטלוג המסמך (document catalog): זרמי המטא-נתונים, עץ המתאר (outline tree), ומערכי ההערות פר-עמוד. הם חולקים תכונה שהופכת אותם לקלים לשגיאה. אף אחד מהם אינו נושא סימנים נראים לעין על העמוד, כך שקובץ יכול להיות מרונדר בצורה מושלמת ועדיין להיות חסר את הסימניות שלו, לסתור את שדה המחבר של עצמו, או להצביע בקישור לאובייקט עמוד שכבר אינו קיים
זוהי השכבה שספריית PDF חושפת כמאפייני מסמך (document properties), ממשקי תכנות יישומים (APIs) של סימניות, וקריאות לקישורים או הערות, והשכבה שסורק חיפוש (search crawler) קורא כדי להחליט על מה המסמך שלכם. מודל האובייקטים שמתחת מכוסה בהדרכה על מבנה מסמך PDF. כאן המיקוד הוא אך ורק במה שתלוי מהקטלוג
כל שלושת המבנים מתחברים בקטלוג. קטלוג שלם המחווט אותם יחד נראה כך:
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
/Outlines 3 0 R
/Names << /EmbeddedFiles 4 0 R >>
/Metadata 5 0 R
>>
endobj
ארבע רשומות, ארבע מערכות משנה בלתי תלויות. /Pages הוא המסמך הנראה לעין; /Outlines הוא עץ הסימניות; /Metadata מצביע על זרם ה-XMP; /Names מגיע למילון השמות הכלל-מסמכי, אשר בין היתר מחזיק קבצים מצורפים מוטמעים. כל אחד מהם אופציונלי, וקורא שלא מוצא אף אחד מהם עדיין מציג את העמודים. אותה אופציונליות היא בדיוק הסיבה לכך ששכבת הניווט היא הדבר הראשון שמרקיב כאשר קובץ נערך על ידי כלים שמבינים רק עמודים
שני מאגרי מטא-נתונים שאינם מסכימים
PDF נושא מטא-נתונים של מסמך בשני מקומות בבת אחת, והצרות מתחילות כשהם אומרים דברים שונים. המנגנון המקורי הוא מילון פרטי המסמך (document information dictionary), המופנה על ידי /Info ב-trailer: קבוצה שטוחה של צמדי מפתח-ערך עבור /Title, /Author, /Subject, /Keywords, /Creator, /Producer ושני התאריכים. זה פשוט וכל מציג קורא אותו. PDF 2.0 מוציא משימוש (deprecates) את רובו לטובת המנגנון השני, זרם מטא-נתונים XMP
XMP הוא מסמך XML עצמאי, כתוב ב-RDF, המאוחסן כזרם אליו מגיע הקטלוג דרך /Metadata ומסומן /Type /Metadata /Subtype /XML. בניגוד למילון ה-Info הקבור בתוך מבנה אובייקט ה-PDF, חבילת (packet) XMP מתוכננת להיות מחולצת ומנותחת בפני עצמה על ידי כלים שאינם יודעים דבר על PDF. הנה חבילה מייצגת:
5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
<xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
<xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
<pdf:Producer>losLab PDF Library</pdf:Producer>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj
שלושה פרטים בבלוק זה מכריעים האם המטא-נתונים שורדים מפגש עם כלים אמיתיים. הוראות העיבוד של ה-xpacket אינן לקישוט: הן ממסגרות את החבילה כך שמחלץ (extractor) יוכל למצוא אותה בתוך זרם בתים גדול יותר, וכותב שמשמיט את הסגירה <?xpacket end="w"?> מייצר קובץ שנפתח בסדר אבל מכשיל מאמתים (validators) קפדניים. גם סוגי הנתונים של המאפיינים (property datatypes) חשובים. dc:title הוא חלופה שפתית העטופה ב-rdf:Alt, בעוד ש-dc:creator הוא רשימה מסודרת ולוקח rdf:Seq; פליטה של כל אחד מהם כצומת טקסט חשוף היא השגיאה הנפוצה ביותר ב-XMP, שנסבלת על ידי רוב המציגים עד לאחד שלא. קידומות מרחב השמות (namespace prefixes) הן מוסכמות, אבל מזהי המשאבים (URIs) שאליהם הם נקשרים הם תקניים: מנתח מזהה על פי ה-URI, לא הקידומת
הכלל הנוקשה עם שני מאגרים הוא שהם חייבים להסכים. אם /Info אומר שהמחבר הוא אדם אחד ו-dc:creator נותן שם של אדם אחר, שלחתם מסמך שעונה על אותה שאלה בשתי דרכים, ואיזו תשובה מנצחת תלוי באיזה שדה קורא הכלי הצורך. ספרייה בדרך כלל כותבת את שניהם עבורכם, אבל ברגע שאתם עורכים אחד מהם ידנית, או ממזגים קבצים ממחוללים שונים, השניים מתרחקים זה מזה. התייחסו למילון ה-Info כתאימות למערכות מיושנות (legacy compatibility) ול-XMP כמקור האמת, וצרו את שניהם מחדש ממערך אחד של ערכים במקום לתקן אותם באופן עצמאי. עבור PDF/A זה הופך לדרישת תאימות: ISO 19005 מחייב XMP ואוסר על כל מאפיין Info שסותר את מקבילו ב-XMP
עץ המתאר שמאחורי חלונית הסימניות
מה שמציג מראה כחלונית סימניות הוא, בקובץ, עץ של מילונים המקושרים דו-כיוונית (doubly linked tree) ונקרא מתאר המסמך (document outline). הקטלוג מצביע על מילון שורש מתאר דרך /Outlines; השורש מצביע על פריטי הרמה העליונה הראשונים והאחרונים שלו; וכל פריט קשור לשכניו ולהורה שלו. אין מערך של סימניות בשום מקום. כל המבנה משוחזר על ידי מעקב אחר הפניות, וזו בדיוק הסיבה שקישור שבור בחיבור בודד יכול לגרום לענף שלם להיעלם מהחלונית ללא כל שגיאה
8 0 obj % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj % top-level: a chapter
<< /Title (Chapter 1: Results)
/Parent 8 0 R /Count 2
/First 12 0 R /Last 15 0 R >>
endobj
12 0 obj % first child
<< /Title (Introduction)
/Parent 9 0 R /Next 15 0 R
/Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj % second child, last sibling
<< /Title (Methodology)
/Parent 9 0 R /Prev 12 0 R
/Dest [3 0 R /Fit] >>
endobj
קראו את הקישורים והאינווריאנטים (invariants) הופכים ברורים. כל פריט מצביע בחזרה אל ה-/Parent שלו. אחים (Siblings) יוצרים שרשרת דרך /Prev ו-/Next, כאשר הפריט הראשון משמיט את /Prev והאחרון משמיט את /Next. הורה נוקב בשמות ילדיו הראשון והאחרון דרך /First ו-/Last, והילדים שבאמצע נגישים רק על ידי הליכה לאורך שרשרת האחים. טעו באחד והכישלון יהיה שקט: /Next מיושן חותך פרק, הורה שה-/Last שלו לא מסיים את השרשרת משאיר פריטים יתומים, והמציג מרנדר כל מה שהוא יכול להגיע אליו
שדה /Count נושא חלק של מצב (state) שמפתיע אנשים. על השורש ועל כל פריט מורחב הוא מחזיק את מספר הצאצאים הנראים כרגע; על פריט מכווץ הוא מספר שלילי שגודלו הוא כמה צאצאים יופיעו בעת הרחבה. אז /Count אינו עובדה מבנית קבועה על העץ, הוא המצב השמור, הפתוח או הסגור, של החלונית, ומחולל שמקודד אותו קשיח כסך הכל חיובי פותח מחדש כל ענף שהמחבר התכוון להשאיר סגור
כל פריט מרוויח את מקומו בכך שהוא מצביע למקום כלשהו. /Title הוא מה שהחלונית מציגה; /Dest הוא היכן שלחיצה נוחתת. יעד (destination) יכול להיות מובנה בפריט, כפי שמוצג למעלה, או שם שנפתר דרך מילון השמות של המסמך, שזו הבחירה הטובה יותר כאשר סימניות וקישורים רבים מכוונים לאותן נקודות, מכיוון שאתם מתקנים יעד שהוזז במקום אחד. ספרייה בדרך כלל מסתירה עץ זה מאחורי ידית (handle) של outline-root ומתודות שמוסיפות רשומות ילדים; ב-HotPDF המסמך חושף OutlineRoot מסוג THPDFDocOutlineObject ומשחיל עבורכם את הקישורים /Prev, /Next, /Parent ו-/Count ככל שאתם מצרפים פריטים. שווה לנצל זאת, מכיוון שתחזוקה ידנית של האינווריאנטים הללו לאורך עריכות היא המקום שבו מתארים (outlines) נשברים
יעדים (Destinations): הדקדוק של לאן מגיעה לחיצה
הן סימניות והן הערות קישור מצביעות ליעדים, ויעד הוא יותר מאשר מספר עמוד. זהו מערך שקורא לאובייקט עמוד ואז מציין, באמצעות פועל בחריץ (slot) השני, כיצד על המציג למסגר אותו. הנפוץ ביותר והמנוצל לרעה ביותר הוא /XYZ, מהצורה [page /XYZ left top zoom]. שלושת האופרנדים שלו הם בלתי תלויים, וכל אחד מהם יכול להיות null שמשמעותו "השאר זאת כפי שהיה לקורא." לכן [page /XYZ null null null] קופץ לעמוד מבלי לגעת במיקום הגלילה או בזום, בדרך כלל מה שתרצו מקישור "עבור לעמוד". המספרים נמצאים במרחב המשתמש (user space) ברירת המחדל, נמדדים מהחלק התחתון-שמאלי כשה-y עולה כלפי מעלה, אותה מערכת קואורדינטות בה משתמש תוכן העמוד. מחברים המגיעים מעיצוב מסך (screen layout) מודדים באופן רפלקסיבי מלמעלה ושולחים את הקורא לקצה הלא נכון של העמוד
משפחת /Fit מחליפה מיקום מדויק בעמידות (resilience). [page /Fit] מתאים את קנה המידה של העמוד כולו לחלון, [page /FitH top] מתאים לרוחב העמוד עם קצה עליון נתון, ו-[page /FitR l b r t] עושה זום למלבן כדי למלא את התצוגה. מכיוון שאלו מחשבים את קנה המידה מתוך הגיאומטריה של העמוד ולא מקואורדינטות קבועות, יעד /Fit עדיין יעשה את הדבר ההגיוני לאחר שינוי גודל העמוד, בעוד שיעד /XYZ עם זום אפוי-פנימה יכול להשאיר את הקורא בוהה בשוליים. עבור תוכן עניינים, /FitH עם הקואורדינטה העליונה של הסעיף מזדקן טוב יותר מאשר /XYZ עם זום מנוחש
הערות (Annotations): כל דבר אינטראקטיבי שאינו תוכן העמוד
הערה היא אובייקט המונח על פני העמוד (overlays the page) מבלי להיות חלק מזרם התוכן שלו. קישורים, פתקיות נדבקות (sticky notes), הדגשות (highlights), ווידג'טים של טפסים, סמלים של קבצים מצורפים, חותמות: כולם הערות, המפורטות במערך /Annots של העמוד שעליו הן יושבות. הסרת הערה ממערך זה מסירה אותה מהעמוד למרות שהתוכן שתחתיה ללא פגע. זה כל העניין: הערות הן שכבת עריכה, נפרדת מהסימנים שעליהם הן יושבות
כל הערה חולקת חוט שדרה קטן. /Subtype מציין את סוגה, /Rect נותן את התיבה התוחמת שלה (bounding box) בקואורדינטות עמוד, ו-/Contents מחזיק טקסט המשמש גם כתיאור נגיש (accessible description). הערת הקישור היא המקרה ששווה ללמוד, מכיוון שהיא מגיעה בשתי צורות: יעד חשוף, ופעולה (action)
12 0 obj % link to a destination
<< /Type /Annot /Subtype /Link
/Rect [100 200 300 250]
/Border [0 0 0]
/Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj % link that runs an action
<< /Type /Annot /Subtype /Link
/Rect [50 50 200 100]
/Border [0 0 0]
/A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj
ה-/Rect הוא נקודה חמה (hotspot); לחיצה בתוכו שולחת את הקורא ליעד, ועושה שימוש חוזר באותו הדקדוק בו משתמש המתאר (outline). /Border [0 0 0] עושה עבודה אמיתית, ומדכא את מלבן ברירת המחדל המכוער שמציגים מציירים סביב קישורים. הצורה השנייה מחליפה את ה-/Dest החשוף לפעולת /A, אשר תת-הסוג שלה /S בוחר את ההתנהגות: /GoTo בתוך קובץ זה, /GoToR לקובץ אחר, /URI לכתובת רשת, /Launch כדי להפעיל תוכנה חיצונית. האחרון מצדיק חשדנות. /Launch שמפעיל קובץ הרצה (executable) הוא ההתנהגות שהופכת מסמכי PDF לווקטור עבור תוכנה זדונית (malware), ולכן מציגים תואמים חוסמים אותו או מתריעים בקול רם, והקישור נכשל עבור רוב הקוראים. השתמשו ב-/URI וב-/GoTo והניחו ל-/Launch
הערות סימון (Markup annotations) כגון הדגשות ופתקיות נדבקות, והערות צורה כגון /Square, מוסיפות קמט (wrinkle): המראה שלהן על המסך אינו משתמע מסוגן. מציג ירנדר גרסה משלו אלא אם כן תקבעו (pin) את המראה באמצעות זרם הופעה (appearance stream), רשומת /AP, המפנה ל-form XObject המחזיק את אופרטורי הציור. דלגו על כך ואותה הדגשה יכולה להיראות שונה בשני קוראים, או לפני ואחרי כניסה ויציאה מעורך. עבור כל דבר שהמראה המדויק שלו הוא חלק מהמסמך, ספקו את ה-/AP. קבצים מצורפים, אגב, עושים שימוש חוזר באותו מנגנון: זרם קובץ מוטמע ומילון מפרט קובץ, הנחשפים בין כהערת /FileAttachment או דרך עץ השמות /EmbeddedFiles תחת רשומת ה-/Names של הקטלוג
היכן שכבה זו נשברת, וכיצד לתפוס זאת
הכשל החוזר ונשנה לרוחב כל אלה הוא ההפניה התלויה (dangling reference). סימניות מפסיקות להופיע כאשר לקטלוג אין רשומת /Outlines או כששרשרת אחים נשברת באמצע העץ; מטא-נתונים נדחים כאשר בזרם ה-XMP חסר סימון /Type /Metadata /Subtype /XML או שעטיפת ה-xpacket פגומה. בכל המקרים תוכן העמוד תקין, ולכן פתיחה אקראית נראית נכונה והפגם צף רק בחלונית שאף אחד לא בדק
שני הרגלים זולים יתפסו את הרוב. פתחו את הקובץ המוגמר במציג אמיתי ולחצו על פני חלונית הסימניות ומדגם של קישורים, מה שמתרגל את גרף ההפניות כפי שיעשה קורא. לאחר מכן קראו את המטא-נתונים בחזרה עם כלי נפרד ואשרו שמילון ה-Info וה-XMP מסכימים, אי-ההסכמה היחידה ששום כמות לחיצות לא תחשוף. חוללו שכבה זו באמצעות ספרייה שבבעלותה ניהול הקישורים ורוב המלכודות הללו לעולם לא ייפתחו. רכיב ה-HotPDF Component עבור Delphi ו-C++Builder חושף את מבני המתאר, ההערות והמטא-נתונים באמצעות APIs ברמת המסמך, כך שאתם מתארים את היררכיית הסימניות והקישורים ונותנים לו להשחיל את ההפניות. לגבי מודל האובייקטים אליו מחוברים מבנים אלה, הסקירה הטכנית של מבנה קובץ PDF מכסה את הקטלוג וטבלת ההפניות הצולבות עליהם הם מסתמכים