Technical Article

التحقق من صحة ملفات PDF المضغوطة: مجاري الكائنات وXRef

تكتب مدققاً صغيراً. يفتح ملف PDF، ويبحث عن النهاية، ويعثر على startxref، ويقرأ الإزاحة، ويتوقع أن يهبط على الكلمة الرئيسية xref مع جدول إسناد ترافقي ذي عرض ثابت تحته. ومن هذا الجدول يجمع إزاحات الكائنات، ثم يمسح للخلف للبحث عن الكلمة الرئيسية trailer لمعرفة /Root و/Size. إنه يعمل بشكل مثالي على كل ملف قمت بإنشائه لاختباره. ثم يصل ملف تم إنتاجه بواسطة إصدار حديث من Word، أو بواسطة مكتبة تستهدف PDF 1.5، ويعلن المدقق أنه معيب. لا توجد كلمة رئيسية xref حيث تشير الإزاحة، ولا يوجد قاموس trailer في أي مكان، وجدول الكائنات الذي بناه المدقق فارغ تقريباً. الملف صالح. المدقق يقرأه من خلال عدسة عمرها خمسة عشر عاماً.

هذا هو السبب الفردي الأكثر شيوعاً لفشل فحص مستوى البايت لـ PDF المكتوب مقابل التخطيط الكلاسيكي على المستندات الحديثة. البنية التي يعتمد عليها، جدول الإسناد الترافقي النصي والكلمة الرئيسية trailer، أصبحت اختيارية في PDF 1.5 وغالباً ما تكون غائبة. وحلت ميزتان محلها: مجرى الإسناد الترافقي (cross-reference stream) ومجرى الكائنات المضغوط (compressed object stream). كلاهما موصوف في ISO 32000-1، والمدقق الذي لا يعرف عنهما يرى الملف السليم كمجموعة من الكائنات المفقودة.

ما الذي غيره PDF 1.5 بشأن ذيل الملف

يحدد القسم 7.5.8 من ISO 32000-1 مجرى الإسناد الترافقي، ويحدد القسم 7.5.7 مجرى الكائنات من نوع /ObjStm. معاً، يسمحان للكاتب بإسقاط البنيتين اللتين يبحث عنهما المحلل الكلاسيكي. قد ينتهي ملف PDF 1.5 بدون جدول xref على الإطلاق. وبدلاً منه، يكون الكائن الذي يشير إليه startxref عبارة عن كائن مجرى عادي يحمل قاموسه القيمة /Type /XRef، ويحتفظ هذا المجرى ببيانات الإسناد الترافقي في شكل ثنائي مدمج. ولا توجد كلمة رئيسية trailer أيضاً، لأن المقطورة (trailer) هي الآن قاموس المجرى نفسه. المفاتيح التي يبحث عنها المحلل الكلاسيكي، /Root و/Size و/ID، تعيش داخل ذلك القاموس.

التغيير الثاني ينقل الكائنات نفسها. بدلاً من كتابة كل كائن غير مباشر عند إزاحة البايت الخاصة به، يمكن للكاتب حزم العديد من الكائنات الصغيرة، قواميس الصفحات، وقواميس التعليقات التوضيحية، وشجرة البنية، في مجرى كائنات واحد وضغط الحاوية بأكملها باستخدام Flate. لم تعد الكائنات الفردية تمتلك إزاحة بايت في الملف. بل لديها موضع داخل كتلة مضغوطة. المدقق الذي يمسح البايتات الخام لـ 1 0 obj لا يجدها أبداً، لأن هذا النص لا يوجد إلا بعد التضخم. بالنسبة للمحلل الكلاسيكي، فإن نصف المستند قد اختفى ببساطة.

مفاتيح المقطورة تكون نصاً واضحاً، حتى في الملف المضغوط

الجزء المطمئن هو أن قراءة مقطورة مجرى الإسناد الترافقي لا تتطلب تضخيم أي شيء. يُكتب كائن المجرى كقاموس تتبعه الكلمة الرئيسية stream ثم بايتات البيانات المضغوطة. القاموس هو نص واضح. لذلك عندما تشير startxref إلى مجرى إسناد ترافقي، فإن البايتات التي تلي رقم الكائن مباشرة تبدو كقاموس عادي، وتجلس /Root و/Size و/ID هناك بوضوح، قبل أن تبدأ الكلمة الرئيسية stream وبيانات Flate.

هذا يعني أن المدقق يمكنه معرفة الحقائق الثلاث التي يحتاجها أكثر من غيرها، أين يقع الكتالوج، وكم عدد الكائنات التي يدعيها الملف، ومعرف الملف، عن طريق تحليل قاموس المجرى فقط. ولا يتعين عليه إلغاء ضغط بيانات الإسناد الترافقي، ولا يتعين عليه تفسير المدخلات الثنائية بداخلها. العمل الذي يهزم المحلل البسيط ليس قراءة المقطورة، بل هو العثور على الكائنات. هاتان مشكلتان منفصلتان، وحل الأولى رخيص.

مجاري الكائنات: رأس، ثم كتلة Flate

مجرى الكائنات هو حاوية. يحمل قاموسه القيمة /Type /ObjStm، وإدخال /N الذي يعطي عدد الكائنات المحزومة بداخلها، وإدخال /First الذي يعطي إزاحة البايت، داخل البيانات المتضخمة، حيث تبدأ هيئة الكائن الأول. تبدأ الحمولة المضغوطة، بمجرد تضخيمها، برأس صغير من أزواج الأعداد الصحيحة /N. كل زوج هو رقم الكائن وإزاحة هيئة ذلك الكائن بالنسبة إلى /First. بعد الرأس تأتي هيئات الكائنات نفسها، متسلسلة.

توسيع أحدها هو عمل ميكانيكي بمجرد تضخيم البايتات. تقرأ القاموس للحصول على /N و/First، وتضخم المجرى باستخدام وحدة فك ترميز Flate، وتمشي في أزواج /N الرائدة لمعرفة أي رقم كائن يعيش عند أي إزاحة، ثم ترفع كل هيئة كما لو كانت كائناً غير مباشر عادياً. التبعية الحقيقية الوحيدة هي وحدة فك ترميز Flate، ولديك واحدة بالفعل: تشحن Delphi وحدة System.ZLib، وتشحن Free Pascal وحدة zstream، وكلاهما يغلف zlib ويضخم مجرى Flate الخام دون أي كود تابع لجهة خارجية. الروتين الذي يلحق كل كائن مستخرج بجدول كائنات المدقق يجعل بقية المدقق، الجزء الذي يمشي في /Root ويفحص شجرة الصفحات، يتصرف تماماً كما يفعل في الملف الكلاسيكي.

ما لا يتعين عليك تنفيذه

من السهل تقدير العمل بأكثر من حجمه. قراءة مفاتيح المقطورة من ملف مضغوط لا تتطلب فك ترميز المدخلات الثنائية لمجرى الإسناد الترافقي. يستخدم مجرى الإسناد الترافقي في القسم 7.5.8 ثلاثة أنواع من المدخلات، ومدخل النوع 2، وهو المدخل الذي يقول هذا الكائن يعيش داخل مجرى الكائنات N عند الفهرس i، هو ما ستفك ترميزه لبناء خريطة إزاحة كاملة. تحتاج إلى تلك الخريطة لحل الكائنات العشوائية بالرقم. لكنك لا تحتاج إليها لقراءة /Root و/Size و/ID، والتي تقع في القاموس ذي النص الواضح، ولا تحتاج إليها لتوسيع مجاري الكائنات، لأن كل /ObjStm يعلن عن محتوياته الخاصة من خلال /N و/First.

كما لا يتعين عليك التعامل مع دوال التنبؤ PNG وTIFF التي قد يطبقها مجرى الإسناد الترافقي من خلال /DecodeParms لمجرد الحصول على مفاتيح المقطورة. تعمل التنبؤات على تصفية صفوف الإسناد الترافقي الثنائية لجعلها تضغط بشكل أفضل، ولا علاقة لها بالقاموس الذي يسبق المجرى. الترقية الدنيا التي تجعل المدقق الكلاسيكي واعياً بملفات PDF الحديثة هي صغيرة: عندما تهبط startxref على مجرى بدلاً من الكلمة الرئيسية xref، قم بتحليل قاموس المجرى لمفاتيح المقطورة، وقم بتوسيع أي كائنات /ObjStm تقابلها حتى تدخل محتوياتها في جدول الكائنات. فك ترميز مدخلات النوع 2 والتنبؤات هو مهمة منفصلة وأكبر يمكنك تأجيلها حتى تحتاج فعلياً إلى حل كائن عشوائي.

لماذا يجب أن يقوم فحص المطابقة بتوسيع المجاري أولاً

يتوقف هذا عن كون أكاديمياً في اللحظة التي تقوم فيها بتشغيل فحص ملف تعريف. يفحص مدقق PDF/A أو PDF/X كائنات معينة: كتالوج المستند لمصفوفة /OutputIntents، ومجرى /Metadata لحزمة XMP بالمعرف الصحيح، وكل واصف خط لملف خط مضمن، والمقطورة لـ /ID. في الملف المضغوط، تكون معظم هذه الكائنات داخل مجاري الكائنات. والمدقق الذي لم يوسع مجاري الكائنات لا يمكنه رؤية مفاتيح الكتالوج، ولا يمكنه العثور على البيانات الوصفية، ولا يمكنه سرد الخطوط. وسيبلغ عن مستند متوافق تماماً بأنه يفتقر إلى نية الإخراج الخاصة به، ويفتقر إلى XMP الخاص به، ويفتقر إلى نصف بنيته، لأن الدليل الذي يحتاجه لا يزال يجلس في كتلة Flate لم يقم بتضخيمها أبداً.

الترتيب مهم. يجب أن يحدث التوسيع قبل تشغيل الفحوصات، وليس إلى جانبها، لأن كل فحص يفترض أنه يمكنه الوصول إلى كائن بالرقم. إذا قمت بربط فحص ملف تعريف مباشرة بمسح بايت خام، فإنه يرث عمى المحلل الكلاسيكي وينتج انتهاكات كاذبة على الملفات الحديثة التي من المرجح أن تكون جيدة التشكيل، لأنها أتت من سلاسل أدوات حديثة بما يكفي لكتابة مجاري الإسناد الترافقي في المقام الأول.

السماح لـ PDFium بالقيام بالتحليل نيابة عنك

يقوم مكون PDFium بتحليل مجاري الإسناد الترافقي ومجاري الكائنات كجزء من تحميل المستند، وهي الطريقة العملية لتجنب التوسيع اليدوي وخطوات التضخيم. عندما تقوم بتحميل ملف باستخدام مكون TPdf، فإن الكائنات المحزومة داخل حاويات /ObjStm تكون قد تم حلها بالفعل، وترى نقاط دخول التحقق المستند الموسع بالكامل. تعيد دالة ValidatePdfA سجل TPdfAValidationResult الذي يكون حقل Conformance الخاص به قيمة TPdfAConformance مثل pac1b أو pacNone، ويكون حقل Issues الخاص به مجموعة من المشاكل المحددة التي تم العثور عليها، وتكون طريقة IsCompliant الخاصة به صحيحة فقط عند اكتشاف مستوى مطابقة وتكون مجموعة المشاكل فارغة. ونظراً لأن الكائنات قد تم توسيعها أثناء التحميل، فإنه يتم العثور على مصفوفة /OutputIntents أو خط مضمن عاش داخل مجرى كائنات، ولا يتم الإبلاغ عن فقدهما.

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // parses xref/object streams on load
    Result := Pdf.ValidatePdfA;    // sees the expanded object table
  finally
    Pdf.Free;
  end;
end;

ينطبق الشيء نفسه على ValidatePdfX، والتي تعيد TPdfXValidationResult بنفس الشكل. الغرض من التوجيه عبر PDFium هو أن إلغاء الضغط الهيكلي الموصوف أعلاه يحدث مرة واحدة، بشكل صحيح، داخل اللودر، لذا لا يرى كود التحقق الخاص بك الفرق بين ملف كلاسيكي وملف مضغوط بالكامل. كلاهما يصل إلى المدقق كمجموعة كائنات تم حلها.

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', Ord(R.Conformance))
    else
      Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
  finally
    Pdf.Free;
  end;
end;

إذا كانت البايتات موجودة بالفعل في الذاكرة وليس على القرص، فإن نفس تسلسل التحميل ثم التحقق يعمل من خلال التحميل الزائد لـ LoadDocument(const Data: TBytes)، الذي يأخذ محتوى الملف الخام ويحلل مجاري الإسناد الترافقي والكائنات بنفس طريقة مسار الملف. الدرس المستفاد للمدقق المكتوب يدوياً هو القاعدة الهيكلية، وليس واجهة برمجة التطبيقات: اقرأ مفاتيح المقطورة من قاموس المجرى بنص واضح، ووسع كل كائن /ObjStm باستخدام وحدة فك ترميز Flate قبل المشي في المستند، وعامل فك ترميز مدخلات الإسناد الترافقي الثنائية كمهمة أكبر واختيارية.

بمجرد توسيع البنية، يمكن للمدقق تشغيل بقية سير العمل عليها. للحصول على أداة سطر أوامر تقوم بالإبلاغ عن المطابقة عبر مجلد من المدخلات، راجع دليلنا لبناء تقرير فحص المطابقة الدفعي CLI. وعندما يكون التحقق بوابة قبل تقسيم مستند كبير، فإن التقنيات الموضحة في دليلنا لتقسيم مستندات PDF إلى ملفات متعددة تقترن بشكل طبيعي بنمط التحميل والفحص الموضح هنا. يعتمد كلاهما على سطح التحميل والتحقق الخاص بـ مكون PDFium لـ Delphi وC++Builder.