ה-THotPDF.CompressDocument של HotPDF הוא מתג אחד שגורם ל-BeginDoc להניב את ה-PDF החסר-אובדן הקטן ביותר שהרכיב יכול לכתוב: FlateDecode ברמה המרבית, זרם cross-reference עם object streams, subsetting של פונטים ותתי-קבצי פונט קומפקטיים שממספרים מחדש את ה-glyph-ים הנשמרים מאחורי /CIDToGIDMap מפורש. ה-EndDoc אז מחזיר את ההגדרות שלכם. מסמך בדיקה בן שלושה עמודים עם Arial ו-SimSun צנח מ-10.2 MB ל-20 KB עם רינדור זהה
מה CompressDocument באמת מדליק?
ה-CompressDocument דורס שש הגדרות כתיבה, ועוד את תקרת ה-object stream, עבור מסמך אחד ומשחזר את כולן אחר כך. ב-BeginDoc, לפני שגרסת ה-PDF מתייצבת, HotPDF רושם את הערכים שלכם ומגדיר Compression ל-cmFlateDecode, CompressionLevel ל-clMaximum, מדליק את EnableFontSubsetting ואת CompactFontSubsetting, ומאפשר UseXRefStream ועוד UseObjectStreams (ISO 32000-1 §7.5.7 ו-§7.5.8). object streams דורשים PDF 1.5, ולכן Version ישנה מועלית ל-1.5 כשהיא לא נעולה. PDF/A-1 אוסר על שני המבנים, ולכן מסמך PDF/A-1 שומר על טבלת ה-cross-reference הקלאסית שלו ומקבל רק את עבודת ה-Flate והפונטים. תמונות נשארות בדיוק כפי שהטמעתם אותן
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'invoice-2026-1042.pdf';
Pdf.CompressDocument := True; // מיושם על ידי BeginDoc, מבוטל על ידי EndDoc
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
השחזור קורה בה-finally החיצונית ביותר של ה-EndDoc, כך שחריגה באמצע דוח לא משאירה רכיב ארוך-חיים תקוע בדחיסה מרבית עבור המשימה הבאה. התכונה CompressDocument עצמה נשארת True; רק שש ההגדרות שהיא השאילה חוזרות. את הגרסה מטפלים בעדינות רבה יותר. HotPDF מבטל את ההעלאה של עצמו ל-1.5 רק אם המסמך עדיין מסתיים ב-1.5, כך שכשתכונה אחרת דחפה את הקובץ ל-1.6 במהלך הריצה (נניח, פונט OpenType מוטמע), הגרסה הגבוהה נשארת, בדיוק כפי שהייתה בלי הדחיסה
למה תתי-קבצי הפונט עדיין גדולים בלי ה-compaction?
תת-קובץ TrueType קלאסי משמיט את קווי המתאר שלעולם לא תציירו אבל שומר על כל glyph ID במקומו, והמיספור הזה הוא מה שמכביד עליו. זרם התוכן מציג CIDs ששווים ל-GIDs המקוריים, ולכן תת-הקובץ חייב לשמור offset של loca ורשומת hmtx עבור כל slot עד ה-glyph הגבוה ביותר שהוא מחזיק, ריק או לא. עבור פונט לטיני התקורה הזאת היא רעש. עבור פונט CJK כמו SimSun, שהאידיאוגרמות שלו יושבות עמוק בטבלת glyph-ים גדולה מאוד, שני תווים סיניים גוררים אחריהם טבלאות במידות של הפונט המלא. כללי סגירת תת-קובץ הפונט עבור glyph-ים מעוצבים מחליטים אילו glyph-ים שורדים; ה-compaction הוא עניין של כמה השורדים עולים
ה-CompactFontSubsetting ממספר מחדש את ה-glyph-ים הנשמרים לטווח צפוף שמתחיל באפס וכותב זרם /CIDToGIDMap על ה-CIDFont, ש-ISO 32000-1 §9.7.4.2 מגדיר כטבלה של GIDs בני שני בייטים הממופה לפי CID. הטבלה הזאת היא כל הטריק. זרמי תוכן, מערך הרוחבים /W וה-ToUnicode CMap כולם שומרים על ה-CIDs המקוריים, ולכן שום דבר שכבר נכתב לא צריך להשתנות; רק החיפוש מ-CID אל glyph עובר אל המפה. בבדיקה שהניעה את התכונה, SimSun עם שני תווים ירד מ-24.8 KB של נתוני פונט ל-3.1 KB
ל-compaction יש גבולות נוקשים, והוא מידרדר בשקט במקום להיכשל. HotPDF בונה תתי-קבצים קומפקטיים רק עבור פנים Type 0 TrueType, גם אלה שנקבעו דרך SetFont עם subsetting דלוק וגם את הפנים הרשומה דרך RegisterUnicodeTTF. פונט TrueType פשוט מוצא את ה-glyph-ים שלו דרך ה-cmap בתוך תוכנית הפונט, מה שהמיספור מחדש היה שובר, ולכן הוא שומר על תת-הקובץ הדליל. לפנים OpenType-CFF אין נתיב קומפקטי בכלל. בנייה קומפקטית שנכשלת נופלת חזרה אל תת-הקובץ הדליל במקום להעלות חריגה. התכונה כבויה כברירת מחדל, כך שפלט קיים נשאר זהה בייט-בייט, ואילו תחת PDF/A לפנים ה-Unicode הרשומה תמיד מתקבל תת-קובץ קומפקטי
Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True; // שמיש בלי CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;
איך הכותב הארוז מצמצם את מבנה הקובץ?
ברגע שהפונטים והזרמים קטנים, המילונים ונתוני ה-cross-reference הופכים לעלות הגדולה שנותרה, ולכן הכותב של ה-object stream שמאחורי ה-CompressDocument גוזם גם אותם. המדריך ל-object streams ועדכונים מצטברים מכסה את פורמט המכל עצמו; נתיב הדחיסה מוסיף ארבעה שיפורים מעליו:
- תחביר קומפקטי לפי ISO 32000-1 §7.2.2: רווח נכתב רק בין שני token-ים שאחרת היו רצים יחד כתווים רגילים, כך ש-
/Type /Pageהופך ל-/Type/Page - שדות זרם ה-cross-reference מקבלים כל רוחב ש-§7.5.8.2 מרשה, כך שקובץ מתחת ל-16 MB מאחסן כל offset ב-3 בייטים במקום 4
- עד 250 אובייקטים נכנסים לכל object stream במקום 100 הרגילים, אלא אם תגדירו תקרה משלכם דרך
ConfigureAdaptiveObjectStreamPacking - כשהקובץ אינו מוצפן, ה-Catalog ומילון ה-Info נארזים גם הם אל object streams; פלט מוצפן משאיר אותם ברמה העליונה
התחביר הקומפקטי בא עם מלכודת ששווה להכיר אם אתם מרחיבים את הכותב. החתימה ממלאת את החתימה אחרי שהקובץ נכתב על ידי חיפוש הבייטים אחר ה-placeholder-ים המילוליים /ByteRange ( ו-/Contents <, ואיות קומפקטי היה הופך אותם ל-/ByteRange( ו-/Contents<, שהחיפוש לעולם לא מוצא. מילוני חתימה (Type Sig או DocTimeStamp, FT Sig) ומילון ההצפנה משמרים לכן את הפריסה המרווחת. פגם קשור פגע ב-build-ים לפני v2.766.41: כל שמירת object stream, ה-CompressDocument כלול, החלה בשתי שורות כותרת %PDF-, ולכן שדרגו אם ולידטור קפדני מסמן את הפלט שלכם
אפשר לדחוס PDF שכבר טעון?
כן, דרך ה-overload עם האפשרויות CompressLoadedDocument(Options, Info), שמריץ את אותם צעדים חסרי-אובדן על קובץ קיים. עם THPDFLoadedDocumentCompressionOptions.Default הוא מסיר משאבי עמוד שאינם בשימוש, ממזג פונטים וטפסים זהים, עושה subsetting לפונטים מוטמעים עם תתי-קבצים קומפקטיים דלוקים, דוחס מחדש זרמים לא מסוננים, Flate, LZW, ASCII ו-RunLength עם Flate כשהתוצאה קטנה יותר, וגורם לשמירה הבאה להשתמש ב-object streams. ה-HighRatioFlate כבוי כברירת מחדל, ו-object streams מדולגים עבור PDF/A-1 ושמירות מצטברות. ה-overload החסר-פרמטרים CompressLoadedDocument הוא הקריאה הישנה והצרה יותר שרק דוחסת Flate זרמים לא דחוסים
var
Doc: THotPDF;
Options: THPDFLoadedDocumentCompressionOptions;
Info: THPDFLoadedDocumentCompressionInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
Doc.LoadFromFile('quarterly-report.pdf');
Options := THPDFLoadedDocumentCompressionOptions.Default;
Doc.CompressLoadedDocument(Options, Info);
if Info.RefusedBySignaturePolicy then
Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
else
begin
Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
[Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
end;
finally
Doc.Free;
end;
end;
שני גבולות חשובים בנתיב הטעון. כל צעד כותב מחדש בייטים שחתימה מכסה, ולכן מסמך עם שדות חתימה נדחה כולו: הקריאה מחזירה 0, מגדירה את RefusedBySignaturePolicy ולא משנה כלום, אלא אם תגדירו AllowSignatureInvalidation, ואז ה-Info.SignaturesInvalidated מספר לכם על מה ויתרתם. ה-compaction גם שמרני יותר כאן מאשר בנתיב היצירה. HotPDF מקפל רק תוכניות פונט שנעשה בהן שימוש אך ורק על ידי פונטים CIDFontType2 עם /CIDToGIDMap של Identity, שבהן CID שווה ל-GID, ומדלג על תוכניות עם זרם מפה קיים, /CIDSet, או טבלאות glyph צבע כמו COLR, sbix, CBDT או SVG, כי בנייה קומפקטית מחדש הייתה משמיטה את שכבות הצבע. שימו לב גם שה-Info.BytesSaved מסכם רק את צעדי המשאבים, הפונטים והזרמים; הרווח של ה-object stream מופיע כשהקובץ נכתב
אילו תוצאות כדאי לצפות בפועל?
הרווחים עוקבים אחרי כמה מהקובץ הוא מבנה לא דחוס ונתוני פונט מנופחים, לא אחרי מספר העמודים שלו. הדוגמה בת שלושת העמודים עם Arial ו-SimSun התכווצה מ-10.2 MB ל-20 KB כשנוצרה עם CompressDocument, ומ-10.2 MB ל-19.8 KB כשהמקור הלא דחוס נטען והורץ דרך CompressLoadedDocument, עם רינדור זהה בשני הכיוונים. PDF שכבר קומפקטי בקושי זז: בקבוצת הרגרסיה, קבצים כאלה נשמרו בטווח -0.07% עד +0.06% מגודלם המקורי. קבצים עשירים בתמונות מרוויחים מעט, כי אף נתיב לא נוגע בנתוני התמונה
אם אתם מייצרים את אותם דוחות CJK כל לילה, שלבו תתי-קבצים קומפקטיים עם מטמון תתי-קבצי הפונט המתמיד על הדיסק כדי שעבודת ה-subsetting לא תחזור בכל ריצה, ובצעו diff בין פלטים דחוסים לפי תוכן אובייקט ולא לפי בייטים, כי שדה אחד שהשתנה מדחס Flate מחדש object stream שלם. הפניות מלאות לתכונות ולרשומות נמצאות בעמוד המוצר של רכיב HotPDF ל-Delphi PDF