כדי לגלות לאן גודל קובץ PDF בפועל הולך, losLab PDF Library חושפת את AuditDocumentSpace, שמסווגת כל אובייקט עקיף לשתים עשרה קטגוריות — תמונות, תוכניות גופנים, מילוני גופנים, זרמי תוכן, טפסי XObject, זרמי אובייקטים, קבצים משוטמעים, מטא-נתונים, עץ מבנה, הערות, עץ עמודים, אחר — ומדווחת על מספר האובייקטים, הבייטים המאוחסנים ואחוז החלק של כל אחת
המצב שבשבילו זה קיים מוכר. דוח בן 40 עמודים יוצא מהמחולל שלך בגודל 80 מגה-בייט, הלקוח שואל למה, וכל מה שאתה יכול להציע הוא ניחוש. כנראה התמונות. אולי הגופנים. אז אתה מפעיל דגימת-משנה, שולח, והקובץ נוחת ב-74 מגה-בייט כי המשקל האמיתי היה במקום אחר לגמרי. המאמר המשלים שלנו על תת-קבוצת גופנים ודגימת-משנה של תמונות מכסה כיצד לכווץ PDF; זה מכסה את השלב שאמור לבוא קודם, שהוא מדידת מה שאתה עומד לכווץ
למה למדוד לפני שדוחסים?
מפני ששלושת מעברי האופטימיזציה הסטנדרטיים מניבים תשואות שונות בתכלית על כל קובץ נתון, ושום דבר בקובץ לא מספר לך איזה מהם רלוונטי עד שתספור. יצירת תת-קבוצת גופנים במסמך שהגופנים שלו כבר 2% מהבייטים שלו היא אחר צהריים שמושקע בהזזת שגיאת עיגול. דגימת-משנה של תמונות בקובץ שרוב נפחו הם זרמי תוכן לא דחוסים מפיקה את אותה אכזבה. האופטימייזר אינו החלק הקשה — לכל ספרייה יש אחד. לדעת לאיזה אופטימייזר לכוון בקובץ הזה הוא החלק הקשה, וזו שאלת הנהלת חשבונות, לא שאלת דחיסה. ביקורת גם תופסת את המקרים שבהם אין תשובה לאופטימייזר: קובץ שמתברר כ-60% קבצים מצורפים משוטמעים לא צריך דחיסה טובה יותר, הוא צריך שיחה על האם הקבצים המצורפים האלה שייכים למסמך, וקובץ שהוא 30% עץ מבנה משלם עבור תיוג נגישות, שבדרך כלל הוא עלות מכוונת שלא צריך להסיר בשקט. ברגע שהבייטים משויכים אתה מקבל החלטת מוצר עם מספרים מאחוריה במקום להושיט יד למתג הקרוב ביותר
מה מכיל דוח שתים-עשרה הקטגוריות
AuditDocumentSpace מחזירה מזהה רשימת מחרוזות ולא רשומה, כך שהדוח שורד את חזיתות ה-DLL השטוחות וה-COM ללא שינוי. הרשימה מחזיקה שורת סיכום Total,Objects,Bytes,100.0 ואחריה בדיוק שתים-עשרה שורות Category,Objects,Bytes,Percent בסדר קבוע שהוא חלק מהחוזה: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. שלוש עשרה שורות, תמיד, גם כשקטגוריה ריקה
var
Lib: TPDFlib;
ListID, I: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
ListID := Lib.AuditDocumentSpace; // 0 when no document is selected
if ListID = 0 then
Exit;
try
// GetStringListItem is 1-based: items run 1..GetStringListCount
for I := 1 to Lib.GetStringListCount(ListID) do
Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
finally
Lib.ReleaseStringList(ListID);
end;
finally
Lib.Free;
end;
end;
פרט אחד של דלפי בלולאה הזו יעקוץ אותך בדיוק פעם אחת. GetStringListItem משתמשת באינדקסי פריטים מתחילים מ-1, בהתאמה ל-GetStringListCount, ואינדקס מחוץ לטווח מחזיר מחרוזת ריקה במקום להעלות חריגה. כתוב את הלולאה for I := 0 to Count - 1 מתוך הרגל ותקבל שורה ראשונה ריקה, שורה אחרונה שנשמטת בשקט, ואף חריגה בשום מקום שתגיד לך שהאינדוקס שגוי. הדוח עצמו ייראה כמעט נכון, וזה מצב הכשל הגרוע ביותר שכלי אבחון יכול לספק
למה הביקורת משתמשת באורך המאוחסן במקום בגודל המפוענח?
מפני שאורך מאוחסן הוא גם המספר שאתה רוצה וגם המספר שזול להשיג. כל אובייקט עקיף נושא את TPDFIndObj.FLength, אורך הבייטים הגולמי שהאובייקט תופס בקובץ כפי שנותח. שימוש בו פירושו שתמונת DCTDecode של 900 קילו-בייט מדווחת כ-900 קילו-בייט — הבייטים שהיא עולה לך על הדיסק — ולא 40 מגה-בייט הדגימות RGB שאליהם היא מפוענחת. זה גם אומר שהביקורת אף פעם לא צריכה לפענח דבר: אובייקטים שנטענים בעצלתיים נשארים עצלים, המסננים נשארים לא-מופעלים, וביקורת קובץ בגודל 500 מגה-בייט היא מעבר על כותרות אובייקטים ולא מחזור פענוח מלא
הכלל השני הוא הגנה נגד ספירה כפולה. כשאובייקט חי בתוך זרם אובייקטים דחוס, מסומן על ידי FObjStrNum שונה מאפס, ספירת הבייטים שלו נרשמת כאפס. האחסון שלו כבר שולם פעם אחת על ידי זרם הקונטיינר, שתקן ISO 32000-1 §7.5.7 מגדיר כזרם /Type /ObjStm שמחזיק אובייקטים רבים בפאילוד דחוס-Flate אחד. חיוב כל חבר בחלק שלו ואז חיוב הקונטיינר שוב היה מנפח את הסך הכולל מעבר לגודל הקובץ האמיתי. לזה יש השלכה ישירה על איך אתה קורא את הפלט, מכוסה להלן ובעומק רב יותר במאמר שלנו על זרמי אובייקטים וזרמי הפניה צולבת
למה תוכנית גופן לא יכולה לסווג את עצמה?
מפני שלקובץ גופן TrueType משוטמע ב-PDF אין סמן שמציין זאת. תקן ISO 32000-1 §9.8.1 מגדיר את תוכנית הגופן המשוטמעת כערך של /FontFile, /FontFile2 או /FontFile3 במתאר גופן, ומילון הזרם בקצה השני של אותה הפניה נושא מפתחות /Length1 ומסנן אבל לא /Type ולא /Subtype שמזהה אותו כגופן. כשמסתכלים עליו בבידוד זהו זרם בינארי אנונימי. רק המתאר שמצביע עליו יודע מה הוא. אותה אסימטריה מופיעה עבור הערות: §12.5.2 הופך את /Type /Annot לאופציונלי במילון הערה, כך שהאות האמין הוא חברות במערך /Annots של עמוד, לא המילון עצמו
אז הסיווג רץ פעמיים. המעבר הראשון קורא את /Type ו-/Subtype של כל אובייקט ולוקח את הניצחונות הקלים: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font ו-/Type /FontDescriptor, /Metadata, /EmbeddedFile ו-/Filespec, /StructTreeRoot ו-/StructElem, /Annot, /Page ו-/Pages. כל השאר נוחת זמנית ב-Other. המעבר השני אז הולך בצד המפנה ודורס: כל מילון עמוד מייחד מחדש את /Contents שלו לזרמי תוכן, את הרשומות /Annots שלו להערות, ואת /Thumb שלו לתמונות, בעוד כל מילון גופן הולך בשרשרת המתאר שלו עצמו
// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
MarkRef(Descriptor.FindValueByKeyName('FontFile'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));
קריאת הדוח ובחירת הצעד הבא
קרא את החלקים תחילה, את ספירות האובייקטים שנית, והתייחס לכל פער גדול ביניהם כאל אות. PDF מודרני שם את רוב המילונים הקטנים שלו בתוך זרמי אובייקטים, כך ש-Page tree ו-Structure tree מציגים באופן שגרתי עשרות אובייקטים כנגד כמעט אפס בייטים — העלות האמיתית שלהם נקפלה לתוך שורת Object streams. אם Object streams עצמה גדולה, הקובץ צפוף במבנה דמוי-מטא-נתונים ולא בתוכן, והמנוף הוא גיזום אובייקטים, לא דחיסתם. זרמי מראה של הערות מתנהגים בדומה: הם נושאים /Subtype /Form, כך שמסמך עם הרבה חותמות מציג את משקלו תחת Form XObjects בעוד שורת Annotations נשארת קטנה
function CategoryShare(Lib: TPDFlib; ListID: Integer;
const Category: string): Double;
var
I: Integer;
Parts: TArray<string>;
Inv: TFormatSettings;
begin
Result := 0;
Inv := FormatSettings;
Inv.DecimalSeparator := '.'; // the report is locale-independent
for I := 2 to Lib.GetStringListCount(ListID) do // line 1 is Total
begin
Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
if (Length(Parts) = 4) and SameText(Parts[0], Category) then
Exit(StrToFloatDef(Parts[3], 0, Inv));
end;
end;
שתי עובדות עיצוב חשובות אם אתה מפענח את האחוזים ולא רק מציג אותם. מפריד העשרוני הוא תמיד נקודה מילולית ללא קשר ל-locale של המכונה, כך שפענוח עם FormatSettings הסביבתי בתחנת עבודה גרמנית או צרפתית ייכשל או, גרוע מכך, יקרא לא נכון. ואפסים נגררים נגזמים, כך שקטגוריה שמחזיקה בדיוק 40% מהבייטים מודפסת כ-40, לא 40.0 — לעולם אל תניח מקום עשרוני קבוע. עם החלק ביד, הניתוב מכני: חלק Images דומיננטי מצביע על DownsampleImages, חלק Font programs דומיננטי על SubsetEmbeddedFonts, ו-Content streams מסיבי על CompressContent
מה הביקורת בכוונה לא מספרת לך
הסך הכולל הוא סכום על פני אובייקטים עקיפים, וקובץ PDF הוא מעט יותר מהאובייקטים שלו. כותרת הקובץ, ה-trailer, רווחים בין-אובייקטיים וטבלת הפניה צולבת קלאסית אינם אובייקטים עקיפים, כך שהבייטים האלה משויכים לכלום וסך הביקורת נוחת מעט מתחת לגודל בדיסק. זרם הפניה צולבת שונה — הוא אובייקט אמיתי עם /Type /XRef, כך שבקובץ מודרני הבייטים האלה כן מופיעים, בקטגוריית Other. אף התנהגות אינה פגם, אבל אם אתה מיישב את הביקורת מול ספירת בייטים ממערכת הקבצים, שם הפער מגיע ממנו
שני גבולות נוספים ראויים לציון ברור. ראשית, המספרים מתארים קובץ שנטען, לא כזה שנוצר: עבור אובייקטים שנבנו בזיכרון ואין להם עדיין אורך מאוחסן, הגודל נסוג לפלט המסודר עם הקצבה נומינלית למילון הזרם, שהוא הערכה של הכתיבה העתידית ולא מדידה. בצע ביקורת אחרי שמירה-וטעינה-מחדש אם אתה רוצה נתונים מדויקים. שנית, שורת Other שמנה היא ממצא, לא דוח באג — היא בדרך כלל אומרת אובייקטים יתומים שאף אחד כבר לא מפנה אליהם, וזו עבודה עבור איסוף אשפה mark-and-sweep ולא עבור שום מעבר דחיסה
בשימוש בדרך הזו הביקורת משנה את צורת השיחה. במקום לנחש לגבי דוח ה-80 מגה-בייט אתה פותח אותו, מריץ קריאה אחת, וקורא שהתמונות הן 8%, תוכניות הגופנים הן 61%, והמסמך משטמע תשע תוכניות גופן מלאות עבור סגנון בית שמשתמש בשלושה פרצופים. זו תשובה ברת-תיקון עם מספר מחובר אליה. AuditDocumentSpace, יחד עם מעברי האופטימיזציה שהיא מכוונת אותך אליהם, משתלב ב-losLab PDF Library for Delphi and C++Builder, שם עמודי ההפניה מתעדים את רשימת הקטגוריות המלאה ואת ה-API של רשימת המחרוזות סביבה