يمكن لمكتبة losLab PDF Library إنتاج إخراج PDF متطابق بايتًا لمدخل متطابق بمجرد استدعاء SetDeterministicDocumentID(1). افتراضيًا، مصفوفة /ID في التذييل هي ملخص MD5 لساعة النظام، لذا يختلف تشغيلان لنفس المولّد على الأقل في تلك البايتات. الوضع الحتمي يشتق /ID من بذرة ثابتة بدلًا من ذلك، مما يعيد بناءات قابلة لإعادة الإنتاج
عادةً ما تظهر العَرَض في بيئة التكامل المستمر (CI) قبل أن يبدأ أحد بالبحث عنه. القالب لم يتغير، وسجل المدخل لم يتغير، والخطوط لم تتغير، وملف PDF المُولَّد لا يزال يُنتج قيمة تجزئة مختلفة في كل تشغيل لخط الأنابيب. ذاكرات التخزين المؤقت للبناء لا تُصيب أبدًا. التخزين القابل للعنونة بالمحتوى يراكم كتلة جديدة لكل بناء ليلي. فروقات الانحدار على مستوى البايت تُضيء لملفات لم يلمسها أحد. تتبّع الفرق حتى البايتات الفعلية وستجد في الغالب نفس الحفنة من الأرقام السداسية العشرية الجالسة في تذييل الملف
ما الغرض من مصفوفة معرّف التذييل؟
/ID في التذييل هو علامة هوية ملف، وليس مجموع تحقق للمحتوى. تُعرّف الفقرة §14.4 من ISO 32000-1 هذا العنصر بأنه مصفوفة من سلسلتي بايت: العنصر الأول هو المعرّف الدائم الذي يُخصَّص عند إنشاء المستند ويُفترض أن يبقى ثابتًا عبر كل تعديل لاحق، والعنصر الثاني هو المعرّف المتغيّر الذي يُحدّثه الكاتب في كل مرة يُعدَّل فيها الملف. معًا، يتيحان لنظام ما تحديد ما إذا كان ملفان مراجعتين لمستند واحد أم ملفين غير مرتبطين. تجعل الفقرة §7.5.5 هذا العنصر إلزاميًا فعليًا في الممارسة، إذ يجب أن يحمل التذييل /ID كلما حمل أيضًا /Encrypt
لا شيء في المواصفة يحدد كيفية حساب القيمة. التوصية هي ملخص لعناصر مثل الوقت الحالي، ومسار الملف، وحجم الملف، وقاموس معلومات المستند، وساعة النظام هي المكوّن الذي يجعل النتيجة فريدة. هذه بالضبط الخاصية التي تريدها للهوية، وبالضبط الخاصية التي تُفسد إمكانية إعادة الإنتاج، ولهذا يجب أن يكون هذا مفتاحًا صريحًا وليس تغييرًا صامتًا في السلوك
لماذا يُنتج نفس البناء ملف PDF مختلفًا في كل مرة؟
لأن المعرّف الافتراضي يُشتق من لحظة التوليد. تاريخيًا، كانت مكتبة losLab PDF Library تبني سلاسل /ID من ملخص MD5 للطابع الزمني الحالي، لذا فإن مستندًا أُنشئ مرتين بفارق ثانية واحدة يحمل معرّفين دائمين مختلفين حتى مع تطابق كل بايت آخر في الملف. التكلفة النهائية حقيقية: نظام بناء يفهرس القطع الأثرية بالتجزئة لا يمكنه أبدًا إعادة استخدام خطوة PDF، ومخزن كائنات يزيل التكرار يحتفظ بنسخة واحدة لكل بناء بدلًا من نسخة واحدة لكل مستند، والمراجع الذي ينظر إلى فرق ثنائي عليه إثبات أن التغيير الوحيد هو ضوضاء قبل الثقة ببقية الفرق. توليد /ID الحتمي موجود لإزالة تلك الضوضاء، بنفس روح عمل استقرار التخطيط الموصوف في ملاحظات دفقات الكائنات ودفقات جدول المراجع المتقاطعة
التبديل إلى معرّف قابل لإعادة الإنتاج
الوضع الحتمي اختياري، لكل مستند على حدة، ومعطّل افتراضيًا بحيث يبقى الإخراج الحالي بلا تغيير حتى تطلبه. تقبل 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 يُميّز القيمة على أنها موفَّرة من المستدعي ويكبح ذلك الختم. إذا أردت ملفًا قابلًا لإعادة الإنتاج وليس مجرد معرّف قابل لإعادة الإنتاج، عامِل الطوابع الزمنية للبيانات الوصفية كمدخلات بناء: اشتقها من السجل المصدر أو من عهد ثابت، لا من Now أبدًا
لماذا تُفسد إعادة كتابة المعرّف ملف PDF مشفّرًا؟
لأن /ID[0] ليس مجرد بيانات وصفية في مستند مشفّر، بل هو مادة مفتاح. تُدخل الخوارزمية 2 في الفقرة §7.6.3.3 من ISO 32000-1 العنصر الأول من معرّف الملف في حساب مفتاح التشفير لمعالج الأمان القياسي في المراجعات 2 حتى 4، جنبًا إلى جنب مع كلمة المرور المحشوّة، وقيمة /O، وبتات الأذونات. المفتاح المشتق ينتج بعد ذلك سلسلة التحقق /U التي يتحقق منها القارئ عند الفتح، ويُشتق مفتاح الملف ويُخزَّن مؤقتًا عند استدعاء Encrypt أو عند تحميل مستند مشفّر، وكلاهما يحدث قبل الحفظ. إعادة كتابة المعرّف أثناء الحفظ ستُنتج إذن ملفًا صحيحًا بنيويًا لكن فحص /U فيه يفشل عند إعادة الفتح: ليس تلفًا خفيًا بل مستند لا يستطيع أحد فتحه، بمن فيهم أنت. لهذا السبب يقتصر التحديث الحتمي على المستندات التي لا تحمل حالة تشفير، ولهذا يحتفظ المستند المشفّر بأي /ID كان يحمله بالفعل، سواء كان الوضع الحتمي مفعلًا أم لا، والإعداد ببساطة ليس له أي تأثير على ذلك المسار. معالجة المراجعات ودلالات الأذونات ذات الصلة مشروحة في شرح تدقيق تشفير PDF والأذونات. لاحظ أيضًا أن مسار استعادة التشفير يُحدّث فقط /ID[1]، معرّف التغيير، تمامًا كما تقصد §14.4
لماذا تحتفظ عمليات الحفظ التزايدية بالمعرّف الأصلي؟
الحد الثاني هو وضع الإلحاق. التحديث التزايدي يترك كل بايت سابق من الملف بلا مساس ويكتب مراجعة جديدة بعده، ودوام /ID[0] عبر §14.4 هو ما يُخبر المستهلك بأن المراجعة الجديدة تنتمي إلى نفس المستند الذي تنتمي إليه المراجعة القديمة. إعادة كتابته ستقطع تلك الصلة، وتناقض المراجعات الموجودة بالفعل في الملف، وتتداخل مع دلالات التوقيع، لأن التوقيع يغطي نطاق بايتات لمراجعة محددة من مستند محدد. لذا تُحدّث مكتبة losLab PDF Library المعرّف الحتمي فقط عند الحفظ الكامل، ولا تفعل ذلك أبدًا أثناء وضع الإلحاق، مما يحافظ على الضمان الموصوف في مقال التحديثات التزايدية لـ PDF والإلحاق إلى الدفق سليمًا
نقطة اختناق واحدة لتوليد المعرّفات
كل توليد لـ /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');
اربط هذه المقارنة بمجموعة اختباراتك قبل أن تعتمد على إخراج قابل لإعادة الإنتاج في أي مكان آخر، لأنها تفشل بصوت عالٍ في اللحظة التي تُعيد فيها ميزة جديدة إدخال طابع زمني. إمكانية إعادة الإنتاج خاصية تتآكل بصمت لولا ذلك، ومقولة واحدة على حفظين في الذاكرة تكلّف تقريبًا لا شيء لتشغيلها في كل بناء
واجهة المعرّف الحتمي البرمجية الموضحة هنا تُشحن مع مكتبة losLab PDF Library لـ Delphi وC++Builder، إلى جانب المرجع الكامل لمعلومات المستند والتشفير والحفظ التزايدي