مقال تقني

تكافؤ بيانات Info وXMP الوصفية في PDF/A في دلفي

يفحص مكوّن PDFium تكافؤ بيانات Info وXMP الوصفية في PDF/A عبر TPdf.InspectPdfAMetadata ويصلحه عبر TPdf.NormalizePdfAMetadata. تشترط ISO 19005-1 (مصححة بـ Cor.1) أن يحمل كل مدخل من مداخيل Info الثمانية المرتبطة، من Title إلى ModDate، القيمة نفسها التي تحملها خاصية XMP المقابلة، لا أن يوجد فحسب؛ والفحص يقرأ شكل RDF الصحيح، ويطابق نطاقات الأسماء بالـ URI، ويقارن التواريخ بوصفها لحظات

تقرير العيب الذي يفتح هذه المحادثة عادة يبدو بريئًا. نظام إدارة مستندات يختم /ModDate جديدة في قاموس Info عند كل حفظ تزايدي، ويترك حزمة XMP وشأنها، وبعد ستة أشهر يعلّم تدقيق أرشفة آلاف الملفات بوصفها غير مطابقة. كلا التاريخين موجود. إنما توقفا عن الاتفاق عند أول تعديل، وفحص الحضور لم ينتبه قط. وتحريرات العنوان عبر API يكتب في Info وحده، وسلسلة مؤلف مثل Finance; Controlling شققتها أداة ما إلى مدخلين dc:creator، تفشل بالطريقة نفسها

لماذا يرفض PDF/A بيانات وصفية موجودة في الموضعين؟

يرفضه PDF/A لأن ISO 19005-1 §6.7.3 قاعدة قيم لا قاعدة حضور: يربط الجدول 1 ثمانية مفاتيح Info بخاصيات XMP، ومتى حضر مفتاح Info وجب على خاصية XMP المرتبطة أن تحمل قيمة مكافئة. الماسح على مستوى البايتات الموصوف في التحقق المسبق PDF/A عبر مكوّن PDFium لا يؤكد إلا وجود xmp:CreateDate وxmp:ModifyDate‏ (pvaiMissingXmpDates). ومنذ v3.72.0 يشغل TPdf.ValidatePdfA أيضًا مقارنة القيم كاملة ويضيف pvaiInfoXmpValueMismatch إلى مجموعة المشاكل حين توجد حزمة XMP لكنها تخالف Info‏ (وحزمة لا يمكن تحليلها تحسب مخالفة). والحزمة الغائبة تظل تبلغ بوصفها pvaiMissingXmpMetadata، فلا تحسب المشكلتان العيب نفسه مرتين قط

أي شكل RDF تحتاجه كل خاصية XMP مرتبطة؟

لكل ربط من الربوط الثمانية نوع XMP ثابت، والقيمة الصحيحة في الحاوية الخطأ ما تزال تفشل. يبحث ComparePdfAInfoAndXmp في FPdfPdfa.pas عن الخاصيات بربط URI نطاق الاسم، فحزمة تربط http://purl.org/dc/elements/1.1/ ببادئة غير مألوفة تقرأ تمامًا كحزمة تستخدم dc. والأشكال المطلوبة هي:

  • ‏Title إلى dc:title وSubject إلى dc:description: بديل لغة rdf:Alt، يقارن مقابل مدخل x-default خاصته فقط‏ (وتطابق وسيم اللغة بلا حساسية لحالة الأحرف)؛ وAlt بلا x-default يحسب غائبًا
  • ‏Author إلى dc:creator:‏ rdf:Seq بمدخل نص واحد بالضبط يحمل سلسلة Info كاملة، فقائمة مؤلفين تفصل بينهم فواصل منقوطة تبقى مدخلًا واحدًا
  • ‏Keywords إلى pdf:Keywords وProducer إلى pdf:Producer‏ (نطاق الاسم http://ns.adobe.com/pdf/1.3/): خاصيات نص بسيطة
  • ‏Creator إلى xmp:CreatorTool وCreationDate إلى xmp:CreateDate وModDate إلى xmp:ModifyDate‏ (نطاق الاسم http://ns.adobe.com/xap/1.0/): خاصيات نص بسيطة
الربوط الثمانية التي يفحصها ComparePdfAInfoAndXmp لتكافؤ بيانات PDF/A الوصفية في دلفي: يحتاج Title وSubject إلى rdf:Alt بمدخل x-default، وAuthor إلى rdf:Seq بمدخل واحد، وKeywords وProducer وCreator والتاريخان نص بسيط، وكلٌ يبحث بربط URI نطاق اسم XMP لا بالبادئة
متى حضر مفتاح Info، تشترط ISO 19005-1 أن تحمل خاصية XMP المرتبطة قيمة مكافئة في شكل RDF المطلوب، فالقيمة في الحاوية الخطأ ما تزال تفشل
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report 2026</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>Finance; Controlling</rdf:li></rdf:Seq></dc:creator>
<pdf:Producer>PDFium Component</pdf:Producer>

تقارن قيم النص بوصفها متتاليات نقاط ترميز Unicode تامة، بلا قص ولا طي حالة ولا تطبيع. مسافة زائدة في الذيل، أو é مركبة مسبقًا في طرف وe مفككة مع علامة تشكيل في الطرف الآخر، عدم تطابق حقيقي. ويأتي طرف Info دائمًا من فك ترميز PDFium الخاص لنصي PDFDocEncoding وUTF-16 عبر FPDF_GetMetaText، مما يحول دون أن تعيد المكتبة تنفيذ فك ترميز السلاسل فتخطئ فيه خطأ خفيًا؛ وطرف XMP نظيف بقدر البايتات التي أنتجته، ولهذا تهم هنا أيضًا فخاخ صفحات الأكواد التي تفسد بيانات XMP الوصفية تحت Free Pascal

متى يتساوى تاريخ PDF وتاريخ XMP؟

يتساوى تاريخ PDF وتاريخ XMP حين يصفان اللحظة نفسها بدقة الثانية، وبالمعرفة نفسها بالمنطقة الزمنية في الطرفين. يقبل كلا المحللين الدقة المخفضة المشروعة، فـ D:2026 و2026 كلاهما يعني 1 يناير 2026 الساعة 00:00:00. وحين تحمل القيمتان منطقة، تحولان إلى UTC قبل المقارنة: ‏D:20260827093659+08'00' يساوي 2026-08-27T01:36:59Z. وحين لا تحمل أي منهما منطقة، تقارن المكونات المحلية كما كتبت. وحين يملك طرف واحد منطقة فقط، كان الناتج pamsValueMismatch، لأن اختلاق إزاحة سيكون تخمينًا. وكسر الثانية غير الصفري مثل .250 في XMP يفرض عدم تطابق أيضًا، لأن تاريخ PDF لا سبيل له للتعبير عنه وإزالته بالتقريب الصامت سيخفي خلافًا حقيقيًا؛ أما .000 فيقبل. والقيم غير القابلة للتحليل تبلغ منفصلة بوصفها pamsInvalidInfoDate أو pamsInvalidXmpDate

كيف يقرر مكوّن PDFium أن تاريخ Info في PDF وتاريخ XMP متساويان في دلفي: منطقتان تحولان إلى UTC وتقارنان لحظةً، وقيمتان بلا منطقة تقارنان كما كتبتا، ومنطقة واحدة وحدها تعني pamsValueMismatch، وكسر ثانية غير صفري لا سبيل للتعبير عنه، والقيم غير القابلة للتحليل تبلغ منفصلة
التساوي يعني اللحظة نفسها بدقة الثانية وبالمعرفة نفسها بالمنطقة الزمنية في الطرفين، فاختلاق إزاحة أو إزالة كسر ثانية بالتقريب سيخفي خلافًا حقيقيًا

للحضور قاعدته الخاصة.‏ TPdfAMetadataValues.Present مجموعة تمتلئ بمرور قاموس /Info في trailer النشط، وهي تفصل «مفتاح غائب» عن «مفتاح حاضر بسلسلة فارغة». المفتاح الغائب يعطي pamsNotRequired ولا يطلب شيئًا من XMP؛ و/Title () حاضر، فيجب أن تحمل حزمة XMP عنوان x-default فارغًا أيضًا

كيف تفحص بيانات Info وXMP الوصفية قبل الحفظ؟

يعيد TPdf.InspectPdfAMetadata قيمة TPdfAMetadataReport بها TPdfAMetadataComparison واحدة لكل حقل، كل منها تحمل قيمة Info وقيمة XMP وTPdfAMetadataState، فيمكن تفسير فشل دون هندسة عكسية لعلم تحقق واحد. ويلخص MismatchFields مجموعة الفاشل، ويخبرك HasXmpPacket هل وُجدت حزمة، ويحمل XmpParseError رسالة المحلل حين تكون الحزمة موجودة لكن لا يمكن قراءتها

uses
  System.SysUtils, PDFium, FPdfPdfa;

const
  FieldNames: array[TPdfAMetadataField] of string = (
    'Title', 'Author', 'Subject', 'Keywords',
    'Creator', 'Producer', 'CreationDate', 'ModDate');
  StateNames: array[TPdfAMetadataState] of string = (
    'not required', 'equivalent', 'XMP missing', 'XMP type mismatch',
    'value mismatch', 'invalid Info date', 'invalid XMP date');

procedure ReportMetadata(Pdf: TPdf);
var
  Report: TPdfAMetadataReport;
  Item: TPdfAMetadataComparison;
begin
  Report := Pdf.InspectPdfAMetadata;
  if Report.XmpParseError <> '' then
    Writeln('XMP packet unreadable: ', Report.XmpParseError)
  else if not Report.HasXmpPacket then
    Writeln('No XMP packet at all');
  for Item in Report.Comparisons do
    if not Item.IsEquivalent then
      Writeln(Format('%-12s %-18s Info="%s" XMP="%s"',
        [FieldNames[Item.Field], StateNames[Item.State],
         Item.InfoValue, Item.XmpValue]));
end;

ماذا يغير NormalizePdfAMetadata، وما الذي يرفضه؟

يعامل TPdf.NormalizePdfAMetadata قاموس Info بوصفه مصدر الحقيقة ويعيد كتابة خاصيات XMP التي حقلها هبط في MismatchFields فقط؛ وكل شيء آخر في الحزمة ينجو. يكتب Title وSubject في مدخل x-default بينما تبقى بدائل اللغات الأخرى سليمة، ويصير Author‏ rdf:Seq بمدخل واحد، وتحفظ نطاقات الأسماء المجهولة والخاصيات غير ذات الصلة، وتترك خاصيات XMP للمفاتيح الغائبة في Info بلا مساس. ويكتب تاريخ Info ذو المنطقة بوصفه تاريخ XMP قانونيًا بالـ UTC بلاحقة Z؛ وما لا منطقة له يحفظ مكوناته المحلية. والتحميل الزائد للملفات يحفظ عبر ملف مؤقت واستبدال ذري، وتلحق تحديثة XMP نفسها بوصفها تحديثًا تزايديًا

ماذا يعيد NormalizePdfAMetadata كتابته عند إصلاح بيانات PDF/A الوصفية في دلفي عبر مكوّن PDFium: Info هو مصدر الحقيقة، ولا يعاد كتابة سوى مداخيل MismatchFields بوصفها نص Alt بقيمة x-default أو Seq بمدخل واحد أو تاريخ UTC قانوني، بينما تنجو نطاقات الأسماء المجهولة والخاصيات غير ذات الصلة وخاصيات المفاتيح الغائبة بلا مساس
يرفض الإصلاح حزمة XMP غائبة وتاريخ Info مشوهًا ومستندات موقعة، لأن بناء مجموعة بيانات PDF/A الوصفية كاملة عمل SaveAsPdfA لا إصلاح تكافؤ موجه

الرفوف مقصودة. بلا حزمة XMP يرفع الأسلوب EPdfError، لأن بناء مجموعة تعريف PDF/A والبيانات الوصفية كاملة عمل SaveAsPdfA، المغطى في إنشاء ملفات PDF/A للأرشفة عبر مكوّن PDFium. وتاريخ Info مشوه يرفع EPdfXmpError بدل كتابة قيمة خاطئة تبدو مقنعة، ولا يحفظ شيء. والمستندات الموقعة ترفض ما لم يمرر المستدعي AllowSignedDocument = True. والتكافؤ قاعدة واحدة في ISO 19005-1 أيضًا، فالملف المطبّع ليس مطابقًا تلقائيًا

uses
  System.SysUtils, PDFium, FPdfPdfa, FPdfXmp;

procedure NormalizeArchive(const Source, Target: string);
var
  Pdf: TPdf;
  Report: TPdfAMetadataReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := Source;
    Pdf.Active := True;
    Report := Pdf.InspectPdfAMetadata;
    if Report.IsEquivalent then
      Exit;                      // متسق سلفًا، اترك الملف وشأنه
    if not Report.HasXmpPacket then
      raise Exception.Create('No XMP packet: convert with SaveAsPdfA instead');
    try
      if not Pdf.NormalizePdfAMetadata(Target) then
        raise Exception.Create('Normalized save failed');
    except
      on E: EPdfXmpError do      // تاريخ Info مشوه أو حزمة غير قابلة للقراءة
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

تشغيل المقارنة على حزمة XMP خاصة بك

إن ComparePdfAInfoAndXmp وSynchronizePdfAInfoToXmp دالتان عاديتان في FPdfPdfa تعملان على TPdfXmpPacket دون تحميل أي مستند، وهو ما يناسب اختبارات الوحدة وخطوط المعالجة التي تركب XMP من قالب. والفخ الوحيد هو Present: سجل مهيأ بـ Default(TPdfAMetadataValues) مجموعته فارغة، فتبلغ كل الحقول pamsNotRequired وينجح المقارنة نجاحًا هوائيًا مهما ملأت من قيم

uses
  System.SysUtils, System.IOUtils, FPdfPdfa, FPdfXmp;

procedure AlignTemplate(const TemplateFile: string);
var
  Info: TPdfAMetadataValues;
  Packet: TPdfXmpPacket;
  Changed: TPdfAMetadataFields;
begin
  Info := Default(TPdfAMetadataValues);
  Info.Title := 'Quarterly Report 2026';
  Info.Author := 'Finance; Controlling';
  Info.ModDate := 'D:20260827093659+08''00''';
  // يقرر Present أي الحقول إلزامية؛ والقيم وحدها تتجاهل
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

  Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
  try
    Changed := SynchronizePdfAInfoToXmp(Info, Packet);
    // أصبح xmp:ModifyDate الآن 2026-08-27T01:36:59Z وdc:creator‏ rdf:Seq بمدخل واحد
    if Changed <> [] then
      TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
  finally
    Packet.Free;
  end;
end;

إن كان خط معالجتك يؤرشف مستندات ما تزال أنظمة أخرى تحررها، فاجمع بين مسح ليلي بـ InspectPdfAMetadata وNormalizePdfAMetadata للملفات المنحرفة، وأبق ValidatePdfA بوابة قبل أن يغادر أي شيء إلى التخزين طويل الأمد. والتقرير المكتوب الأنواع ومسار الإصلاح وبقية أدوات PDF/A تأتي في مكوّن PDFium لدلفي وC++Builder