مقال تقني

دورات مظهر التعليقات الذهاب والإياب في Delphi مع PDFium

في مكوّن PDFium قبل v3.121.1، كانت قراءة تعليق عبر TPdf.Annotation[] ثم إسناد السجل مرة أخرى قد تضيف مدخلات /R و/D فارغتين إلى قاموس المظهر /AP الخاص به، حتى حين كان الأصل يحمل /N وحدها. ويرفض مدققو PDF/A ذلك القاموس. ومنذ v3.121.1 لا يبلّغ الجالب عن مظهر إلا إذا قرأه فعلًا، فالدورة غير المتغيرة لا تكتب شيئًا جديدًا. ويستحق هذا الإخفاق الفهم بالتفصيل، لأن المحفز المعتاد إصلاح كان يقصد جعل الملف أكثر امتثالًا لا أقل

مخطط لدورة تعليقات PDFium Component الذهاب والإياب حيث تضيف إضافة afPrint عبر TPdf.Annotation[] وSetAnnotationData أيضا تدفقات /R و/D فارغتين عبر FPDFAnnot_SetAP، فتحوّل قاموس مظهر نظيف في PDF/A إلى قاموس يرفضه veraPDF حتى يبلّغ v3.121.1 عن المظاهر التي قرأها فعلًا وحدها
كانت قراءة تعليق وإعادة كتابته دون تغيير تضيف تدفقَي مظهر تمرير وضغط فارغين، وهذا ما يبوظ PDF/A، لا علم الطباعة الذي قصدت إضافته

ما الذي يخيب حين تعيد كتابة تعليق دون تغييره؟

الإجابة المختصرة: ينال التعليق تدفقات مظهر لم يملكها قط، وملف اجتاز تحقق PDF/A قبل تعديلك يبوظ بعده. السيناريو النمطي يجري هكذا. يصل أرشيف عميل بتعليقات مربعات ونصوص تنقصها علم الطباعة، ويشترط PDF/A أن يُطبع كل تعليق، فتجوب الصفحات وتضيف afPrint وتُسند كل سجل مرة أخرى. لا شيء في ذلك الكود يلمس المظاهر. السجل القادم من TPdf.Annotation[] هو TPdfAnnotation، وتكتب SetAnnotationData كل حقل ضُبط أسيسه Has*، وهذا بالضبط كيف صُممت أزواج HasContents / ContentsText. كانت المشكلة أن الجالب ضبط HasAppearanceRollover وHasAppearanceDown إلى True بنص فارغ للأنماط غير الموجودة، وكتب الواضع بكل اجتهاد تدفقين فارغين:

procedure MarkAnnotationsPrintable(const FileName: string);
var
  Pdf: TPdf;
  PageNo, I: Integer;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    for PageNo := 1 to Pdf.PageCount do
    begin
      Pdf.PageNumber := PageNo;
      for I := 0 to Pdf.AnnotationCount - 1 do
      begin
        A := Pdf.Annotation[I];
        if not (afPrint in A.Flags) then
        begin
          A.Flags := A.Flags + [afPrint] - [afHidden, afInvisible, afNoView];
          // قبل v3.121.1 كان هذا الإسناد يكتب أيضا /AP/R و
          // /AP/D فارغتين حين كان التعليق المصدر يملك /AP/N وحدها
          Pdf.Annotation[I] := A;
        end;
      end;
    end;
    Pdf.SaveAs(ChangeFileExt(FileName, '.printable.pdf'));
  finally
    Pdf.Free;
  end;
end;

يعرف ISO 32000-1 §12.5.5 قاموس المظهر بثلاثة مدخلات: /N للمظهر العادي، و/R للتمرير فوقه، و/D للضغط عليه. و/R و/D اختياريتان، وحين تغيبان يرتد العارض إلى /N. لكن تدفق /R فارغ ليس غائبًا؛ إنه تدفق صالح لا يرسم شيئًا، فالعارض الذي يحترم مظاهر التمرير يعرض مستطيلًا فارغًا لحظة تحرك المؤشر فوق التعليق. وPDF/A أصرم من ذلك: يسمح ISO 19005-1 (مع Corrigendum 2) وISO 19005-2 / 19005-3 بـ /N وحدها في قاموس مظهر التعليق. يبلّغ veraPDF عن الملف العابر للدورة تحت القاعدة 6.5.3-4 لـ PDF/A-1 والقاعدة 6.3.3-2 لـ PDF/A-2 وPDF/A-3، ويسردها TPdf.ValidatePdfA المدمج بوصفها pvaiAnnotationApDictViolation. التعديل الذي أضاف علم الطباعة ليستيفي بندًا من المعيار بوظ بندًا آخر

قاموس مظهر التعليق من ISO 32000-1 بمدخلاته العادية وللتمرير وللضغط: يعيد PDFium بايتين للتدفق الغائب وللفارغ الموجود على السواء، فيقرأ كلاهما بلا محتوى عبر TPdf، بينما يكشف الفحص على مستوى البايتات مثل TPdf.ValidatePdfA التدفق الفارغ الذي يحرمه PDF/A وحده
تتراجع /R الغائبة إلى /N؛ وترسم /R الفارغة مستطيلًا أبيض ومع ذلك تُسقط PDF/A، وعبر السجل لا سبيل للتمييز بينهما

لماذا تعيد FPDFAnnot_GetAP القيمة 2 لمظهر غائب؟

لا تعيد PDFium الصفر قط من FPDFAnnot_GetAP، حتى لو كان تدفق المظهر المطلوب غير موجود. تتبع الدالة نمط PDFium الاعتيادي للاستدعاءين: تمرر مخزنًا nil لتأخذ الحجم المطلوب بالبايتات، فتحجز، ثم تستدعي ثانيةً لنسخ نص UTF-16LE. الحجم يشمل دائمًا منتهي UTF-16، فالتدفق الغائب يبلّغ 2 بايت، أي نص فارغ مع منتهيه. كان جالب ما قبل v3.121.1 يفحص ByteLength >= SizeOf(FPDF_WCHAR)، وهو شرط يجتازه كل استدعاء، فعادت أعلام HasAppearance* الثلاثة True لأي تعليق يملك أي مظهر أصلًا. ثم طلبت دورة السجل من FPDFAnnot_SetAP أن يخزن نصًا فارغًا لكل نمط، فأنشأ PDFium التدفق ليحتويه. لا استثناء ولا تحذير، وكانت الصفحة المرئية مطابقة، ولهذا برز العيب في ثابت veraPDF لا في عارض

كيف يبلّغ FPDFAnnot_GetAP عن تدفق مظهر غائب في PDFium: يعيد نمط الاستدعاءين دائمًا بايتين على الأقل لمنتهي UTF-16، وكانت البوابة القديمة المقارنة بـ SizeOf(FPDF_WCHAR) تجتاز كل استدعاء وتضبط جميع أسيس HasAppearance إلى true، بينما تشترط بوابة v3.121.1 أكثر من المنتهي وعدد بايتات زوجيًا
البايتان هما النص الفارغ المشفر، لا دليل على وجود مظهر؛ يعامل الجالب المصحح كل ما دون طول المنتهي بوصفه بلا محتوى فيبقى الإعيد صامتًا

كيف يحسم v3.121.1 أن مظهرًا موجود فعلًا

تعامل ReadAppearance، المساعدة داخل GetPageAnnotation التي تملأ AppearanceNormal وAppearanceRollover وAppearanceDown، النتيجة الآن بوصفها محتوى فقط عندما تحمل محرفًا واحدًا على الأقل فوق المنتهي. يجب أن يعيد الاستدعاء الأول أكثر من SizeOf(FPDF_WCHAR) بايت وعددًا زوجيًا من البايتات، لأن الطول الفردي لا يمكن أن يكون UTF-16. ويُتحقق من الاستدعاء الثاني الذي ينسخ النص فعلًا مرة أخرى: طول معاد بقيمة 2 أو أقل، أو أكبر من المخزن المحجوز، يعيد HasValue إلى False ويترك النص فارغًا. أما جانب الكتابة فلم يتغير شيء. ما زالت SetAnnotationData تستدعي FPDFAnnot_SetAP للأنماط التي علم HasAppearance* فيها True فقط، فالسجل المقروء من تعليق يملك /N وحدها يُعاد كتابته بـ /N وحدها. يغطي ثابت الانحدار الاتجاهين: مربع تعليق بمظهر عادي، يُقرأ ويُعاد كتابته دون تغيير، يجتاز PDF/A-1b وPDF/A-2b وPDF/A-3b، بينما يفشل المربع نفسه بعد إزالة علم الطباعة على قاعدة العلم المتوقعة وحدها وبلا شيء غيرها

التدفقان الغائب والفارغ يبدوان متطابقين، فيبقى الجالب متحفظًا

لا تستطيع الـ API الأصلية تمييز تدفق مظهر غائب عن آخر موجود لكنه فارغ، ولا يدّعي مكوّن PDFium خلاف ذلك. كلا الحالتين تعيد البايتين نفسهما من FPDFAnnot_GetAP، فيقرأ كلاهما HasAppearanceRollover = False مع AppearanceRollover فارغة. لذلك أمران عليك أن تصمم على أساسهما. أولًا، الأسيس False يعني «لم يُقرأ محتوى، فلن يلمس الإعيد هذا النمط»، لا «مفتاح /R غائب عن القاموس». وثانيًا، لا يستطيع السجل كشف تدفق فارغ موجود سلفًا في الملف: مستند أتلبه نسخة أقدم أو أداة أخرى يعود نظيف المظهر، وإعادة إسناد السجل لا ترممه ولا تزيده سوءًا. لإيجاد تلك الملفات تحتاج فحصًا على مستوى البايتات، وهو ما صُمم له TPdf.ValidatePdfA ومسار التحقق القبلي لـ PDF/A مع مكوّن PDFium

كيف تمحو مظهرًا عمدًا؟

تضبط الأسيس صراحةً وتمرر نصًا فارغًا، فيكتبه الواضع. كان حجب النصوص الفارغة في SetAnnotationData سيكون العلاج الغليظ لهذا العيب، لكنه كان سيكسر أيضًا المستدعين الذين يمحوون مظهرًا عمدًا، وهو العقد نفسه الذي يتبعه HasContents وHasAuthor للنصوص. لذلك سكن الإصلاح كله في الجالب، ويواصل الواضع إجابة ما يطلبه المستدعي مهما كان:

// استبدل مظهر التمرير، ثم امحوه من جديد
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;

A := Pdf.Annotation[0];
// قيمة A.HasAppearanceRollover تساوي True والنص يعود دورة كاملة 'q Q'
A.HasAppearanceRollover := True;   // أعد التأكيد على القصد صراحة
A.AppearanceRollover := '';        // اكتب تدفقًا فارغًا عمدًا
Pdf.Annotation[0] := A;

A := Pdf.Annotation[0];
// يُقرأ HasAppearanceRollover = False مع نص فارغ:
// التدفق الفارغ والغائب لا يفرَّق بينهما هنا

تذكر أن /R أو /D المُفرَّغتين صراحةً يبقيان يُحسبان مفتاحًا زائدًا وفق قواعد PDF/A المذكورة أعلاه. إن كان الهدف ملف أرشيف، فكتابة /N غير فارغة وترك النمطين الآخرين بلا مساس هو الشكل الوحيد الذي يجتاز التحقق. وأي مسار معالجة ينقل التعليقات بين مستندات، مثل تصدير XFDF واستيراده مع مكوّن PDFium، عليه اتباع القاعدة نفسها: انسخ الأنماط التي حملها المصدر فعلًا واترك بقية الأسيسات False

نمط قراءة-تعديل-كتابة يبقى آمنًا مع PDF/A

ارتقِ إلى v3.121.1 أو أحدث، واترك أسيسات المظهر تمامًا كما أعادها الجالب، وتحقق من الملف المحفوظ قبل شحنه. ولأن التدفق الفارغ القديم يُقرأ غائبًا، فخطوة التحقق عليها أن تنظر إلى المستند المُسلسل لا إلى السجل، وهي رخيصة بما يكفي لتجرى بعد كل دفعة:

uses
  PDFium, FPdfPdfa;  // يعلن FPdfPdfa نوع TPdfAValidationIssue

function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
  Report: TPdfAValidationResult;
begin
  // يتحقق من المستند المحمل حاليًا في Pdf، بما في ذلك التعديلات
  // المنفذة عبر Pdf.Annotation[] منذ فتحه
  Report := Pdf.ValidatePdfA;
  Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;

ينطبق الانضباط نفسه على أي لوحة تعيد تلوين الصفحات أو تعلّقها للمراجعة، وهو مسار مغطى في بناء مسار مراجعة تعليقات في Delphi مع مكوّن PDFium: السجل لقطة لما استطاع المحرك قراءته، والأسيس الذي لم تضبطه بنفسك عليه أن يعود دون تغيير. تُشحن واجهة التعليقات الكاملة والتحقق القبلي لـ PDF/A ومحرك PDFium الأصلي معًا في PDFium Component لـ Delphi وC++Builder وLazarus