מאמר טכני

לינאריזציה של PDF ו-Fast Web View: כיצד זה עובד

שים דוח סרוק של 80 MB מאחורי קישור, פתח אותו בדפדפן, וצפה במה שקורה: המציג יושב על חלונית ריקה עד שחלק גדול מאותם בתים יגיעו, ואז מצייר את דף אחד בבת אחת. קפוץ לדף 40 ובקובץ בנוי רע, ההורדה כולה עשויה להתחיל מחדש. החלק המתסכל הוא שהקורא רצה בסך הכל את הדף הראשון. לינאריזציה היא התשובה המבנית לבעיה זו. היא מסדרת מחדש קובץ PDF כך שמציג יוכל לרנדר את הדף הפותח מתוך קידומת קטנה של הקובץ ולהביא את השאר לפי דרישה, וזו הסיבה ש-Adobe משווקת את התכונה כ-"Fast Web View"

שום דבר מזה אינו פורמט קובץ שונה. PDF שעבר לינאריזציה הוא PDF רגיל שקורא תואם יפתח ללא טיפול מיוחד. הטריק הוא לחלוטין באופן בו הבתים מסודרים ובשני מבנים נוספים שהקובץ נושא. ISO 32000-1 מפרט את ההסדר כולו בנספח F (Annex F), וברגע שראית את הפריסה, ההתנהגות מפסיקה להיראות כמו קסם ומתחילה להיראות כמו החלפה מכוונת של סדר קובץ תמורת השהיית ציור-ראשון (first-paint latency)

מה לינאריזציה מסדרת מחדש בפועל

קובץ PDF רגיל יכול לפזר את האובייקטים שלו כמעט בכל סדר. טבלת ההפניות המקושרות (cross-reference table) בסוף הקובץ היא מה שגורם לזה לעבוד: קורא מדלג לסוף, קורא את המצביע startxref, טוען את ה-xref, ומשם יכול לאתר כל אובייקט לפי היסט (offset) שלו. עיצוב זה מצוין עבור קבצים מקומיים, שבהם דילוג לסוף אינו עולה דבר, וגרוע עבור קובץ המוזרם ברשת, כאשר הסוף הוא בדיוק החלק שמגיע אחרון. כדי לרנדר את הדף הראשון קורא קונבנציונלי זקוק לאובייקט הדף, לזרם התוכן שלו, לגופנים שאליהם הוא מפנה, ולכל תמונה שהוא מצייר, ובקובץ לא מסודר אלה יכולים לשבת בכל מקום, כולל במגה-בייט האחרון

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

סט אובייקטי הדף-הראשון ומילון פרמטרי הלינאריזציה

האובייקט הראשון ממש בקובץ שעבר לינאריזציה, לאחר כותרת ה-%PDF, הוא מילון פרמטרי הלינאריזציה. זה מה שקורא מוזרם (streaming reader) מחפש כדי להחליט האם האופטימיזציה נוכחת וכיצד להשתמש בה. המילון מתעד את אורכו של הקובץ כולו, היסט הבתים שבו מתחיל אזור ההפניה המקושרת הראשי, מספר האובייקט של הדף הראשון, ואת המיקום והאורך של זרם הרמזים (hint stream) שבא אחריו. עם המספרים האלה קורא יודע, מהקילובייטים הפותחים בלבד, כמה עליו להביא כדי להציג את הדף הראשון והיכן לחפש את האינדקס המאפשר לו לקפוץ למקום אחר

נספח F מקפיד מאוד על מה ש-"דף ראשון" אומר כאן. אזור הדף-הראשון חייב להכיל את אובייקט הדף עצמו, זרמי התוכן שלו, והמשאבים שאליהם זרמים אלה מפנים, כך שהדף עומד בפני עצמו ברגע שקידומת זו ירדה. משאבים משותפים, גופן המשמש בכל דף, לוגו שחוזר על עצמו בכותרת עליונה, מטופלים במיוחד: הם מופיעים מוקדם מספיק כדי לשרת את הדף הראשון אך מסומנים כמשותפים כדי שהקורא לא יביא אותם מחדש כאשר הוא מרנדר מאוחר יותר את דף 30. הבחנה זו בין אובייקטים פרטיים-לדף לבין אובייקטים משותפים היא החלק שרוב ה-"אופטימיזטורים" מתוצרת-בית טועים בו, ולטעות בכך זה מה שמפיק קובץ שטוען שעבר לינאריזציה אך עדיין נתקע (stalls)

זרמי רמזים (Hint streams): האינדקס שהופך קפיצות דף לזולות

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

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

למה השרת חייב לשתף פעולה

לינאריזציה מניחה שהתעבורה יכולה לספק פרוסות שרירותיות של הקובץ, והנחה זו שווה בדיקה לפני שאתה מאשים את הפורמט בתוצאות גרועות. המנגנון הוא HTTP byte-serving: הקורא מנפיק בקשות טווח (range requests), והשרת עונה עליהן בתגובות 206 Partial Content. אם השרת אינו מפרסם Accept-Ranges: bytes, או אם פרוקסי או CDN לפניו קורס את בקשות הטווח להעברות מלאות, לקורא אין דרך להביא את דף 40 בבידוד והוא חוזר לאחור (falls back) להורדת הקובץ כולו. המבנה בתוך ה-PDF במקרה זה מושלם ונכון לחלוטין ומבוזבז לגמרי

זהו הכשל שלעיתים קרובות ביותר מאובחן בטעות כ-"לינאריזציה אינה עובדת". הקובץ בסדר; נתיב המסירה אינו בסדר. לפני שאתה בונה מחדש מסמך, ודא באמצעות בקשה מותנית (conditional request) שהמארח למעשה מחזיר תוכן חלקי (partial content) עבור ה-URL בו הקורא פוגע. מארחים סטטיים רבים עושים זאת כברירת מחדל, ושרתי יישומים ושכבות מטמון (caching layers) רבים המוגדרים באופן שגוי אינם עושים זאת

עדכונים הדרגתיים שוברים בשקט את הלינאריזציה

הנה האילוץ שמפתיע אנשים המייצרים קבצים שעברו לינאריזציה נכונה ואז תוהים מדוע האופטימיזציה מתנדפת. לינאריזציה תלויה בפריסה יחידה ומסודרת בקפידה עם האינדקס שלה בחזית. עדכון הדרגתי (incremental update) מפר זאת במכוון (by design). כאשר כלי מוסיף חתימה, ממלא שדה טופס, או מצרף הערה דרך שמירה הדרגתית, הוא אינו כותב מחדש את הקובץ. הוא מצרף (appends) את האובייקטים שהשתנו, אזור הפניה מקושרת חדש, ו-trailer חדש לסוף, תוך השארת הבתים המקוריים ללא פגע. הצירוף הזה הוא כל הנקודה של עדכונים הדרגתיים: הוא מהיר, והוא שומר על המהדורה המוקדמת יותר עבור ביקורת או אימות חתימה

תופעת הלוואי היא שלקובץ יש כעת את נתוני ההפניה המקושרת החדשים ביותר שלו בזנב, אחרי בלוק הדף-הראשון הממוקם בקפידה, ומילון פרמטרי הלינאריזציה בחזית מתאר פריסה שכבר אינה תואמת לקובץ. קורא תואם מאתר את חוסר ההתאמה ומתייחס למסמך כקובץ PDF רגיל שאינו לינארי. Fast Web View נעלם, למרות שהמבנה הלינארי המקורי עדיין יושב שם במחצית הראשונה של הקובץ. אם אתה מצרף מספר עדכונים, כל אחד מהם מערם (stacks) מהדורה נוספת בסוף והפער בין האינדקס הקדמי המיושן לבין המצב האמיתי מתרחב

אם זרימת העבודה שלך דורשת גם עריכות וגם Fast Web View, הכלל נובע ישירות מהמבנה: ערוך הדרגתית (incrementally) בעוד המסמך נמצא בתהליך שינוי, ולאחר מכן בצע לינאריזציה מחדש פעם אחת בסוף. שכתוב מלא הוא מה שמשחזר את הפריסה. במונחי HotPDF, משמעות הדבר היא שעריכה בתהליך עוברת דרך BeginIncrementalUpdate ו-SaveIncrementalUpdate, אשר מצרפים דלתא (delta), בעוד שלב הסיום טוען את המסמך כולו ויוצר עליו סריאליזציה חדשה עם LoadFromFile ואחריו SaveLoadedDocument, המפיל את המהדורות הישנות שהצטברו ופולט פריסה נקייה אחת. אותה פשרה מופיעה עם זרמי אובייקט (object streams): הפעלת UseObjectStreams יחד עם UseXRefStream דוחסת את ההפניה המקושרת ואורזת אובייקטים בחוזקה, מה שעוזר לגודל הקובץ אבל, כמו כל בחירה מבנית, צריך להיות מיושם במהלך שכתוב סופי זה במקום להיות מחובר למהדורה מצורפת

// עריכות בתהליך: צרף דלתא, שמור על מהדורות קודמות ללא פגע.
// זה משאיר את הקובץ ללא לינאריזציה.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');

// שלב סיום: סריאליזציה מחדש מלאה מפיקה פריסה נקייה אחת,
// הפלת המהדורות הנערמות. הרץ מחדש את מבצע הלינאריזציה שלך על הפלט.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');

HotPDF אינו חושף שגרת "לינאריזציה" בקריאה אחת, ולכן התבנית המעשית היא להפיק קובץ נקי, משוכתב במלואו, ולהריץ עליו כלי אופטימיזציה ייעודי. כלי שורת-פקודה (Command-line) מטפלים בסידור מחדש ישירות. qpdf כותב מחדש קובץ לצורה לינארית עם דגל יחיד:

qpdf --linearize report-final.pdf report-web.pdf

איך לדעת האם קובץ עבר לינאריזציה

אל תסמוך על שם הקובץ או על הכלי שטוען שהפיק אותו; ודא את הבתים. הבדיקה הישירה ביותר היא בראש הקובץ: פתח אותו וחפש את מילון פרמטרי הלינאריזציה בתור האובייקט הראשון אחרי הכותרת (header), הנושא את המפתח /Linearized. קיצור דרך המופנה לקורא הוא תיבת הדו-שיח מאפייני מסמך של Acrobat, אשר מדווחת "Fast Web View: Yes" רק כאשר המבנה באמת נוכח ועדכני

לבדיקות באמצעות סקריפט, qpdf מדווח הן על הנוכחות והן על תקינות המבנה, מה שחשוב מכיוון שקובץ יכול לשאת מילון לינאריזציה שכבר אינו משקף את הפריסה שלו, בדיוק המצב שעדכון הדרגתי (incremental update) משאיר מאחוריו:

# מדווח "File is linearized" ומאמת טבלאות רמזים מול הפריסה
qpdf --check report-web.pdf

# שופך (Dumps) את פרמטרי הלינאריזציה ונתוני הרמזים בפירוט
qpdf --show-linearization report-web.pdf

שלב האימות הוא זה שמצדיק את עצמו. מעבר שרק מאשר שהמילון קיים יברך בשמחה קובץ שהאינדקס שלו מצביע על הסטות שגויות (wrong offsets); בדיקה המיישבת את טבלאות הרמזים מול המיקומים בפועל של האובייקטים היא זו שאומרת לך שהאופטימיזציה תחזיק מעמד תחת בקשות הטווח של קורא אמיתי

לינאריזציה נשארת כדאית ליישום על כל מסמך גדול המוגש ברשת, במיוחד לקוראים ניידים בחיבורים לא יציבים, והיא עולה כמה אחוזים מגודל הקובץ עבור האינדקס הנטען-מראש (front-loaded). שני הדברים שיש להקפיד עליהם הם שהמבנה בתוך ה-PDF וה-byte-serving מחוץ לו שניהם חייבים להיות נכונים, ושכל עריכה בדיעבד מבטלת את האופטימיזציה עד שתכתוב מחדש את הקובץ. התייחס ללינאריזציה-מחדש כשלב האחרון בצינור העבודה (pipeline), לאחר שכל שינוי אחר יושב. התנהגות ההפניה המקושרת (cross-reference), זרם האובייקט (object-stream), והעדכון ההדרגתי (incremental-update) המתוארת כאן היא חלק מהמודל המבני שרכיב HotPDF Component עבור Delphi ו-C++Builder מיישם; לרקע רחב יותר על פריסת קבצים ראה כיצד בנוי PDF, ולזרימת עבודה של עדכון הדרגתי וקבצים גדולים בקוד ראה עיבוד קובצי PDF גדולים מ-Delphi