losLab PDF Library יכולה להפיק פלט PDF זהה בייט-לבייט עבור קלט זהה ברגע שאתה קורא ל-SetDeterministicDocumentID(1). כברירת מחדל, מערך ה-/ID של ה-trailer הוא תקציר MD5 של שעון הקיר, כך ששני הרצות של אותו מחולל נבדלות לפחות בבייטים האלה. מצב דטרמיניסטי גוזר את /ID מזרע יציב במקום זאת, מה שמשחזר בנייה ניתנת לשחזור
התסמין בדרך כלל מופיע ב-CI לפני שמישהו מתחיל לחפש אותו. התבנית לא השתנתה, רשומת הקלט לא השתנתה, הגופנים לא השתנו, ופלט ה-PDF שנוצר עדיין מתגבב אחרת בכל הרצת pipeline. מטמוני בנייה אף פעם לא פוגעים. אחסון בכתובת-תוכן צובר blob חדש לכל בנייה לילית. השוואות רגרסיה ברמת הבייט נדלקות על קבצים שאף אחד לא נגע בהם. עקוב אחרי ההבדל עד הבייטים בפועל וזה כמעט תמיד אותה קומץ ספרות הקסדצימליות ששוכבות ב-trailer של הקובץ
למה מיועד מערך ה-ID של ה-trailer?
ה-/ID של ה-trailer הוא סמן זהות קובץ, לא checksum של התוכן. תקן ISO 32000-1 §14.4 מגדיר אותו כמערך של שני מחרוזות בייטים: האלמנט הראשון הוא המזהה הקבוע שהוקצה כשהמסמך נוצר ואמור לשרוד כל עריכה מאוחרת יותר, והאלמנט השני הוא המזהה המשתנה שכותב מרענן בכל פעם שהקובץ משתנה. יחד הם מאפשרים למערכת להחליט אם שני קבצים הם גרסאות של מסמך אחד או שני מסמכים לא קשורים. §7.5.5 הופך את הרשומה לחובה כמעט בפועל, כיוון שה-trailer חייב לשאת /ID בכל פעם שהוא נושא גם /Encrypt
שום דבר בתקן לא אומר איך לחשב את הערך. ההמלצה היא תקציר של דברים כמו הזמן הנוכחי, נתיב הקובץ, גודל הקובץ ומילון מידע המסמך, ושעון הקיר הוא המרכיב שהופך את התוצאה לייחודית. זו בדיוק התכונה שאתה רוצה לזהות, וזו בדיוק התכונה שהורסת יכולת שחזור, וזו הסיבה שזה חייב להיות מתג מפורש ולא שינוי התנהגות שקט
למה אותה בנייה מפיקה PDF שונה בכל פעם?
מפני שהמזהה הברירת-מחדל נגזר מרגע היצירה. באופן היסטורי losLab PDF Library בנתה את מחרוזות ה-/ID מ-MD5 של חותמת הזמן הנוכחית, כך שמסמך שנוצר פעמיים בהפרש שנייה אחת נושא שני מזהים קבועים שונים גם כשכל בייט אחר בקובץ זהה. העלות במורד הזרם אמיתית: מערכת בנייה שממפתחת artifacts לפי hash אף פעם לא יכולה לעשות שימוש חוזר בשלב PDF, מאגר אובייקטים דה-דופליקטיבי שומר עותק אחד לכל בנייה במקום עותק אחד לכל מסמך, וסוקר שמסתכל על diff בינארי צריך להוכיח שהשינוי היחיד הוא רעש לפני שהוא סומך על שאר ה-diff. יצירת /ID דטרמיניסטית קיימת כדי להסיר את הרעש הזה, באותה רוח כמו עבודת יציבות הפריסה המתוארת בהערות על זרמי אובייקטים וזרמי טבלת הפניה צולבת
מעבר למזהה בר-שחזור
מצב דטרמיניסטי הוא opt-in, למסמך, וכבוי כברירת מחדל כך שפלט קיים נותר ללא שינוי עד שתבקש זאת. SetDeterministicDocumentID מקבל 0 או 1 ומחזיר 1 כשהערך התקבל, 0 עבור כל דבר מחוץ לטווח; GetDeterministicDocumentID מדווח על המצב הנוכחי. SetDocumentIDSeed מספק מחרוזת זרע מפורשת שגוברת על הכול, והעברת זרע ריק חוזרת לזרע הנגזר. GetDocumentFileID קורא בחזרה את /ID[0] אחרי השמירה כך שתוכל לרשום אותו ביומן או לבדוק אותו
var
Lib: TPDFlib;
FileID: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed('invoice-4471-rev3');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Invoice 4471');
Lib.SaveToFile('invoice.pdf');
FileID := Lib.GetDocumentFileID; // identical on every run
finally
Lib.Free;
end;
end;
הרענון קורה בזמן השמירה, לא כשאתה מהפך את הדגל, כך שהפעלת מצב דטרמיניסטי מאוחר בבניית מסמך עדיין נכנסת לתוקף. זה גם אומר שזרע ששונה מגיע לקובץ בשמירה המלאה הבאה: קבע זרע A, שמור, קבע זרע B, שמור, ושני הקבצים נושאים מזהים שונים, בעוד שחזרה לזרע A משחזרת את הערך המקורי. זרע מפורש הוא הבחירה הנכונה בכל פעם שלמסמך שלך יש מפתח יציב טבעי כמו מספר חשבונית, גרסת רשומה או מזהה commit של git, כי הוא מנתק את המזהה ממטא-נתונים אקראיים
מאיפה מגיע הזרע כשאינך מספק אחד?
ללא זרע מפורש, losLab PDF Library גוזרת אחד ממצב המסמך שאמור להיות בלתי-משתנה על פני יצירות מחדש זהות: כותרת גרסת ה-PDF, מספר העמודים, וכל רשומה במילון מידע המסמך. ערכי מחרוזת ושם נלקחים כלשונם, סוגי אובייקטים אחרים תורמים את הצורה המסודרת שלהם, וכל זה מתגבב לתוך מחרוזות ה-/ID. התוצאה החשובה היא ש-CreationDate ו-ModDate הם חלק ממילון המידע ולכן חלק מהזרע בכוונה. שתי הרצות מרוויחות אותו מזהה רק כשהן מפיקות באמת את אותם מטא-נתוני מסמך
Lib.SetDeterministicDocumentID(1);
// No SetDocumentIDSeed: the seed is derived from document state,
// so the timestamps in the Info dictionary have to be pinned.
Lib.SetInformation(2, 'Quarterly Report'); // Title
Lib.SetInformation(5, 'reporting-service 4.2'); // Creator
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SaveToFile('report.pdf');
נעיצת ModDate עם מפתח 8 עושה כפל תפקיד, וזה החלק שתופס אנשים לא מוכנים. /ID דטרמיניסטי לבדו לא הופך את הקובץ לזהה בייט-לבייט, כי נתיב השמירה מטביע את ModDate עם הזמן הנוכחי אלא אם הקורא קבע אותו במפורש. קביעת מפתח 8 מסמנת את הערך כמסופק על ידי הקורא ומדכאת את ההטבעה הזו. אם אתה רוצה קובץ בר-שחזור ולא רק מזהה בר-שחזור, התייחס לחותמות זמן של מטא-נתונים כאל קלטי בנייה: גזור אותן מרשומת המקור או מ-epoch קבוע, לעולם לא מ-Now
למה כתיבה מחדש של ה-ID שוברת PDF מוצפן?
מפני ש-/ID[0] אינו רק מטא-נתונים במסמך מוצפן, הוא חומר מפתח. תקן ISO 32000-1 §7.6.3.3 אלגוריתם 2 מזין את האלמנט הראשון של מזהה הקובץ לתוך חישוב מפתח ההצפנה עבור מטפל האבטחה הסטנדרטי ברוויזיות 2 עד 4, לצד הסיסמה המרופדת, הערך /O וסיביות ההרשאה. המפתח הנגזר מפיק אז את מחרוזת האימות /U שקורא בודק בפתיחה, ומפתח הקובץ נגזר ונשמר במטמון כשאתה קורא ל-Encrypt או כשמסמך מוצפן נטען, שני הדברים קורים לפני השמירה. כתיבה מחדש של המזהה בזמן השמירה תפיק לכן קובץ תקין מבחינה מבנית שבדיקת ה-/U שלו נכשלת בפתיחה מחדש: לא שחיתות עדינה אלא מסמך שאף אחד לא יכול לפתוח, כולל אתה. זו הסיבה שהרענון הדטרמיניסטי מוגבל למסמכים שלא נושאים מצב הצפנה, ומדוע מסמך מוצפן שומר על כל /ID שכבר היה לו, מצב דטרמיניסטי או לא, וההגדרה פשוט לא משפיעה על הנתיב הזה. הטיפול ברוויזיות הקשורות וסמנטיקת ההרשאות מכוסים במדריך ביקורת הצפנה והרשאות PDF. שים לב גם שנתיב שחזור ההצפנה מרענן רק את /ID[1], מזהה השינוי, בדיוק כפי ש-§14.4 מתכוון
למה שמירות incremental שומרות על המזהה המקורי
הגבול השני הוא מצב append. עדכון incremental משאיר כל בייט קודם של הקובץ ללא שינוי וכותב רוויזיה חדשה אחריו, ותמידות /ID[0] לאורך §14.4 היא מה שאומר לצרכן שהרוויזיה החדשה שייכת לאותו מסמך כמו הישנה. כתיבה מחדש שלו הייתה מנתקת את הקישור הזה, סותרת את הרוויזיות שכבר יושבות בקובץ, ומפריעה לסמנטיקת חתימה, כיוון שחתימה מכסה טווח בייטים של רוויזיה ספציפית של מסמך ספציפי. losLab PDF Library לכן מרעננת את המזהה הדטרמיניסטי רק בשמירות מלאות ואף פעם לא במצב append, מה שמשמר את הערבות המתוארת במאמר על עדכוני PDF incremental והוספה לזרם שלמה
נקודת צוואר בקבוק אחת ליצירת מזהים
כל יצירת /ID ב-losLab PDF Library עוברת כעת דרך שגרה פנימית יחידה, NewFileIDString, וזה מה שהופך את המתג הדטרמיניסטי לאמין ולא סתם טלאי על נתיב קוד אחד. יצירת מסמך ריק, יצירה עצלה של מערך /ID חסר לפי דרישה, ונתיב שחזור טביעת האצבע של ההצפנה כולם קוראים לה, כך שיש בדיוק מקום אחד שבו שעון הקיר עלול לדלוף בחזרה. זה גם אומר שגרסאות עתידיות, כמו מזהה נגזר-תוכן, הן שינוי בפונקציה אחת ולא ביקורת של כל המסדרן
function BuildQuote(const Seed: WideString): AnsiString;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(Seed);
Lib.SetInformation(7, 'D:20260101000000Z');
Lib.SetInformation(8, 'D:20260101000000Z');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Quote 8812');
Result := Lib.SaveToString;
finally
Lib.Free;
end;
end;
// Regression guard: two independent builds, one byte sequence.
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
WriteLn('reproducible')
else
WriteLn('nondeterminism leaked into the output');
חבר את ההשוואה הזו לתוך חבילת הבדיקות שלך לפני שאתה סומך על פלט בר-שחזור בכל מקום אחר, כי היא נכשלת בקול רם ברגע שתכונה חדשה כלשהי מכניסה מחדש חותמת זמן. יכולת שחזור היא תכונה שדועכת בשקט אחרת, ואישור בודד על שתי שמירות בזיכרון עולה כמעט כלום להריץ בכל בנייה
ה-API של המזהה הדטרמיניסטי המוצג כאן משתלב עם losLab PDF Library עבור דלפי ו-C++Builder, לצד הפניית מידע המסמך, ההצפנה והשמירה ה-incremental המלאה