رفضت بوابة إدخال أرشفة دفعة من ملفات "PDF/A-2b" كانت تفتح بشكل طبيعي في كل عارض على المكتب. وأقسم المورّد أنها متوافقة. لكنها لم تكن كذلك: كان كل ملف يحمل فعل JavaScript مخفيًا داخل الكتالوج، من النوع الذي لا تلتقطه العين السريعة أبدًا بينما يضعه مدقق PDF/A كامل مثل veraPDF في خانة الخطأ فورًا. المشكلة أن أحدًا لم يرغب في إلصاق سلسلة أدوات Java فوق خدمة دفعات Delphi لمجرد الإجابة عن سؤال نعم أو لا لكل ملف. هذه هي الفجوة ValidatePdfAComplianceالتي يسدّها PDFium Component، ومن المهم فهم كيف يصل إلى الحكم من دون أن يحلل تيار محتوى بالكامل
لماذا لا يستطيع PDFium نفسه الإجابة عن هذا
أول ما يجب أن تكون صريحًا بشأنه: المكوّن pdfium.dll لا يملك أي قدرة على PDF/A على الإطلاق. لا توجد ConvertToPDFA، ولا كاتب OutputIntent، ولا واجهة برمجة XMP في السطح العام. كل جزء من PDF/A في هذه المكتبة، سواء جانب الكتابة أو جانب التحقق، يعيش في Pascal خالص داخل FPdfPdfa.pas ويعمل بتحليل على مستوى الرمز ثم تحديث تزايدي. لذلك عندما تستدعي المدقق فأنت لا تسأل عارض Chromium عن أي شيء. أنت تشغّل ماسح رموز Pascal فوق البايتات البنيوية للملف
واجهة البرمجة العامة صغيرة عمدًا. دالة واحدة تقرأ تيارًا من الموضع 0 وتعيد سجلًا:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant يشفّر القاعدة التي تهم في أي بوابة: يكون الملف ناجحًا فقط عندما يكون مستوى توافق حقيقي قد اكتُشف وتكون مجموعة المشكلات فارغة. أما التحليل الذي ينجح لكنه لا يجد أي علامة pdfaid فيُحسم على أنه pacNone، وهو ليس نجاحًا صراحة. وهذه هي النقطة نفسها التي يعلنها batch preflight report CLI من الخارج: قائمة نتائج فارغة على ملف غير معرَّف ليست شهادة نظافة
إزالة أجسام التدفقات قبل أي فحص رموز
إليك أهم تفصيل تنفيذي، وهو أسهل شيء أن تخطئ فيه إذا كتبت ماسحك الخاص. يلتقط الكاشف المخالفات عبر البحث عن رموز اسمية مفصولة، أشياء مثل /JavaScript, /LZWDecode، /BM. إذا مسحت البايتات الخام للملف، فإن أجسام التدفقات الثنائية المدمجة، والصور المضغوطة، وملفات ICC، وبرامج الخطوط، ستحتوي عشوائيًا على سلاسل بايت تبدو كأنها تلك الرموز. وستبلغ عن /AA أو /3D على أنها "موجودة" لأن ثلاثة بايتات داخل JPEG صادفت أن تهجّيها كذلك. وهذه آلة إنتاج للنتائج الإيجابية الكاذبة
الإصلاح هو PdfStructureBytes: فهي تمشي عبر الملف وتفرغ البايتات الواقعة بين كل كلمة stream وendstream إلى مسافات، مع إبقاء بنية القاموس intact. وبعد ذلك فقط يبدأ الفحص. كل اختبار لرمز اسم داخل المدقق يعمل على هذه النسخة المجردة. إذا أخذت فكرة واحدة من هذه المقالة، فخذ هذه. الانضباط نفسه ينعكس في مدقق PDF/UA، الذي يحتفظ بنسخته الخاصة من الروتين لأن المعيارين يتطوران بشكل مستقل
المشكلات 29 وما يعنيه كل منها
TPdfAValidationIssue هي عقدة موثقة. الأرقام ثابتة لأن اختبارات DUnitX والعروض وطبقة التقارير كلها تعتمد عليها، لذلك لا تُضاف النتائج الجديدة إلا في نهاية القائمة. وحتى v1.63.0 يوجد 29 عضوًا. وهي تتوزع على عدة عائلات:
- البيانات الوصفية والهوية:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - اللون والإخراج:
pvaiMissingOutputIntent,pvaiMissingIccProfile, andpvaiMixedDeviceColorSpacesعندما يظهر كل من DeviceRGB وDeviceCMYK معًا (6.2.3.3) - حظر صريح لكل جزء:
pvaiEncryptionPresent(معجم/Encryptمحظور تمامًا)،pvaiJavaScriptPresent،pvaiForbiddenAction،pvaiAdditionalActions،pvaiLzwUsed،pvaiXfaPresent،pvaiNeedAppearancesTrue،pvaiForbiddenAnnotation - الخطوط:
pvaiFontNotEmbeddedوالمستوى الأشدpvaiUnembeddedFont، بالإضافة إلىpvaiUnicodeMappingMissingلادعاء مستوى U من دون/ToUnicode - الوسوم:
pvaiLevelAStructureMissingعندما يكون ادعاء conformance=A من دون بنية معنونة
ستة الأعضاء الأحدث، التي أضيفت في الرتب 24 إلى 29، تغطي الحالات الدقيقة التي يتعثر عندها المراجعون فعلًا: pvaiTrappedTrue (تعيين /Trapped /True في حقل Info، وهو "صديق زائف" لأن القيمة يجب أن تكون False أو Unknown), pvaiForbiddenActionSubtype (استخدام Sound أو Movie كفعل، لا كتعليق فحسب), pvaiTransparentColorSpace (نمط مزج غير Normal أو /CA//ca لا يساوي 1.0), pvaiAnnotationDictViolation، pvaiUnembeddedFont، pvaiMixedDeviceColorSpaces
التهدئة الواعية بالجزء: A-1 صارم، وA-2 وA-3 أكثر مرونة
PDF/A ليس كتاب قواعد واحدًا. ثلاثة أشياء يحظرها PDF/A-1 تُسمح صراحةً ابتداءً من PDF/A-2: الشفافية (مجموعة /Transparency أو /SMask، 6.4)، والمحتوى الاختياري (/OCProperties، 6.1.13)، والملفات المضمنة (/EmbeddedFiles أو /EF، 6.1.11). والمدقق الساذج الذي يعلّم الثلاثة كلها لكل ملف سيرفض دفعة كاملة من مستندات PDF/A-2 الصالحة تمامًا
لذلك يقرأ المدقق رقم الجزء من وسم pdfaid عبر PdfAPartOf ويُخضع تلك الفحوص خلف PartNo = 1 كما أن فحوص blend-mode وشفافية التعليقات لمشكلات الشفافية الجديدة خاصة بالجزء 1 فقط:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
قاعدة افتراضية محافظة تستحق الذكر: عندما لا توجد علامة pdfaid على الإطلاق، يُعامل الجزء على أنه 1، أي الأكثر صرامة. والمنطق هنا أن الملف غير المعرّف يجب أن يُحاسب على أشد القواعد بدل أن يُمرر. يظل JavaScript والأفعال المحظورة وLZW وXFA وNeedAppearances والتعليقات المحظورة والخطوط غير المضمنة محظورًا في كل جزء، لذلك لا توضع تلك الفحوص خلف البوابة أبدًا
توسيع تدفقات الكائنات حتى لا يختبئ شيء
أدخل PDF 1.5 تيار cross-reference وتيار الكائنات (/Type /ObjStm)، وهما يخلقان نقطة عمياء لماسح بايتات ساذج. يمكن لـ catalog أو OutputIntent أو قاموس فعل، أو أي شيء ليس تيارًا في حد ذاته، أن يُضغط بـ Flate داخل ObjStm. افحص البنية الخام ولن ترى منه شيئًا، ثم ستبلغ عن ملف نظيف وهو ليس كذلك
PdfExpandObjectStreamsيغلق Data := PdfExpandObjectStreams(Data)هذه الفجوة. قبل أن يعمل أي فحص، يفعل المدقق /Nالروتين يعثر على كل ObjStm، ويقرأ /First وPdfInflate في رأسه ليأخذ أرقام الكائنات والإزاحات المضمنة، ويفك الجسم بـ System.ZLib (zlib في RTL، وzstream في Delphi وN 0 obj ... endobj في FPC)، ثم يلحق كل كائن مضمن على هيئة عاديًا إلى نهاية نسخة من البايتات. بعدها تعثر اختبارات الرموز الحالية على تلك الكائنات من دون أي تغيير في منطقها
يجعل شرطان هذا العمل نظيفًا لا هشًا. كائنات التدفق مثل Metadata وملف ICC وبرامج الخطوط لا يمكن أن تعيش داخل object stream، بل القواميس غير المتدفقة فقط هي التي يمكنها ذلك، لذا فإن التوسيع يتعامل دائمًا مع القواميس فقط، وتحمل الكائنات الملحقة لا شيء stream keyword يربك مرحلة إفراغ أجسام التدفقات. ولأن المحتوى الملحق يقع بعد %%EOF، فإن البحث العكسي من startxref ما يزال يجد الترويسة الأصلية. أما ترويسة تيار cross-reference نفسها فقد جرى التعامل معها سابقًا، في v1.49.3، بقراءة Root وSize وID مباشرة من قاموس xref-stream النصي، وهو موضوع تستعرضه القطعة المرافقة حول التحقق من تدفقات الكائنات وcross-reference المضغوطة؛ ولم تكن مهمة object-stream إلا إضافة خطوة فك الضغط، من دون حاجة إلى فك ترميزات xref من النوع 2 أو حل predictive PNG
الحدود الصادقة لمدقق على مستوى البايت
هذا أداة فحص مسبق، لا مدققًا معتمدًا، والحدود هنا حقيقية. تضمين الخطوط Heuristic قائم على العدّ، وضبطه جيدًا استلزم تصحيحًا يستحق أن يُعرف. كان الفحص الأصلي يستخدم PdfCountName('/FontDescriptor')، لكن كل خط يساهم برمزَي /FontDescriptor، واحدًا كمرجع من قاموس الخط وواحدًا /Type في كائن الوصف نفسه، لذلك كانت النتيجة 2N أمام N من البرامج المضمنة وكان الاختبار دائمًا true. الإصلاح هو PdfCountDescriptorRefs، الذي يعدّ فقط صيغة المرجع /FontDescriptor N G R، واحدًا لكل خط، ولا يرفع pvaiUnembeddedFont إلا عندما تكون البرامج المضمنة أقل فعلًا:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
حتى بعد التصحيح يظل خشنًا: فمستند مختلط قد يحمل فيه كل descriptor مصادفةً بعض FontFile، ومع ذلك يسمح بمرور خط غير متوافق فردي. وتوسيع تدفقات الكائنات له أثر جانبي معروف أيضًا، إذ يكشف الموارد الافتراضية القياسية 14 التي يحملها AcroForm /DR، مثل /Helv، ويبلّغ عنها الـ heuristic بأمان على أنها غير مضمنة، رغم أن veraPDF يمررها لأنها لا تُستخدم فعلًا في العرض. أما فحوص معاملات تيار المحتوى (6.2.10) فهي خارج النطاق تمامًا، لأنها تحتاج محلل محتوى كاملًا بدلًا من فحص بايتات. عامل المدقق كبوابة أولى سريعة بلا اعتماد خارجي تلتقط المخالفات التي لا تستطيع حقن العلامات إصلاحها، واحجز مدققًا كاملًا للتصديق النهائي
هذه هي جهة التحقق من القصة. أما جهة الكتابة المكملة، حيث SaveAsPdfA يحقن XMP وOutputIntent وملف sRGB ICC ويخفض بصراحة طلبًا من المستوى A لا يملك بنية معنونة، فتبني على الآلية نفسها على مستوى البايت. ويأتي القسمان معًا ضمن PDFium Component for Delphi، وهي حزمة VCL واحدة فوق تنفيذ PDF/A خالص بـ Pascal من دون runtime خارجي لتثبيته