למילון ה-PDF Catalog יש בדיוק מפתח ניווט אחד נדרש: /Pages. מפתח זה חייב להצביע לאובייקט עקיף מסוג /Pages, אשר בתורו מחזיק את מערך ה-/Kids ואת ספירת הדפים הכוללת (/Count). הסר את המצביע הזה ושום קורא תואם לא יוכל לאתר אף דף אחד בקובץ. תקן ISO 32000-1 §7.7.2 אינו משתמע לשתי פנים בנקודה זו: לקטלוג יהיה ערך /Pages, והאובייקט אליו יש הפניה יהיה מסוג /Pages. קבצים שמפרים דרישה זו אינם רק בלתי-תואמים; הם שבורים מבנית באופן שרוב המנתחים (parsers) מטפלים בו בצורה גרועה
מה שהמפרט אומר בפועל
PDF תואם מינימלי מכיל לפחות שלושה אובייקטים. אובייקט 1 הוא ה-Catalog, אובייקט 2 הוא שורש ה-Pages, ואובייקט 3 ואילך הם מילוני Page בודדים. ה-Catalog מצביע לשורש ה-Pages; שורש ה-Pages מפרט את ילדיו ב-/Kids; כל Page נושא הפניה לאחור /Parent. השרשרת כולה היא דו-כיוונית במכוון, כך שמנתח יכול להתחיל מכל קצה ולעבור לכל דף בזמן O(log n) עבור עצים מאוזנים
% מבנה תואם מינימלי (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
עץ ה-Pages יכול להיות מקונן. מסמך בן אלפי דפים מקבץ בדרך כלל דפים לאובייקטי צומת ביניים הנושאים גם הם את הסוג /Pages, כל אחד עם /Kids משלו ו-/Count המשקף את תת-העץ תחתיו. ה-/Count של צומת השורש שווה תמיד לספירת הדפים הכוללת. ספירה זו היא מה שמציגים מראים בשדה מספר-הדף לפני שניתחו אפילו דף אחד, מכיוון שקריאת מספר שלם אחד מאובייקט 2 זולה בהרבה ממעבר על העץ כולו
איך נראה קובץ ללא Pages
קבצים חסרי מילון Pages מקורם בדרך כלל ממחוללי PDF שכותבים אובייקטי דפים ישירות ללא הרכבתם לעץ, או מהשחתה המסירה את צומת השורש תוך השארת אובייקטי ה-Page שהם עלים שלמים. ה-Catalog בקובץ כזה חסר את מפתח ה-/Pages לחלוטין, או שהוא מחזיק הפניה לאובייקט שאינו קיים יותר בטבלת ההפניות המקושרות (cross-reference)
% לא תואם: Catalog ללא הפניית /Pages
1 0 obj
<< /Type /Catalog >>
endobj
% אובייקטי דף קיימים אך אינם נגישים מתוך ה-Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj
25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj
מנתח שעוקב אחר המפרט יקרא את ה-Catalog, ינסה לפתור את /Pages, לא ימצא דבר (או הפניה מתה), ויעלה שגיאה או ידווח על אפס דפים. מה שאסור לו לעשות הוא להמשיך כאילו בקובץ היו אפס דפים ולהצליח בשקט; הדבר מפיק פלט ריק שנראה נכון לכלים אוטומטיים ושגוי לכל אדם שפותח אותו
מדוע מנתחים (parsers) קורסים
רוב מנתחי ה-PDF מקצים את טבלת הדפים הפנימית שלהם בזמן הטעינה על סמך ערך ה-/Count משורש ה-Pages. כאשר שורש זה חסר, המנתח קורא אפס, אינו מקצה דבר, ולאחר מכן מבצע dereference למצביע null בפעם הראשונה שקוד כלשהו מבקש את דף 1, או שהוא קורא זבל ומקצה חוצץ (buffer) שגוי לחלוטין. אף אחת מהתוצאות אינה אלגנטית. הפרת הגישה (access violation) בכתובת 0x008E5D78 שמופיעה ביומני קריסה מעיבוד קובץ כזה היא בדיוק זה: dereference של מצביע null בתוך נתיב הגישה לדף, המופעל על ידי היעדר המבנה שהמנתח הניח שתמיד יהיה שם
הנחת התכנון הבסיסית היא סבירה. לרוב המכריע של קובצי ה-PDF הקיימים יש מילון Pages. מנתחים שמדלגים על בדיקת הקיום כדי לחסוך כמה הוראות אינם פזיזים; הם מבצעים אופטימיזציה עבור המקרה הנפוץ. הקבצים שמענישים על אופטימיזציה זו נדירים מספיק כך שקוד ייצור (production code) לעולם לא ייתקל בהם עד שהוא כן, ובשלב זה הקריסה היא גם ניתנת לשחזור וגם מבלבלת אם המהנדס לא קרא את §7.7.2
התאוששות ללא עץ Pages
אם מנתח חייב לטפל בקבצים אלה במקום לדחות אותם, ההתאוששות עוקבת אחר נתיב צפוי: סרוק כל אובייקט עקיף בטבלת ה-cross-reference, אסוף את אלה עם /Type /Page, ומיין אותם לפי מספר אובייקט. סדר מספרי אובייקטים לא מובטח שיתאים לסדר הקריאה במפרט, אבל בפועל, מחוללים (generators) שמשמיטים את עץ ה-Pages נוטים להפיק דפים ברצף, כך שסדר מספרי האובייקטים נכון ברוב המקרים
הבדיקה עצמה זולה. לפני מעבר על מצביע ה-/Pages של ה-Catalog, אשר כי המצביע קיים, שהוא מוביל לאובייקט אמיתי, ושה-/Type של האובייקט שנקרא שווה ל-/Pages. אם אחד משלושת התנאים הללו נכשל, עבור לסריקה הליניארית. הסריקה איטית יותר ממעבר על העץ עבור מסמכים גדולים, משום שהיא קוראת כל כותרת אובייקט במקום לעקוב אחר נתיב מאוזן, אבל זה עובד, ועבור קובץ שכבר אינו תקין (malformed), נכונות גוברת על מהירות
מקרה קצה אחד שסריקה ליניארית אינה פותרת אוטומטית: סדר דפים. ללא מערך /Kids להגדרת הרצף, הסדר ה"נכון" אינו מוגדר על ידי המפרט. סדר מספרי אובייקטים הוא ברירת המחדל הפרגמטית; אם הקובץ חשוב מספיק כדי לעבד אותו בקפידה, בדיקה האם אובייקטי ה-Page נושאים /StructParents מפורש או הפניות הערה (annotation references) המרמזות על רצף קריאה שווה את העבודה הנוספת
השלכות עבור מחוללי (generators) PDF
עבור כל מי שכותב מחולל PDF במקום מנתח (parser), הלקח צר: הפק תמיד את שורש ה-Pages לפני סגירת הקובץ. Catalog ללא ערך /Pages אינו PDF תקף תחת אף מהדורה של המפרט. מחוללים שבונים אובייקטי דף בזמן ריצה ומרכיבים את העץ בשלב הסיום (הגישה שרוב כותבי הזרמת-הנתונים משתמשים בה) הם בסדר כל עוד שלב הסיום אכן פועל. אופן הכשל הנפוץ הוא חריגה (exception) או חזרה מוקדמת המבטלת את הכתיבה לפני שה-trailer הושלם, ומשאירה מאחור קובץ שנפתח בחלק מהמציגים (שיש להם היוריסטיקות התאוששות) ונכשל באחרים (שאין להם)
PDF/A ו-PDF/UA מטילים אילוצים נוספים על עץ הדפים מעבר למה שהמפרט הבסיסי דורש, אך אף אחד מהם אינו מקל בדרישת ה-/Pages. כלי אימות שבודק תאימות ל-ISO 19005 או ל-ISO 14289 יתפוס מילון Pages חסר כהפרה של המפרט הבסיסי לפני שהוא בכלל מגיע לכללים הספציפיים לפרופיל