HotPDF כותבת קובצי PDF מלוינרים, הפריסה ש-Acrobat מתייג Fast Web View, דרך התכונה LinearizeOutput על THotPDF. הגדרתה לפני BeginDoc גורמת ל-HotPDF לסדר מחדש את גרף האובייקטים המוגמר כך שקורא שמודע לטווח-בייטים יכול להציג עמוד אחד אחרי שהוא שולף רק את החלק המוביל של הקובץ, במקום להוריד את המסמך כולו קודם. המנגנון הוא תקן ISO 32000-1 נספח F
הסיבה שזה חשוב היא לא-מרשימה. PDF רגיל שם את טבלת ההפניה הצולבת שלו בסוף, כך שצופה חייב להגיע לבייט האחרון לפני שהוא יודע איפה כל דבר. תן לדפדפן דוח סרוק בן 200 עמודים והמשתמש יבהה בגלגל הטעינה למשך ההעברה המלאה, למרות שכל מה שהוא רצה היה עמוד 1. לינריזציה מתקנת זאת על ידי תשלום עלות בזמן הכתיבה. המאמר הזה עוסק בנתיב הכתיבה הזה ספציפית, החלוקה, לולאת המדידה והמגבלות הקשות; לרקע המושגי על מה ש-Fast Web View קונה לך, ההסבר קודם על לינריזציית PDF ו-Fast Web View מכסה את הקרקע
מה הפריסה המלוינרת בעצם מבטיחה
קובץ מלוינר הוא PDF רגיל עם סדר פיזי ספציפי ביותר, וכל ערבות שהוא מציע מגיעה מהסדר הזה ולא מאיזה סוג אובייקט חדש. HotPDF פולטת את החלקים ברצף שנספח F מכתיב: מילון פרמטרי הלינריזציה בתוך 1024 הבייטים הראשונים, טבלת הפניה צולבת מוקדמת, האובייקטים ברמת המסמך, זרם הרמז הראשי, העמוד הראשון והאובייקטים הפרטיים שלו, אז שאר העמודים, אחר כך האובייקטים המשותפים, ואז כל השאר, ולבסוף טבלת ההפניה הצולבת הראשית
החלוקה נגזרת, לא מוצהרת. HotPDF הולכת בגרף ההפניות מכל אובייקט עמוד ורושמת, לכל אובייקט עקיף, כמה עמודים מגיעים אליו ואיזה עמוד הגיע אליו ראשון. אובייקט שבו משתמש בדיוק עמוד אחד הופך פרטי לאותו עמוד. אובייקט שנגיש מיותר מאחד הופך משותף. הקטלוג, בתוספת כל מה שהוא מפנה אליו תחת /ViewerPreferences, /OpenAction, /Threads ו-/AcroForm, בתוספת מילון ההצפנה כשההגנה פעילה, מהווים את קבוצת רמת-המסמך שחייבת לקדום את הכול. צמתי עץ העמודים מוחזקים אחור בכוונה כדי שלא יזהמו את קטע העמוד הראשון
מילון הפרמטרים נושא את המספרים שקורא צריך לפני שהוא קרא משהו אחר: /L לאורך הקובץ הכולל, /H להיסט ולאורך של זרם הרמז, /O למספר האובייקט של העמוד הראשון, /E לבייט שבו קטע העמוד הראשון מסתיים, /N למספר העמודים ו-/T להיסט של רשומת טבלת ההפניה הצולבת הראשית. כל אחד מאלה הוא היסט בייטים לתוך קובץ שעדיין לא קיים ברגע שאתה צריך לכתוב אותם
למה היסטי טבלת הרמז חייבים להתכנס?
מפני שהמספרים במילון הפרמטרים מתארים את הקובץ שמכיל אותם, ושינוי כל אחד מהם משנה את הקובץ. זה הקושי המרכזי של כותב מלוינר, וזו הסיבה ש-HotPDF מודדת שוב ושוב במקום לכתוב פעם אחת. הרחב את /T מ-6 ספרות ל-7 ומילון הפרמטרים גדל בבייט; הכותרת גדלה; כל אובייקט זז; טבלת ההפניה הצולבת הראשית זזה; /T עכשיו זקוק לערך אחר. הפריסה חייבת להגיע לנקודת שיווי-משקל לפני שבייט אחד של פלט אמיתי מחויב
HotPDF מטפלת בזה עם איטרציה חסומה. היא מסדרת קודם כל אובייקט לתוך זרם ספירה שרושם אורך בלי לשמור בייטים, כך שלכל אובייקט יש גודל מסודר ידוע. היא אז מריצה מעבר פריסה שמקצה היסטים לקבוצת רמת-המסמך, זרם הרמז, קבוצת העמוד הראשון, קבוצות העמודים המאוחרים יותר, קבוצת השיתוף והשארית, ומדווחת איפה טבלת ההפניה הצולבת הראשית תנחת. התוצאה הזו מוזנת בחזרה כקלט למעבר הבא. הלולאה מוגבלת לשמונה ניסיונות, ואי-התכנסות מעלה חריגה במקום להפיק קובץ עם היסטים שגויים שנראים אמינים
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
שני פרטים שומרים על הלולאה מלנוע ללא הרף. מילון הפרמטרים נכתב לתוך משבצת קבועה בת 384 בייטים, מרופדת ברווחים, כך שהצמיחה שלו עצמו לעולם לא יכולה לערער את הפריסה; אם טקסט המילון אי-פעם היה חורג מההקצבה הזו, HotPDF מעלה חריגה במקום להזיז הכול בשקט. ואחרי ההתכנסות HotPDF מריצה מעבר פריסה מאשר נוסף אחד ובודקת מחדש את אורך זרם הרמז, כי זרם הרמז עצמו מקודד היסטים שהיו ידועים רק ברגע שהפריסה התייצבה. התועלת מכל המדידה הזו היא ש-HotPDF אף פעם לא ממאגנת עותק שני של המסמך: ברגע שההיסטים קבועים, אובייקטים מסודרים ישירות לתוך זרם היעד, עם assertion בכל גבול קטע שהבייטים שנכתבו תואמים להיסט שהובטח
הפעלה מדלפי
משטח ה-API הוא בוליאני אחד, והדרישה היחידה שלו היא שתקבע אותו לפני שהיצירה מתחילה. LinearizeOutput ברירת המחדל שלו False, ומעבר הפריסה רץ כשהמסמך נכתב, כך שהקצאתו אחרי EndDoc לא משיגה כלום
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
אזהרת פריסה אחת עולה על הכול בצד הקוד. לינריזציה משתלמת רק כשהתעבורה תומכת בבקשות טווח HTTP. שרת את אותו קובץ מנקודת קצה ששולחת אותו זרם שלם, או מתצורת CDN שמתעלמת מ-Range, וקנית לעצמך נתיב כתיבה איטי יותר וקובץ גדול יותר ללא רווח נראה-לעין למשתמש. בדוק את השרת לפני שאתה בודק את הקוד
למה לינריזציה דורסת את UseXRefStream ו-UseObjectStreams?
מפני שהכותב המלוינר צריך שלכל אובייקט יהיה היסט בייטים כתובת-ישירה משלו, ושתי התכונות הללו לוקחות את זה. HotPDF לכן פולטת טבלאות הפניה צולבת טקסטואליות מסורתיות ואובייקטים עקיפים לא-ארוזים בכל פעם ש-LinearizeOutput מופעל, גם אם הקורא גם קבע UseXRefStream או UseObjectStreams. זו דריסה מכוונת, לא קונפליקט שאתה צריך לפתור בעצמך
ההיגיון עוקב אחרי טבלאות הרמז. טבלת רמז מתארת איפה קטע עמוד מתחיל וכמה הוא ארוך, כך שקורא יכול לבקש בדיוק את הטווח הזה. אובייקט שארוז בתוך קונטיינר /ObjStm אין לו שום היסט עצמאי כלל; הוא קיים רק כפרוסה בתוך זרם דחוס אחר שחייב להישלף ולהתנפח כיחידה. אם היית סומך על זרמי אובייקטים לגודל קובץ, הבן שלינריזציה ודחיסה מושכות בכיוונים מנוגדים כאן, וקרא את הטרייד-אוף במאמר הנלווה על זרמי אובייקטים ועדכונים incremental ב-HotPDF. אותו מתח מעצב קבצי הפניה-היברידית, שקיימים בדיוק כדי לשמור על קוראים ישנים יותר עובדים לצד טבלאות מבוססות-זרם, כמכוסה במאמר על זרמי הפניה צולבת היברידיים בקבצי PDF שנוצרו על ידי Office
יש גם רצפת גרסה. לינריזציה דורשת PDF 1.2 או מאוחר יותר. אם הגרסה הנבחרת ישנה יותר, HotPDF מעלה אותה אוטומטית, אלא אם StrictVersionLock מוגדר, ובמקרה כזה כתיבה מעלה חריגה במקום לקדם בשקט מסמך שנעצת בכוונה
קיר ה-4 GiB, ולמה HotPDF מסרבת במקום לקטוע
טבלאות הרמז של לינריזציה מאחסנות היסטים כערכים בני 32 סיביות, כך שקובץ מלוינר לא יכול לכתוב כתובת לשום דבר בגובה 4 GiB או מעל, ו-HotPDF דוחה פלט כזה עם חריגה מפורשת במקום לכתוב קובץ עם היסטים שהתגלגלו. המגבלה אינה בחירת מימוש של HotPDF; היא רוחב השדות שנספח F מגדיר
הבדיקה מוחלת בשלושה מקומות, ושלושתם חשובים. HotPDF מאמתת כל אובייקט ברגע שהאורך המסודר שלו ידוע, מאמתת כל אורך קטע עמוד בזמן בניית רשומות הרמז, ומאמתת את אורך הקובץ הסופי אחרי שטבלת ההפניה הצולבת הראשית נקבעה בגודלה. כישלון מוקדם הוא כל הטעם: טבלת רמז עם היסט שקוצץ בשקט מפיקה קובץ שנפתח נכון בצופה שמוריד אותו זרם שלם ונכשל רק עבור לקוח טווח-הבייטים שלינריזציה קיימת כדי לשרת, וזה מצב הכישלון הגרוע ביותר האפשרי כי הצופה בבדיקה שלך אף פעם לא משחזר אותו. אם אתה מפיק פלט של כמה גיגה-בייט, לינריזציה אינה הכלי, והגישה הזורמת המתוארת בהערות על Direct File API לתהליכי עבודה PDF גדולים היא הכיוון להסתכל אליו
איתור לינריזציה בקובץ שטענת
THotPDF.IsLoadedLinearized מדווחת האם המסמך הנטען כרגע כבר נכתב בצורה מלוינרת, והיא עונה מתמונת מצב שנלקחה לפני הפענוח, לא מהזרם החי. HotPDF קוראת את 1024 הבייטים הראשונים ממיקום אפס של זרם המקור, סורקת אותם למילת המפתח הראשונה obj ואז לרשומת /Linearized עם הערך 1, ומאחסנת את התוצאה הבוליאנית במטמון
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
שתי אילוצים בתיאור הזה הם קריטיים. הזיהוי לא יכול להסתמך על מיקום הזרם, כי עד שקוד היישום שואל את השאלה המפענח כבר הזיז אותו, והוא לא יכול לקרוא מחדש לפי דרישה כי LoadFromFile משחררת את זרם המקור הפנימי ברגע שהטעינה מסתיימת. מכאן העיצוב של לכידה-לפני-פענוח-ואז-מטמון. הסריקה גם מכוונת להיות מילולית לגבי הערך: רק /Linearized 1 או צורה מספרית שקולה עם שבר כולו-אפס מתקבלת, כי קובץ שמילון הפרמטרים שלו אומר משהו אחר לא מבטיח את הבטחת נספח F
מלכודת רשומת דלפי ששווה לגנוב
רשומות מקומיות המכילות מערכים דינמיים מאתחלות את השדות המנוהלים שלהן ושום דבר אחר, ואם אתה שומר שדה Count פשוט לצד המערך אתה חייב לנקות אותו בעצמך. זה נגס בחלוקת הלינריזציה במהלך הפיתוח, וזה סוג הבאג שעולה יום בדיוק כי פלטפורמה אחת מסתירה אותו
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
שדה המערך הדינמי נספר-הפניות, כך שהמהדר מאפס אותו. ה-Count לצידו הוא מספר שלם רגיל ללא ערבות כזו, ו-Count לא-מאותחל שולח את ההוספה הראשונה לאינדקס שרירותי. תחת Win32 משבצת המחסנית במקרה החזיקה אפס, ההוספה נחתה באינדקס 0, וכל הבדיקות עברו. תחת Win64 אותו קוד כתב מעבר לסוף המערך. הלקח מוכלל הרבה מעבר ללינריזציה: כשרשומה מערבבת שדות מנוהלים ובלתי-מנוהלים, הקצה Default(TRecord) והפסק לנמק לגבי אילו שדות המהדר מכסה, ולעולם אל תתייחס להרצת Win32 ירוקה כהוכחה שהאתחול נכון
החברים LinearizeOutput ו-IsLoadedLinearized המתוארים כאן משתלבים עם HotPDF Component הסטנדרטי עבור דלפי ו-C++Builder; עמוד המוצר נושא את הפניית התכונה המלאה, כולל כללי האינטראקציה עם זרמי הפניה צולבת, זרמי אובייקטים ונעילת גרסה