مقال تقني

التوافق مع PDF/A للأرشفة في Delphi مع PDFium VCL

قد تُصدر أداة تحويل تضع علامة PDF/A-1b على كل ملف، وتستقبلها منظومة السجلات لدى العميل لعام كامل، ثم يأتي تدقيق شامل عبر veraPDF فتعود ثلث الملفات غير متوافقة. لم يتعطل شيء، ولم يُرمَ أي استثناء، والملفات تفتح بشكل طبيعي في كل عارض على مكتبك. لكنها ببساطة لم تكن المعيار الذي وسمتها به. هذه هي صورة الفشل المعتادة في PDF الأرشيفي، ولهذا فقولك "أضبطت الراية" لا يساوي أبدًا قولك "تحققت من صحتها"

أول ما ينبغي فهمه في PDFium وPDF/A هو أن المحرك ليس هو المسؤول هنا. PDFium يعرض PDF ويحلله ويكتبه، لكن واجهته العامة لا تحتوي على ConvertToPDFA، ولا على كاتب OutputIntent، ولا على واجهة برمجة XMP. كل جزء من التوافق الأرشيفي، من حزمة XMP وOutputIntent وملف ICC الخاص به إلى علامات الفهرس والتحقق، يعيش داخل PDFiumPas نفسه، في وحدة Pascal خالصة تقارب 2,000 سطر (FPdfPdfa.pas)، تحلل البايتات المحفوظة وتعيد كتابتها عبر تحديث تزايدي. معرفة موضع العمل تخبرك أين تختبئ الأخطاء، وهي لا تختبئ في PDFium

ما الذي يطلبه PDF/A فعلًا، وأين يضغط

PDF/A ليس صيغة واحدة. يعرّف ISO 19005 ثلاثة أجزاء (PDF/A-1 و-2 و-3)، وداخل كل جزء مستويات توافق تعد بأشياء مختلفة. المستوى B (الأساسي) يضمن فقط أن المظهر المرئي قابل لإعادة الإنتاج. والمستوى A (القابل للوصول) يضيف فوق B شجرة بنية معنونة وتخطيط Unicode. أما المستوى U، الموجود فقط في الجزأين 2 و3، فيقف بينهما: نص Unicode موثوق من دون شجرة البنية الكاملة. لا يملك ISO 19005-1 مستوى U، وهي قيد يشفّره المستودع مباشرة

بعض قواعد الصيغة هي التي تضرب في الممارسة. التشفير محظور تمامًا (ISO 19005-1 §6.1.3 وما بعده): لا يجوز لملف PDF/A أن يحمل /Encrypt معجمًا. يجب على المستند أن يعلن شرط عرض الإخراج عبر OutputIntent يكون فيه ملف الوجهة ICC profile صالحًا (§6.2.3.2). أما ادعاء التوافق نفسه فيجب أن يظهر كبيانات وصفية XMP ضمن مخطط تعريف PDF/A. والمستوى A يتطلب أيضًا §6.8 البنية المنطقية، وهي شجرة الوسوم التي تجعل المستند قابلًا للقراءة آليًا. إذا فقدت أيًا من ذلك، فسيرفض المدقق ملف التوافق حتى لو كان العرض يبدو سليمًا تمامًا

الدعوة الواحدة التي تنتج أرشيفًا

يكشف PDFiumPas كامل السلسلة خلف TPdf.SaveAsPdfA. أما التخصيص البسيط فيأخذ مستوى التوافق الهدف افتراضيًا إلى PDF/A-1b، وهو الافتراض الصحيح للحالة الشائعة: "اجعل هذا قابلاً للعرض إلى الأبد"

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

تحت الغطاء، هذه حركة على مرحلتين. SaveAsPdfA أولًا يطلب من PDFium تسلسل المستند مع FPDF_SaveAsCopy، ثم يسلم ذلك التدفق البايتي إلى InjectPdfAMarkers، الذي يضيف بيانات XMP الوصفية، وOutputIntent من sRGB مع ملف ICC المضمن، وكتالوجًا أعيدت كتابته كتحديث تزايدي. تُقرأ المصادر من الموضع صفر وتُكتب الوجهة من الموضع صفر؛ وتبقى شجرة الكائنات الأصلية كما هي، بينما تركب العلامات بعد %%EOF.SaveAsPdfAToStream يأخذ TStream ونفس الخيارات

اختيار مستوى التوافق باستخدام سجل الخيارات

للاستهداف نحو جزء ومستوى محددين، مرّر TPdfASaveOptions سجلًا. الحقل Conformance فيه يأخذ قيمة TPdfAConformance. تغطي التعدادات كل تركيبة صالحة ولا شيء غيرها: pac1b، pac1a للجزء 1؛ pac2b، pac2u، pac2a للجزء 2؛ pac3b، pac3u، pac3a للجزء 3، بالإضافة إلى pacUnknown وpacNone لجهة التحقق. لا يوجد pac1u، لأن هذا المستوى غير موجود في المعيار

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

يمكن لمعظم السجل أن يبقى فارغًا. اترك Title، Author، Subject، Keywords، Creator، وProducer فارغًا وSaveAsPdfA يملؤها تلقائيًا من حقل Info في المستند عبر FPDF_GetMetaText. اترك CreationDate وModDate فارغًا فيستخدم التوقيت الحالي UTC لكل من تاريخي XMP. اترك DocumentId وInstanceId فارغًا فتملؤهما المكتبة من FPDF_GetFileIdentifier، مع الرجوع إلى معرّف حتمي مشتق من بايتات المصدر. الحقل الوحيد الذي قد ترغب في تجاوزه عمدًا هو IccProfileData: تعني القيمة الفارغة ملف sRGB IEC61966-2.1 المضمّن، أما سير عمل CMYK أو grayscale فيجب أن يزوّد ملفه الخاص

لماذا يتراجع المستوى A، ولماذا هذا هو الخيار الصادق

هناك تفصيلة دقيقة توقع من يتوقع أن تكون الراية ضمانًا. يمكنك أن تطلب pac1a على مستند لا يحتوي على شجرة وسوم، لكن PDF/A-1a يتطلب البنية المنطقية §6.8، والمكتبة لا تستطيع أن تصنع شجرة بنية من PDF غير معنّون. بدلًا من إخراج ملف يدّعي المستوى A ثم يفشل فيه، SaveAsPdfA يتحقق من بنية معنونة حقيقية (/StructTreeRoot بالإضافة إلى /MarkInfo مع /Marked true)، وإذا لم تكن موجودة، يخفض الادعاء: pac1a يصبح pac1b، pac2a يصبح pac2b، وهكذا عبر الأجزاء الثلاثة كلها. أما الدوال الداخلية فهي PdfAIsLevelA وPdfADowngradeToLevelB

المنطق يستحق أن يُقال بوضوح: الملف الذي يصرح صراحة بالمستوى الذي يحققه أكثر فائدة من ملف يكذب بشأن مستوى لا يحققه. أما Level U فيُعالج بطريقة مختلفة. فالكشف عن تغطية Unicode حقيقية سيعني اختبارًا ساذجًا من نوع "هل يحتوي على /ToUnicode"، وهو ما سيبالغ في خفض مستوى مستندات صحيحة (WinAnsi وتشفيرات مشابهة مستثناة)، لذا يعلن جانب الحفظ ادعاء U كما طلبه المستدعي ويترك الفجوة لتُرصد في جانب التحقق. إذا كنت تحتاج أرشيفًا مضمونًا من المستوى A، فضع الوسوم على المستند قبل التحويل؛ فالمحول لن يخترع بنية غير موجودة

مطبات ICC التي لا يلتقطها إلا مدقق حقيقي

هذا هو الفشل الذي علّمني أصعب درس، لأن مدقق المكتبة نفسه قبله بينما رفضه veraPDF، وهو المدقق المرجعي لـ ISO 19005. يتطلب PDF/A أن يكون ملف الوجهة الخاص بـ OutputIntent تيار ICCBased صالحًا، وتلزم §6.2.3.2 المدقق بأن يفحص ذلك التيار بوصفه مساحة لونية. يجب أن يعلن تيار ICCBased /N، عدد مكونات اللون. وقد كتب إصدار مبكر من الحاقن قاموس تيار ICC مع /Length فقط ومن دون /N، ورفض veraPDF النتيجة مع الرسالة "The N entry (value null)... is missing"

ما جعل الأمر خبيثًا أن الرفض لم يظهر إلا مع PDF/A-1b و-1a. نماذج التوافق في الجزأين 2 و3 لم تشغّل ذلك الفحص بعينه على ملف الوجهة، لذلك كانت البنية المحقونة نفسها تُقبل تحت pac2b، pac3b، وpac2u لكنها تفشل تحت pac1b بسبب قيمة pdfaid:part وحدها. ولم يكن بإمكان اختبار وحدات أن يراه، لأن مدقق المكتبة نفسه ValidatePdfACompliance يتحقق فقط من أن المفتاح /DestOutputProfile موجود، لا مما يوجد داخل قاموس التيار. ظلت الاختبارات الداخلية خضراء؛ وفشل التحقق الأرشيفي الحقيقي

الإصلاح هو IccComponentCount, التي تقرأ توقيع مساحة اللون للبيانات عند الإزاحة 16 من ترويسة ICC وتحوّله إلى عدد مكونات: GRAY هو 1، RGB , Lab ، وXYZ هي 3، CMYK هو 4، مع افتراض 3 لملف غير معروف. ويُكتب هذا العدد في قاموس التيار على أنه /N. وهو محسوب، لا ثابتًا عند 3، حتى يبقى صحيحًا لمن يزود ملف CMYK أو grayscale عبر IccProfileData ما يزال يحصل على القيمة الصحيحة. والدرس الأوسع منهجي: المدقق المدمج والمدقق المرجعي لكل منهما نقاط عمياء، ويجب اختبار مخرجات PDF/A من طرف إلى طرف مقابل تنفيذ مرجعي مثل veraPDF بدلًا من الوثوق بالاختبارات الذاتية. وتُغطى نفس صرامة التحديث التزايدي الكامنة خلف الأرشيفات النظيفة في التحقق من تدفقات الكائنات وxref المضغوطة، وهو أمر مهم لأن ملفات PDF الحديثة التي يتعامل معها الحاقن تُبنى كثيرًا على تدفقات cross-reference

التشفير وتدفقات xref وحواف أخرى

لأن ISO 19005 يحظر التشفير، فإن مسار الحفظ يزيله قبل الكتابة. SaveAsPdfA يطبّق FPDF_REMOVE_SECURITY عند التسلسل، لذلك يُفك تشفير المصدر المشفّر (الذي حُمّل بكلمة مروره) أثناء إدخاله إلى الأرشيف. وعلى مستند غير مشفّر تكون النتيجة بلا أثر ولا تغيّر شيئًا. والنتيجة المقابلة هي القيد نفسه الذي يفرضه HotPDF من الجهة الأخرى: لا يمكن لملف واحد أن يكون مشفّرًا وPDF/A في الوقت نفسه. وعندما يحتاج سير العمل إلى الاثنين معًا، فالجواب ملفان، نسخة مشفّرة للتوزيع ونسخة نظيفة منفصلة للأرشيف

ومطبة أخرى لا تُرى إلا حين تعض: مستندات PDF 1.5+ التي تستخدم تيار cross-reference خالصًا ولا تحمل كلمة trailerالترويسة. يقرأ الحاقن الترويسة ليعثر على /Info المصدر ويُلحق تحديثه التزايدي، وعليه أن يقبل صيغة xref-stream، وإلا فسيُنسخ مثل هذا المستند مع إسقاط العلامات بصمت. وتنص ISO 32000-1 §7.5.6 صراحة على أن تحديثًا تزايديًا بترويسة كلاسيكية يمكن أن يتبع مستند xref-stream، مع /Prev يشير إلى إزاحة xref-stream، وهذا هو بالضبط الهيكل الذي يخرجه الحاقن. أما FPDF_SaveAsCopy في PDFium نفسه فيكتب دائمًا ترويسة كلاسيكية، لذلك لا يواجه الحاقن في المسار المعتاد مصدر xref-stream خالصًا، لكن مسار القراءة يتعامل معه عندما تأتي المستندات من مكان آخر

التحقق قبل أن تثق في الادعاء

تُشحن المكتبة بمدقق على مستوى البايت، TPdf.ValidatePdfA، الذي يعيد TPdfAValidationResult. وحقل Conformance فيه يبلّغ عن المستوى المكتشف، وIssues هي مجموعة من TPdfAValidationIssue قيم IsCompliant؛ والطريقة المساعدة

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

تكون true فقط عندما يُكتشف مستوى حقيقي وتكون مجموعة المشكلات فارغة. شغّلها كبوابة أولى سريعة في دفعة معالجة./Encryptكن صريحًا بشأن ما يضيفه هذا. المدقق على مستوى البايت يلتقط المشكلات البنيوية (غياب OutputIntent، أو فعل محظور، أو وجود ، أو الشفافية حيث يمنعها الجزء 1) بدرجة ثقة عالية، كما أن كشف تضمين الخطوط يستخدم أسلوبًا إرشاديًا قائمًا على العدّ يعلن عن إشارة عالية الثقة فقط بدلًا من مطاردة التغطية لكل glyph. وما لا يفعله هو تحليل معاملات operator في تيار المحتوى، وهو ما يتطلب محلل محتوى كاملًا وهو خارج النطاق عمدًا. وللوصول إلى بوابة إصدار، اقترن المدقق المدمج مع veraPDF: المدقق فوري ويعمل في كل مكان من دون DLL، وveraPDF مرجعي. موضوع ربط هذا الاقتران في تشغيل دفعي هو batch preflight report CLI

، وهو المكان المناسب لهذا التحقق في سير عمل أرشيفي حقيقي.SaveAsPdfAتُنشر، وInjectPdfAMarkersواجهات برمجة التطبيقات المعروضة هنا مع ValidatePdfAPDFium Component لـ Delphi وC++Builder وLazarus/FPC.وتربط صفحة المنتج المرجع الكامل لواجهات البرمجة، بما في ذلك تعداد التوافق الكامل وسجل الخيارات الكامن خلف هذه الأمثلة