PDF הוא ביסודו קונטיינר של טקסט רגיל (plain-text). פתחו רוב קבצים אלו בעורך הקסדצימלי והחלק העליון יהיה קריא: הערת גרסה, ואז רצף של אובייקטים ממוספרים, ואז אינדקס קטן ומצביע ממש בתחתית שאומר לקורא מאיפה להתחיל. קלפו את הדחיסה והפורמט נגיש מספיק כדי שתוכלו להקליד מסמך עובד לתוך עורך טקסט ולגרום למציג לפתוח אותו. עשיית הדבר פעם אחת מלמדת אתכם יותר על האופן בו PDF מחזיק את עצמו ביחד מאשר כל כמות של קריאת המפרט, מכיוון שאתם חייבים לחווט את האובייקטים זה לזה ביד, והקובץ מסרב להיפתח עד שתעשו את החיווט נכון
מדריך זה בונה את ה-PDF הקטן ביותר שלמעשה מרנדר משהו: עמוד אחד, המילים "Hello, World!" בגופן מובנה, על נייר US Letter. הקובץ המוגמר דורש בדיוק חמישה אובייקטים וכמה שורות של הנהלת חשבונות (bookkeeping) סביבם. נכתוב את האובייקטים תחילה, ואז נרכיב את הכותרת (header), טבלת ההפניות הצולבות (cross-reference table), וה-trailer שמאגדים אותם לקובץ שקורא יקבל
חמשת האובייקטים שמציג מתעקש עליהם
קורא אינו סורק PDF מלמעלה למטה בחיפוש אחר תוכן. הוא מתחיל ב-trailer, עוקב אחר הפניה לקטלוג המסמך (document catalog), והולך לאורך שרשרת של אובייקטים משם. כל אובייקט בשרשרת זו חייב להיות קיים, אחרת הפתיחה נכשלת. עבור מסמך בן עמוד אחד השרשרת קצרה, ולכל חוליה יש תפקיד יחיד:
- קטלוג (Catalog) הוא השורש. זהו האובייקט שה-trailer מצביע עליו, והרשומה הנדרשת היחידה שלו כאן היא הפניה לעץ העמודים (page tree)
- עמודים (Pages) הוא צומת עץ העמודים. הוא מפרט את העמודים במסמך ומדווח כמה מהם יש
- עמוד (Page) מתאר עמוד פיזי אחד: גודלו, המשאבים שהוא מצייר איתם, ואיזה זרם תוכן (content stream) מצייר אותו
- זרם תוכן (Content stream) מחזיק את אופרטורי הציור, פקודות ה-postfix המניחות טקסט וגרפיקה על אותו עמוד
- גופן (Font) מצהיר על הגופן שאליו זרם התוכן מתייחס. השתמשו באחד מ-14 הגופנים הסטנדרטיים ולא תצטרכו להטמיע דבר
כל אובייקט ממוספר וניתן לפנות אליו (addressable). אובייקט עקיף (indirect object) נכתב כ-N 0 obj ... endobj, כאשר N הוא מספר האובייקט וה-0 הוא מספר הדור שלו (תמיד 0 בקובץ שאתם כותבים מחדש). בכל מקום אחר בקובץ אתם מצביעים על אובייקט זה באמצעות הפניה: 5 0 R פירושו "אובייקט 5." ההפניות הללו הן החיווט. הקטלוג מחזיק ב-2 0 R במיספור שלנו כדי להגיע לעץ העמודים, עץ העמודים מחזיק הפניה בחזרה למטה אל העמוד, וכן הלאה. טעו במספר, והקורא יעקוב אחרי מצביע תלוי (dangling pointer) אל הכלום
שמות, מילונים וזרמים
שלושה חלקי תחביר נושאים כמעט הכל. שם (name) מתחיל בלוכסן: /Type, /Page, /F0. שמות הם מזהים רגישים לרישיות (case-sensitive), לא מחרוזות, ו-PDF משתמש בהם כמפתחות למילון וכדי לתייג מהו אובייקט. מילון (dictionary) הוא אוסף של צמדי מפתח-ערך עטופים בסוגריים משולשים כפולים, כאשר כל מפתח הוא שם: << /Type /Page /MediaBox [0 0 612 792] >>. הערכים יכולים להיות מספרים, שמות, מערכים בסוגריים מרובעים, הפניות או מילונים מקוננים. רוב אובייקטי ה-PDF הם מילונים
זרם (stream) הוא מילון שלאחריו בלוק של בתים בין מילות המפתח stream ו-endstream. שם חיים אופרטורי הציור של העמוד, ובקבצים אמיתיים שם חיים גם תמונות דחוסות וגופנים מוטמעים. מילון הזרם מתאר את הבתים; בקובץ ייצור (production file) עליו לשאת רשומת /Length הנותנת את ספירת הבתים המדויקת, ולעתים קרובות /Filter כגון /FlateDecode כאשר הנתונים דחוסים. אנו נישען על כלי שימלא את /Length, מכיוון שספירת בתים ידנית היא החלק בתרגיל זה שאין לו שום תועלת חינוכית ויש בו סיכוי גבוה לשגיאת off-by-one שתשבור את הקובץ
כתיבת האובייקטים
הנה חמשת האובייקטים לפי הסדר. פרט הקואורדינטות שכדאי לזכור לפני קריאת זרם התוכן: PDF מודד מהפינה השמאלית התחתונה של העמוד בנקודות (points), כאשר נקודה אחת היא 1/72 אינץ', ו-Y גדל כלפי מעלה. עמוד US Letter הוא 612 על 792 נקודות, ולכן 50 700 יושב קרוב לפינה השמאלית העליונה, לא בתחתית
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F0 4 0 R >> >>
/Contents 5 0 R
>>
endobj
4 0 obj
<< /Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj
קראו את ההפניות והמבנה מתבהר. אובייקט 1, הקטלוג, מפנה את רשומת ה-/Pages שלו לאובייקט 2. אובייקט 2, עץ העמודים, מפרט את אובייקט 3 ב-/Kids ומצהיר על /Count 1. אובייקט 3, העמוד, מצביע מ-/Parent בחזרה למעלה אל אובייקט 2 (העץ והעמוד מפנים זה לזה, דבר הכרחי), מגדיר את גודלו עם /MediaBox, חושף את הגופן תחת השם המקומי /F0 ב-/Resources שלו, ונותן את שם אובייקט 5 כתוכן שלו. אובייקט 4 הוא הגופן: /BaseFont /Helvetica בוחר באחד מ-14 הגופנים הסטנדרטיים שכבר יש לכל קורא תואם, כך שאין מה להטמיע. אובייקט 5 הוא זרם התוכן
מה בעצם אומר זרם התוכן
גוף הזרם הוא תוכנית זעירה בשפת תיאור העמודים של PDF, שהיא postfix: אופרנדים (operands) באים קודם, ואז האופרטור (operator) שצורך אותם. חמש שורות עושות את העבודה. BT ו-ET פותחים וסוגרים אובייקט טקסט; כל מה שממקם או מציג טקסט חייב לשבת ביניהם. /F0 36 Tf מגדיר את הגופן הנוכחי למשאב ששמו /F0 בגודל 36 נקודות (Tf הוא "set text font and size"). 50 700 Td מזיז את מיקום הטקסט ל-(50, 700) בקואורדינטות העמוד. (Hello, World!) Tj מציג את המחרוזת, ש-PDF כותב כטקסט ליטרלי בסוגריים, תוך שימוש ב-Tj כדי לצייר אותה במיקום הנוכחי. השמיטו את BT/ET וקורא קפדן ידחה את אופרטורי הטקסט; שכחו להגדיר גופן לפני Tj ואין גופן נוכחי לצייר איתו
ה-/Length 44 במילון הזרם הוא ספירת הבתים בין stream ל-endstream, והוא חייב להיות מדויק. זהו הערך ששווה למסור לכלי במקום לספור ירידות שורה באופן ידני, במיוחד מכיוון שהשאלה האם העורך שלכם כותב סיומי שורות כ-LF או כ-CRLF משנה את הסך הכל
Header, xref ו-trailer
האובייקטים הם התוכן. שלושה חלקים מבניים הופכים אותם לקובץ. הראשון הוא הכותרת (header), השורה הראשונה ממש, שקוראת בשם לפורמט ולגרסה:
%PDF-1.7
ה-% מתחיל הערה בתחביר PDF, אבל קורא מתייחס להערה הספציפית הזו כאל חתימת הפורמט וקורא ממנה את הגרסה. כותב אמיתי מצרף אליה מיד שורת הערה שנייה של בתים מסוג high-bit, רמז לכלי העברת קבצים שהקובץ הוא בינארי ואין להשחית אותו כטקסט
בסוף הקובץ מגיעה טבלת ההפניות הצולבות (xref), האינדקס שמאפשר גישה אקראית. היא מתעדת את היסט הבתים (byte offset) של כל אובייקט מתחילת הקובץ, כך שקורא יכול לקפוץ ישר לאובייקט 3 מבלי לנתח קודם את אובייקטים 1 ו-2. הטבלה נוקשה: הרשומות הן ברוחב קבוע, 20 בתים כל אחת כולל סיום השורה, מעוצבות כהיסט בן 10 ספרות, דור בן 5 ספרות, מילת מפתח (n עבור in-use, ו-f עבור פנוי - free), וסיומת (terminator) של שני בתים. טבלה נכונה לששת הרשומות שלנו (אובייקט 0 הוא תמיד ראש הרשימה הפנויה) נראית כך:
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
/Root 1 0 R
>>
startxref
408
%%EOF
ההיסטים הללו הם החלק השביר (brittle) בכתיבת PDF ידנית. כל אחד מהם הוא מיקום הבתים המדויק שבו מתחיל ה-N 0 obj המתאים, וכל היסט זז ברגע שאתם מוסיפים תו איפשהו מעליו. ה-trailer הוא נקודת הכניסה שקורא משתמש בה אחרונה וראשונה: /Root 1 0 R נותן את שם הקטלוג, /Size 6 מציין את ספירת האובייקטים, ו-startxref 408 נותן את היסט הבתים של המילה xref עצמה. קורא פותח את הקובץ, קופץ לסוף, קורא את startxref, מחפש אל טבלת ההפניות הצולבות, ומשם מגיע לקטלוג ולכל מה שמתחתיו. %%EOF מסמן את הבית האחרון
תנו לכלי לתקן את ספירות הבתים
ההיסטים למעלה הם להמחשה בלבד; בפועל הם יהיו שגויים עד שתסיימו להקליד, מכיוון שהם תלויים בפריסת הבתים המדויקת של הקובץ שלכם. במקום לחשב אותם מחדש, כתבו את המבנה עם ערכי placeholder ותנו לכלי עזר לבנות מחדש את טבלת ההפניות הצולבות ואת אורכי הזרמים. הכלי החינמי וחוצה-הפלטפורמות pdftk עושה זאת במעבר אחד:
pdftk hello-draft.pdf output hello.pdf
הוא מנתח את האובייקטים שלכם, מחשב מחדש כל היסט בתים, ממלא את ערכי ה-/Length הנכונים, כותב טבלת xref ו-trailer חוקיים, ופולט את hello.pdf. פתחו אותו בכל מציג ותקבלו עמוד אחד עם "Hello, World!" ב-Helvetica בגודל 36 נקודות קרוב למעלה. Qpdf עושה את אותה העבודה, ומציגים רבים גם יתקנו תוך כדי תנועה (on the fly) קובץ פגום במקצת. המטרה של הישענות על כלי כאן אינה עצלנות; היא שהאריתמטיקה של ההיסטים היא החלק היחיד בפורמט עם אפס תוכן מושגי ואחוז השגיאות הגבוה ביותר, ולכן האוטומציה שלו מאפשרת למבנה להישאר הדבר שאתם לומדים
מדוע זה מתרחב למסמכים אמיתיים
שום דבר בדו"ח בן מאה עמודים לא משנה את הצורה שבניתם כרגע. הקטלוג עדיין יושב בשורש, עץ העמודים עדיין אוסף את העמודים, וכל עמוד עדיין מצביע על המשאבים ועל זרם התוכן שלו. מה שגדל הוא הרוחב, לא חוט השדרה: עץ העמודים מסתעף כך שקורא יכול לדלג על תתי-עצים שלמים, זרמי תוכן נושאים מאות אופרטורים במקום חמישה, גופנים מוטמעים כאובייקטי זרם משלהם עם טבלאות רוחב וקידודים, ותמונות מגיעות כזרמים עם מסננים ספציפיים לתמונות. קבצים מודרניים גם נוטים לארוז אובייקטים רבים לתוך זרמי אובייקטים דחוסים ולהחליף את טבלת ה-xref הרגילה בזרם הפניות צולבות, וזו הסיבה שפתיחת PDF אמיתי בעורך טקסט מציגה בדרך כלל קיר של בינארי. המודל שמתחת זהה לזה שבקובץ שיצרתם בעצמכם. עבור גרף האובייקטים הרחב יותר והאופן שבו הקטלוג, עץ העמודים ומילוני המשאבים מתקשרים לרוחב מסמך גדול יותר, הסיור המעמיק במבנה מסמך PDF מתחיל היכן שמאמר זה מסתיים, וסקירת מבנה הקובץ מכסה עדכונים אינקרמנטליים וכיצד ה-trailer משתרשר לאורך מהדורות
מכתיבה ידנית לספרייה
הקלדת אובייקטים ידנית היא תרגיל למידה, לא טכניקת ייצור. ברגע שאתם זקוקים לגופנים אמיתיים, טקסט גולש (wrapped text), תמונות, או יותר מעמוד טריוויאלי, ניהול החשבונות של הבתים ש-pdftk תיקן עבורכם הופך להיות העבודה כולה, ואתם רוצים ספרייה (library) שתיקח על זה בעלות. אותם חמישה אובייקטים עדיין נכתבים, אבל ספרייה מחשבת כל היסט, מנהלת את מילוני הגופנים והמשאבים, ודוחסת את זרמי התוכן מבלי שתעקבו אחר בית בודד. ב-Delphi ו-C++Builder, רכיב ה-HotPDF Component מצמצם את הקובץ השלם הזה לקומץ קריאות: הגדרת המסמך, קריאה ל-BeginDoc, SetFont ו-TextOut כדי למקם את אותה ברכה, ואז EndDoc כדי לכתוב קטלוג, עץ עמודים, xref ו-trailer נכונים. הבנת האובייקטים שנמצאים מתחת היא מה שמאפשר לכם להסיק מסקנות (reason about) על הפלט כאשר מסמך אינו מרונדר בדרך שציפיתם