עדכוני PDF תוספתיים (incremental updates) מאפשרים לאפליקציית דלפי לשנות מסמך על ידי הוספת האובייקטים שהשתנו בלבד, תוך השארת כל בית מקורי ללא שינוי. ספריית PDF של losLab מממשת זאת באמצעות AppendToStream, הכותבת רק את חלק התוספת המוגדר ב-ISO 32000-1 §7.5.6, כך שעריכת סימנייה אחת בקובץ של 2GB עולה קילובייטים בודדים של פלט במקום שכתוב מלא. מנגנון זה הוא גם הסיבה לכך שניתן לעדכן מסמכים חתומים מבלי לבטל את תוקף החתימות שלהם
הבעיה שזה פותר היא קונקרטית. שמירה מלאה משכתבת את כל הקובץ: כל אובייקט עובר סדרה מחדש (re-serialization), כל היסט של הפניה צולבת מחושב מחדש, והפלט אינו נושא שום קשר ברמת הבתים לקלט. עבור חשבונית של 40KB זה בסדר. עבור ארכיון סרוק של 2GB שבו תיקנתם רק שגיאת הקלדה בכותרת המסמך, שכתוב של שני ג'יגה-בייט כדי לשנות עשרים בתים הוא אבסורדי — ואם הקובץ נשא חתימה דיגיטלית, השכתוב פשוט הרס אותה
מדוע שמירת PDF שוברת את החתימה הדיגיטלית שלו?
חתימה דיגיטלית של PDF אינה חותמת על התוכן הלוגי של המסמך; היא חותמת על טווחי בתים (byte ranges) של הקובץ הפיזי. ערך ה-/ByteRange במילון החתימה מתעד בדיוק אילו חלקים של הקובץ מכוסים על ידי הסיכום הקריפטוגרפי. כל פעולת שמירה המבצעת סדרה מחדש של בתים אלה — אפילו כזו שמייצרת מסמך זהה מבחינה סמנטית — משנה את הסיכום, וכל מאמת ידווח על החתימה כשבורה. זהו חלק מהתכנון: החתימה מעידה על הבתים שהחותם ראה, ולא על מודל מסמך מופשט כלשהו
עדכוני תוספת הם פתח המילוט שמפרט ה-PDF מספק. מכיוון ששמירה תוספתית מוסיפה נתונים חדשים לאחר ה-%%EOF המקורי ולעולם אינה נוגעת בטווחי הבתים החתומים, החתימה הקיימת ממשיכה להיות מאומתת מול הבתים שהיא מכסה. מאמתים מסווגים לאחר מכן את השינויים שנוספו בנפרד — חתימה שנייה, מילוי טפסים, הערת שוליים — ומחליטים אם הם שינויים מותרים. כל זרימת עבודה מרובת חתימות תלויה בכך: כל חותם מוסיף קטע תוספת על גבי האחרון. אם אתם בונים צינורות חתימה, המאמר המלווה על חתימה ואימות PAdES בדלפי מכסה בפירוט כיצד טווחי בתים של חתימה וקטעי תוספת מקיימים אינטראקציה
כיצד פועלים עדכונים תוספתיים תחת תקן ISO 32000-1 §7.5.6
תקן ISO 32000-1 §7.5.6 מגדיר את המודל בשלושה כללים. ראשית, תוכן הקובץ המקורי נותר ללא שינוי לחלוטין — אף בית אינו זז. שנית, אובייקטים ששונו ואובייקטים חדשים שנוצרו מתווספים לאחר ה-%%EOF האחרון, כל אחד עם אותו מספר אובייקט שהיה לו קודם (אובייקטים ששונו פשוט מקבלים הגדרה חדשה יותר המאפילה על הישנה). שלישית, מתווספים קטע הפניה צולבת חדש וטריילר (trailer); ערך ה-/Prev של הטריילר מצביע בחזרה להיסט הבתים של קטע ההפניה הצולבת הקודם, ויוצר שרשרת שהקורא עובר עליה מהחדש ביותר לישן ביותר כדי לפענח כל אובייקט להגדרה העדכנית ביותר שלו
שתי תכונות שימושיות נובעות ממבנה זה. העדכונים זולים ביחס למה שהשתנה, ולא ביחס לגודל המסמך — עלות התוספת היא גודל האובייקטים ששונו בתוספת תקורה קטנה של xref/trailer. והקובץ הופך להיסטוריית הגרסאות של עצמו: כל גרסה קודמת עדיין נוכחת פיזית, כך שמבקר יכול לקטום את הקובץ בכל %%EOF מוקדם יותר ולשחזר בדיוק את המסמך שהיה קיים באותה נקודה. עבור זרימות עבודה של תאימות החייבות להוכיח כיצד נראה מסמך לפני כל תיקון, נתיב ביקורת מובנה זה הוא לעיתים קרובות הטיעון המכריע לטובת שמירה תוספתית
כתיבת עדכון תוספתי באמצעות AppendToStream
ספריית PDF של losLab חושפת פלט תוספתי באמצעות AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, המחזירה 1 בהצלחה ו-0 בכישלון. הפרמטר AppendMode קובע מה ייכתב לזרם היעד. מצב 0 (Mode 0) כותב קובץ שלם: בתי המקור הראשוניים מועתקים תחילה לזרם, ולאחר מכן מתווסף קטע התוספת. מצב 1 כותב רק את קטע התוספת עצמו — הדלתא — ומדלג לחלוטין על בתי המקור. מצב 2 כותב תחילה קידומת שסופקה על ידי הקורא ונרשמה באמצעות SetAppendInputFromString, ואז מוסיף את קטע העדכון מעליו
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Small edit: the kind of change that should not
// trigger a rewrite of the whole file
Doc.SetInformation(3, 'Amended 2026-07-04'); // key 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: write only the incremental section.
// Original bytes + Delta = a complete, valid PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
מצב 1 הוא המעניין ביותר לתכנון מערכות. מכיוון שהדלתא עצמאית, ניתן להעביר אותה ללא תלות בקובץ המקורי: לאחסן גרסאות כקבצי blob נפרדים באחסון אובייקטים, לשכפל רק דלתאות לאתר מרוחק, או לשחזר כל גרסה על ידי שרשור קובץ הבסיס עם שרשרת התוספות שלו. כלל השחזור הוא שרשור בתים פשוט — הקובץ המקורי תחילה, ולאחר מכן כל דלתא לפי הסדר — מכיוון שזהו בדיוק המבנה שתקן §7.5.6 מכתיב עבור קובץ שעודכן תוספתית
כיצד הספרייה מחשבת את היסטי ה-xref מבלי להעתיק את הקובץ המקורי?
רשומות ההפניה הצולבת (cross-reference) בתוך קטע תוספת חייבות להכיל היסטי בתים מוחלטים — מיקומים הנמדדים מתחילת המסמך המלא, ולא מתחילת הדלתא. זה מייצר אתגר עבור מצב 1: הכותב לעולם אינו פולט את הבתים המקוריים, אך כל היסט שהוא מתעד חייב להעמיד פנים שהם שם. ספריית PDF של losLab פותרת זאת באמצעות מתאם זרם פנימי, TPDFAppendSectionStream, המציג מרחב קואורדינטות וירטואלי למסדר הנתונים (serializer). המתאם נוצר עם אורך הבתים של הקובץ המקורי כהיסט הבסיס שלו, מדווח על המיקום והגודל שלו כבסיס זה בתוספת כל מה שנוסף עד כה, ומעביר רק את הבתים החדשים שנכתבו לזרם היעד של הקורא
התוצאה היא שמצב 1 לעולם אינו מייצר עותק פיזי של מסמך המקור — לא בדיסק ולא בזיכרון. המימוש הנאיבי (כתיבת הקובץ המלא לבאפר זמני, ואז חיתוך הזנב) היה דורש עותק זמני של כל ה-PDF המקורי, שבמקרה של קלטים בקנה מידה של גיגה-בייטים הוא בדיוק העלות שעדכונים תוספתיים נועדו למנוע. טכניקת וירטואליזציית היסט זו היא קרובת משפחה של הסטת הפניות בתים (byte reference shifting) המשמשת במקומות אחרים בספרייה; המאמר על מיזוג PDF מהיר עם הסטת הפניות בתים מציג את אותו רעיון המיושם על שילוב מסמכים, והמדריך למיזוג ופיצול PDF גדול עם גישה ישירה לקבצים מכסה את ארכיטקטורת ה-I/O הסובבת עבור קבצים שאינם נכנסים בנוחות ל-RAM
הזרמת שמירות מלאות באמצעות SaveToStream
פלט תוספתי הוא חצי מסיפור ההזרמה; החצי השני הוא מה שקורה בשמירה מלאה. הפונקציה SaveToStream בספריית PDF של losLab מפעילה את מסדר הנתונים של המסמך ישירות מול זרם היעד, במקום לרנדר תחילה את כל המסמך לתוך AnsiString זמני ואז לכתוב את הבאפר הזה בקריאה אחת. הגישה הישנה עבדה, אך פירושה היה שכל שמירה מלאה החזיקה באופן זמני עותק שלם שני של הפלט בזיכרון — לא מזיק ב-10MB, כואב ב-500MB, ומחסום קשיח לפלטים של מספר גיגה-בייטים בתהליכי 32 סיביות. סדרה ישירה גורמת לזיכרון השיא לעקוב אחר מבני האובייקטים של המסמך במקום אחר אורכו הסדרתי
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... edits that justify a full rewrite ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('Save failed, error ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
שיעור במצב שיתוף: כאשר AppendToFile החזיר 0
רגרסיה אחת בתחום זה ראויה לסיפור מחדש מכיוון שדפוס הכשל משליך על מקרים רבים. AppendToFile(FileName) מוסיף עדכון תוספתי ישירות ל-PDF קיים בדיסק — הקריאה הטבעית עבור זרימת עבודה של נתיב ביקורת במקום: טעינת קובץ, ביצוע שינוי, והוספה לאותו נתיב. בגרסה v3.71.2 רצף מדויק זה החל להחזיר 0. שורש הבעיה היה בטוען (loader), לא בכותב: כדי לתמוך בקריאה לפי דרישה של מסמכים גדולים, LoadFromFile שומר על מזהה הקובץ (handle) פתוח למשך כל מחזור החיים של אובייקט המסמך, ומזהה זה נפתח עם fmShareDenyWrite. כאשר AppendToFile ניסה לאחר מכן לפתוח מחדש את אותו קובץ לכתיבה, מצב השיתוף של הטוען עצמו מנע זאת, וה-API נכשל לפני כתיבת בית אחד
התיקון הקל על מצב השיתוף של הטוען ל-fmShareDenyNone, וזה בטוח בדיוק בגלל מה שתוספת תוספתית מייצגת: היא מוסיפה בתים אך ורק אחרי סוף הקובץ ולעולם אינה משכתבת את האזור שמזהה הקובץ ארוך-החיים של הקורא משרת. השיעור הכללי עבור כל מי שעוטף ספרייה זו — או בונה טועני הזרמה דומים — הוא שקוראים עצלנים המחזיקים במזהים וכותבים לאותו קובץ נמצאים במתח, ומצב השיתוף שאתם בוחרים בזמן הפתיחה הוא חוזה API, לא פרט מימוש. אם AppendToFile מחזיר אי פעם 0 בקוד שלכם, בדקו תחילה אם משהו אחר בתהליך שלכם עדיין מחזיק בקובץ היעד עם מצב שיתוף מגביל
העלויות האמיתיות: מתי עדכונים תוספתיים הם הכלי הלא נכון
עדכונים תוספתיים מחליפים את גודל הקובץ ביעילות כתיבה, והעסקה הזו לא תמיד כדאית. כל גרסה מוסיפה את האובייקטים ששונו בעוד שההגדרות שהוחלפו נשארות בקובץ, כך שמסמך שנערך מאות פעמים צובר אובייקטים מתים ושרשרת /Prev ארוכה שכל קורא חייב לעבור עליה. גרוע מכך, תוכן "מחוק" אינו נעלם: טקסט שהוסר בגרסה חמש עדיין נוכח פיזית בבתים של גרסה ארבע, וניתן לשחזור על ידי כל מי שיקטום את הקובץ. לכן, השחרה (redaction), חיטוי או כל הסרה של תוכן רגיש דורשים שכתוב מלא — שמירה תוספתית של השחרה היא דליפת נתונים במסווה
שמירה מלאה היא גם ההחלטה הנכונה כאשר המטרה היא דחיסה (צמצום של תוספות שהצטברו ואובייקטים שאינם בשימוש), בעת שינוי תכונות רחבות-מסמך כגון הצפנה — הצפנה מחדש נוגעת בכל מחרוזת וזרם, כך שלא נשאר שום דבר "תוספתי" בשינוי — או בעת הפקת תוצר נקי שבו היסטוריית העריכה אינה צריכה ללוות את הקובץ. כלל סביר: השתמשו ב-AppendToStream או ב-AppendToFile כל עוד מסמך פעיל ומשתנה, במיוחד לאחר שהוא נושא חתימות; השתמשו בשכתוב מלא של SaveToStream בגבולות מחזור חיים, כאשר המסמך עוזב את המערכת שלכם או כאשר יש לשטח את ההיסטוריה שלו
עדכונים תוספתיים, פלט דלתא עם היסט וירטואלי וסדרה ישירה לזרם הם כולם חלק מ-losLab PDF Library הסטנדרטית עבור דלפי, C# ו-VB.NET; דף המוצר מפרט את כל ממשק ה-API לשמירה ותוספת, לצד תכונות החתימה והקבצים הגדולים שנדונו לעיל