مقال تقني

تقارير الفحص المسبق لملفات PDF المجمعة في Delphi مع مكون PDFium CLI

إن أداة الفحص المسبق (preflight) المجمعة عبارة عن برنامج وحدة تحكم (console) بلا نافذة، موجه إلى مجلد من ملفات PDF، والذي يتحقق من صحة كل منها وفقًا لمعايير المطابقة التي تسميها ويترك وراءه دليلاً مقروءًا آليًا لما وجدته. لا أحد يجلس ويراقب ذلك. يتم تشغيله في الساعة الثانية صباحًا ضمن جدول مهام cron أو Windows، أو كبوابة في مسار CI (التكامل المستمر)، والشخص التالي الذي يهتم بإنتاجه هو إما مجدول يقرأ رمز الخروج أو مدقق حسابات يفتح تقريرًا بعد أسابيع. هذا يغير ما يعنيه "الصحيح". محرك الفحص المسبق لمكون PDFium، وهو مكتبة مصدر PDF لـ Delphi و C++Builder و Lazarus، يجعل مكالمات التحقق نفسها شبه تافهة. يقع العمل الذي يقرر ما إذا كانت الأداة تكسب ما تبقيه حول تلك المكالمات: أي ملف تعريف قمت بفحصه، وماذا قال كود الخروج للمجدول، وما إذا كان التقرير الذي كان من شأنه أن يمسك بخطأ لا يزال موجودًا عندما يبحث عنه شخص ما

العقد: ما يمكن للمجدول أن يراه بالفعل

يرى عداء CI أو جدولة مهام Windows شيئين بالضبط من أداتك: رمز الخروج (exit code) وأي ملفات تركها وراءها. خطوط السجل (Log lines)، ألوان وحدة التحكم، الإخراج المرحلي (progress output): كل ذلك لمشاهدة الإنسان على الهواء، وفي الساعة الثانية صباحاً لا أحد كذلك. لذا أصلح مفردات كود الخروج قبل أن تلمس API، وحافظ عليها مملة:

  • 0: كل ملف يتوافق مع كل ملف تعريف مطلوب
  • 1: أنتج ملف واحد على الأقل نتائج التحقق
  • 2: فشلت الأداة نفسها في ملف واحد على الأقل (إدخال تالف، قفل، انهيار)

التمييز بين الكودين 1 و 2 هو التمييز الذي يتخطاه الفرق وتندم عليه لاحقًا. ملف PDF التالف الذي لن يُفتح لا يُعد فشلاً في التحقق. ادمجها في الرمز 1 وتظهر حمولة شاحنة من عمليات المسح التالفة في لوحات المعلومات الخاصة بك على شكل انهيار مفاجئ للتوافق، مما يرسل شخصًا ما لمطاردة تراجع في المعايير لم يحدث أبدًا، في حين أن القصة الحقيقية هي ماسح ضوئي معطل في المنبع

يوجد عنصران آخران ينتميان إلى العقد. الأول هو مهلة لكل ملف (per-file timeout). يمكن لـ PDF باثولوجي، بآلاف الصفحات ذات هياكل الكائنات المتداخلة بعمق، أن يحمل ممر تحقق واحد (single validation pass) لدقائق، وليس لنافذة ليلية أي صبر عليه. اقتل مهمة ذلك الملف في الموعد النهائي، واعتبره فشلاً في الأداة، واستمر في تحريك الدفعة. الدليل الثاني هو دليل الحجر الصحي: ضع كل مدخل مهلة أو غير قابل للفتح جانباً بدلاً من تركه في مكانه. على مدار بضعة أشهر، يجمع هذا الدليل بهدوء أسوأ المستندات التي يرسلها عملاؤك الحقيقيون، وهذه المجموعة تستحق إطلاق الاختبار أكثر من أي عينة تركيبية (synthetic sample) يمكنك كتابتها يدويًا

اختيار المعايير، ولماذا يهم مستوى التوافق

يغطي التعداد TPdfPreflightStandard العائلات التي تظهر في الممارسة: ppsPdfA للامتثال الأرشيفي لـ ISO 19005، و ppsPdfUa لإمكانية الوصول إلى ISO 14289، و ppsPdfX لتبادل المطبوعات، بالإضافة إلى ppsPdfE، و ppsPdfR، و ppsPdfVT للأعمال الهندسية والنقطية والبيانات المتغيرة. داخل عائلة ما، يقرأ المحرك مستوى المطابقة الذي يدعيه المستند ويُبلغ عنه لكل معيار في ConformanceName النتيجة. نادرًا ما تكون تسمية العائلة كافية، لأن المستوى هو المكان الذي يعيش فيه الفرق الحقيقي. يَعِد PDF/A-2b بإمكانية استنساخ بصرية ولا شيء أكثر. يضيف PDF/A-3a طلبًا لوضع علامات البنية المنطقية ويسمح بملفات المصدر المضمنة، وهو شريط (bar) أصعب بكثير لتوضيحه بالنسبة للمواد الممسوحة ضوئيًا التي لا تحتوي على شجرة علامات على الإطلاق. اخطئ في هذا في أي من الاتجاهين وستكذب الدفعة عليك. إذا كانت سياسة الاحتفاظ الخاصة بك تريد بالفعل PDF/A-2b لكنك فشلت في وضع علامات على البنية المفقودة في الملفات، فإن التقرير يمتلئ بالنتائج التي لن يقوم أحد بإصلاحها أبدًا. اقبل أي تسمية PDF/A دون التحقق من المستوى وستوقع على المستندات التي تلبي مستوى أضعف مما وعدت به. تزيد تفويضات إمكانية الوصول من المشترين الحكوميين بشكل متزايد من تكديس PDF/UA على رأس كل هذا، مما لا يضيف أي تكلفة على التشغيل لأن BuildPdfPreflightReport (من الوحدة FPdfPreflightReport) يأخذ مجموعة من المعايير:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

يُقيّم استدعاء واحد كلاً من المعيارين ويسلم سجلاً واحداً موحداً للتقرير

لماذا لا تعتبر قائمة النتائج الفارغة تمريرًا

يحصي التقرير النتائج لكل معيار، وتعني قائمة المشكلات الفارغة فقط "لم يتم العثور على مشاكل في المعايير التي تم تشغيلها بالفعل." هذا ادعاء أضيق من "الملف يتوافق مع المعيار الذي تهتم به"، والفجوة بين الاثنين هي المكان الذي يتعفن فيه الفحص المسبق للدُفعات (batch preflight) بهدوء. يؤدي وجود خطأ مطبعي في التكوين يؤدي إلى إسقاط ppsPdfA من المجموعة إلى إنتاج قائمة المشكلات الفارغة نفسها تمامًا مثل ملف نظيف حقيقي. لذا عامل الصمت كأمر مشبوه (treat silence as suspect). تجاوز Report.Results وأكد على أمرين لكل معيار قصدت التحقق منه: وجود إدخال نتيجة له على الإطلاق، وأن علامة IsCompliant الخاصة به، مدعومة بـ Status = pfsPass، صحيحة. المهمة الليلية التي تساوي "لا توجد نتائج" مع "جاهز للأرشفة" دون تأكيد أي المعايير تم تقييمها هي الطريقة الكلاسيكية التي يبحر بها مجلد من الملفات غير المطابقة لعدة أشهر، حتى يفتح مدقق خارجي واحدًا باستخدام veraPDF ويصبح الأرشيف بأكمله موضع تساؤل

فخ ثان يختبئ في ما هي حتى النتيجة. يحمل كل TPdfPreflightIssue كوداً Code، وفئة Category، ووصفاً Description، وتوصية Recommendation، ويسمي القاعدة التي تم انتهاكها، وليس صفحة أو كائن. هذا اختيار تصميم له عواقب على حلقة التغذية الراجعة (feedback loop). يخبر التقرير الفريق المُنتج بفئة العيب الموجودة، سواء كانت خطًا غير مضمن (unembedded font) أو مُعرّف XMP مفقود، ويعد العثور على الكائن المسيء المحدد هو وظيفة أداة الإصلاح في المصب (downstream)، وليس المُدقق (validator). قم ببناء مستهلكي تقريرك ضد قيم الكود المستقرة (stable Code values)، وليس أبدًا ضد نص الوصف القابل للقراءة بواسطة الإنسان، والذي يمكن إعادة صياغته بين الإصدارات دون سابق إنذار

ملفات التقارير للأجهزة والشخص المناوب

يكتب سجل التقرير نفس النتائج بخمسة تنسيقات: SaveJsonToFile و SaveCsvToFile و SaveHtmlToFile و SaveTextToFile و SaveMarkdownToFile، وكل منها يتمتع بوظيفة مطابقة بأسلوب ToJson عندما تريد السلسلة (string) في الذاكرة بدلاً من القرص. قاوم الرغبة في اختيار واحدة. اكتب JSON لمسار التدفق (pipeline)، حتى يتمكن CI من إرفاقه بسجل المهمة وتحليل رموز المشكلات (issue codes) وحالات لكل معيار (per-standard statuses) دون تجريد النص (scraping text). اكتب HTML للإنسان الذي يتلقى رسالة بيج (paged)، لأنه يفتح في أي متصفح دون أي أدوات على الإطلاق. يكلف الاثنان معًا سطرًا إضافيًا واحدًا لكل ملف ويوفر لمهندس المناوبة (on-call engineer) لديك أسوأ مهمة في المعالجة الدفعية (batch processing)، وهي الهندسة العكسية لنقطة JSON خام (raw JSON blob) في الثانية صباحًا لمعرفة الملف الذي كُسر. هناك انضباط واحد يهم أكثر من اختيار التنسيق: اشتقاق اسم كل تقرير من اسم ملف الإدخال، وليس من طابع زمني أبدًا، أو ستتداخل فترتان متوازيتان مع التقارير التي لم يعد بإمكانك مطابقتها مع مدخلاتها

تنتمي عتبات الخطورة (Severity thresholds) إلى التكوين بدلاً من التعليمات البرمجية. يعد التعليق التوضيحي الذي لا يحتوي على وصف بديل فشلاً صعبًا لبوابة تقديم PDF/UA وملاحظة يمكن تجاهلها للأرشيف الداخلي، ومع ذلك فهو النتيجة المتطابقة في كليهما. كشف مستوى الفشل لكل ملف تعريف بحيث يمكن أن تتحول السياسة دون إعادة تجميع، وختم المستوى الذي كان سارياً في ملخص المهمة نفسه. في الربع القادم لن يتذكر أحد العتبة التي تمت تحتها دفعة أكتوبر الماضي، والملخص هو المكان الوحيد الذي تنجو فيه الذاكرة

عزل الملفات بحيث لا يمكن لملف PDF سيء واحد أن يغرق الدفعة

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // load failures are silent, not raised
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

توجد ثلاثة اختيارات مدروسة في تلك الحلقة. يضمن كل TPdf جديد لكل ملف أن مستندًا واحدًا يفسد حالة المحرك لا يمكنه تسميم الملفات التي تتبعه. يكسب الفحص الصريح لـ Active مكانه لأن Active := True يبتلع أخطاء التحميل بدلاً من رفعها؛ قم بإسقاط الحرس وينجرف الملف المقطوع (truncated file) إلى مكالمة التحقق قبل أن يفشل في مكان ما في المصب (downstream) برسالة مضللة. الداخلي try..except يعيش داخل النطاق لكل ملف عن قصد، لذا فإن استثناءً واحداً يصطدم بعداد الفشل وتستمر الحلقة. تريد تقارير نظيفة لـ 4999 ملفًا جيدًا حتى عندما يتم تمزيق 5000 ملف. ويتم كتابة كلا التنسيقين من التقرير إلى القرص قبل فرز الحكم (verdict is tallied)، مما يعني أن الدليل ينجو حتى إذا أخطأ الخلل لاحقًا في منطق الملخص في الحساب

ينهار تعيين رمز الخروج (exit-code mapping) بعد ذلك إلى بضعة أسطر في ملف المشروع:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // falling through exits with 0: every file conformed
end.

ما لن يفعله الفحص المسبق لك

المحرك يكتشف؛ لا يصلح. النتيجة حول خط غير مضمّن (unembedded font) أو مساحة لون تعتمد على الجهاز هي أمر عمل لكل من ينتج الملفات، ولا يملك المدقق أي طريقة لتصحيحها في مكانها. لذا خطط لحلقة التغذية الراجعة (feedback loop) بشكل متعمد. يجب أن تهبط التقارير حيث يقرأها فريق الإنتاج بالفعل، أو تظهر نفس النتائج كل ليلة حتى يسأل شخص ما أخيرًا لماذا لا يتحسن معدل المطابقة أبدًا. كما أنه من المجدي التحقق من عينة من الأحكام ضد مُدقق مستقل (independent validator)، مثل veraPDF لـ PDF/A أو Acrobat's preflight لـ PDF/X، قبل أن يتحقق مدقق خارجي منها نيابة عنك. عندما يختلف محركان على ملف عميل حقيقي، فإن هذا المستند ليس مصدر إزعاج؛ إنها بالضبط حالة الانحدار (regression case) التي كان اختبار الإصدار (release testing) يفتقدها. احتفظ بها وسمها وشغلها على كل مبنى

اقتران آخر يستحق المعرفة. يقود محرك التحقق نفسه الفحوصات التفاعلية في واجهة مستخدم المراجعة (review UI)، لذا يمكن لمفردات CLI المقطوعة الرأس هذه و منصة مراجعة استقبال PDF المواجهة للمحلل مشاركة مفردات تحقق واحدة بدلاً من الانجراف بعيدًا بمرور الوقت. ولأن [ppsPdfA, ppsPdfUa] يقيّم إمكانية الوصول في نفس التمريرة، فإن جانب PDF/UA من الدفعة يتماشى بشكل نظيف مع عمل جانب العارض مثل بناء قارئ PDF يمكن الوصول إليه في Delphi. تم توثيق ملفات التعريف (Profiles)، وتنسيقات التقارير، و API الكاملة لما قبل الطيران على صفحة منتج مكون PDFium