מאמר טכני

זרמי אובייקטים ועדכונים מצטברים ב-Delphi עם HotPDF

PDF 1.5 הציגה שתי מבני אחסון שלפורמט הקובץ הקודם לא הייתה שום דרך לבטא: זרם האובייקטים ואת זרם ההפניה הצולבת. זרם אובייקטים הוא מיכל Flate-מכווץ אחד, מתויג /Type /ObjStm, שמחזיק אובייקטים עקיפים קטנים רבים ארוזים אחד אחרי השני במקום לפזרם בגוף הקובץ. זרם הפניה צולבת הוא טבלת החיפוש של הקובץ שנכתבה מחדש כבינארי דחוס עם שדות ברוחב משתנה, במקום הטבלת ASCII ברוחב קבוע שסגרה כל PDF עד לגרסה 1.4. הם נוסעים יחד. ברגע שאובייקטים מקופלים לתוך זרם, הטבלת הטקסט הישנה לא יכולה עוד לטפל בהם, כך שה-xref הבינארי חייב להגיע איתה

הצב זאת מול הפריסה הקלאסית והעלות שהיא מסירה קלה לראות. בקובץ PDF 1.4 כל אובייקט עקיף יושב לא-מכווץ מאחורי כותרת obj משלו, והטבלה בסוף מבזבזת בדיוק 20 בייטים של ASCII לכל ערך, כאשר כיווץ אסור. מסמך עם 200,000 אובייקטים נושא כ-4 MB של נתוני הפניה צולבת לפני שגליף אחד נצייר, עם כל גופי המילון הלא-מכווצים ערומים מעל. PDF 1.5 תוקף את שני המספרים בבת אחת: המילונים מתקפלים למכלי Flate, והטבלה של 4 MB מצטמצמת לכמה מאות קילובייטים של בינארי. ISO 32000-1 מגדיר את שני המבנים ב-§7.5.7 ו-§7.5.8

היכן החיסכון נוחת בפועל

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

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

ב-HotPDF אתה מפעיל את שניהם דרך זוג מאפיינים, וכיצד הם תלויים זה בזה חשוב יותר מהסדר שאתה כותב אותם:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.UseXRefStream := True;      // binary xref, prerequisite for ObjStm
    Pdf.UseObjectStreams := True;   // pack objects into /Type /ObjStm
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
    Pdf.EndDoc;                     // emits XRefStm + ObjStm containers
  finally
    Pdf.Free;
  end;
end;

UseObjectStreams צריך ש-UseXRefStream יהיה מוגדר ל-True. אובייקט מכווץ מגיע דרך ערך xref מסוג-2, שמתעד מספר זרם-אובייקטים פלוס אינדקס, ושורת טקסט קלאסית של 20 בייטים אין לה מקום לאחסן את הזוג הזה. לכן UseObjectStreams לבד לא עושה כלום גלוי; שני הדגלים, שהוגדרו לפני BeginDoc, הם התצורה שעובדת. הגדר אותם אחרי BeginDoc ו-HotPDF כבר התחייב לפריסה הישנה

מדוע שניהם כבויים כברירת מחדל

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

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

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

חתימה דיגיטלית מכסה /ByteRange מפורש: שתי ריצות של הקובץ הפיזי, שניתנו כקיזוזי בייטים מוחלטים, שעליהן נלקח CMS digest. כתוב מחדש את הקובץ, גם למשהו שנראה זהה על המסך, וכל הקיזוזים הללו זזים. ה-digest מפסיק להתאים והחתימה נקראת כשבורה. זו הבעיה המדויקת ש-ISO 32000-1 §7.5.6 פותר עם עדכונים מצטברים. אובייקטים חדשים ושונים מצורפים אחרי ה-%%EOF הקיים, ואז נכתב קטע הפניה צולבת חדש שערך ה-/Prev שלו מצביע חזרה על הקודם. הבייטים המקוריים לעולם לא מופרעים, כך שגרסה חתומה נשארת ניתנת לאימות ו-Acrobat יכול להציג כל גרסה חתומה בנפרד בלוח החתימות

HotPDF חושף זאת דרך נקודת הכניסה שלו:

Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf');  // appends the delta only

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

שינוי קובץ טעון עובר דרך LoadFromFile

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

PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5);  // pages 1-3 after page 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');

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

מתי לדחוס קובץ שהוצמד

שמירה append-only נושאת עלות אטית. עבודת לילה שמחתמת שורת סטטוס אחת על אותו PDF מייצרת 365 גרסאות במשך שנה, וכל גרסה גוררת קטע xref חדש אחריה. כאשר אותה היסטוריה חרגה מתועלתה, ואין חתימה בקובץ שצריכה לשרוד, אתה יכול לשטח את הכול על ידי סריאליזציה מחדש דרך נתיב המסמך-הטעון:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

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

בדיקת הפלט לפני שהוא יוצא

אימות זוג התכונות הזה הוא קונקרטי לרענן. פתח את התוצאה ב-Adobe Acrobat ואשר שלוש נקודות: מאפייני המסמך מדווחים PDF 1.5 ומאוחר יותר ברגע שזרמי אובייקטים פועלים; לוח החתימות עדיין מאמת כל גרסה חתומה קודמת לאחר עדכון מצטבר; ספירת דפים וסימניות עברו מחזור טעינה, שינוי ושמירה ללא פגע. לפלט ארכיון, דחוף גם את הקובץ דרך veraPDF, מכיוון ש-xref מכווץ הוא בדיוק סוג המבנה שאמת validator מחמיר בודקת יותר מקרוב מאשר מציג סלחני אי פעם יעשה. אם עבודתך כוללת גם קלטים גדולים מאוד, שיטות הבדיקה במאמר על ה-Direct File API לזרימות עבודה של קבצי PDF גדולים מצמדות טבעית לשמירה מצטברת, ומנגנוני החתימה מאחורי טווחי הבייטים לעיל מטופלים לעומק במאמר על חתימות דיגיטליות ו-PAdES של HotPDF

שתי התכונות מגיעות כחלק מHotPDF Component עבור Delphi ו-C++Builder, לצד ה-APIs של גנרציה, טפסים, הצפנה וחתימה המכוסים במקומות אחרים בבלוג זה; דף המוצר מקשר את הפניית ה-API המלאה אם אתה רוצה להסדיר את הקריאות לעיל מול צינור המסמך שלך