مقاله فنی

گزارش‌های دسته‌ای پیش‌از-پرواز PDF در دلفی با CLI کامپوننت PDFium

یک ابزار دسته‌ای پیش‌از-پرواز (batch preflight)، یک برنامه کنسول بدون پنجره است که به پوشه‌ای از PDFها اشاره می‌کند، هر کدام را بر اساس استانداردهای انطباق (conformance) که شما نام می‌برید اعتبارسنجی می‌کند و مدرکِ قابل خواندن توسط ماشین را از آنچه پیدا کرده است، به جای می‌گذارد. هیچ‌کس نمی‌نشیند و آن را تماشا نمی‌کند. این ابزار ساعت دو صبح تحت cron یا Windows Task Scheduler اجرا می‌شود، یا به عنوان یک دروازه (gate) در یک خط لوله CI، و نفر بعدی که به خروجی آن اهمیت می‌دهد، یا یک زمان‌بند (scheduler) است که کد خروج را می‌خواند یا بازرسی است که هفته‌ها بعد گزارشی را باز می‌کند. این موضوع، معنای کلمه "درست" (correct) را تغییر می‌دهد. موتور پیش‌از-پرواز PDFium Component، یک کتابخانه PDF سورس‌کد برای دلفی، C++Builder، و لازاروس، خود فراخوانی‌های اعتبارسنجی را تقریباً بی‌اهمیت و آسان می‌کند. کاری که تصمیم می‌گیرد آیا این ابزار ارزش زحمات خود را داشته است یا خیر، در اطراف این فراخوانی‌ها قرار دارد: کدام پروفایل را بررسی کردید، کد خروج به زمان‌بند چه گفت، و آیا گزارشی که می‌توانست اشتباهی را بگیرد، زمانی که کسی به دنبال آن می‌گردد هنوز وجود دارد یا خیر

قرارداد: یک زمان‌بند واقعاً چه چیزی را می‌تواند ببیند

یک اجراکننده CI یا Windows Task Scheduler دقیقاً دو چیز از ابزار شما می‌بیند: کد خروج (exit code) و هر فایلی که به جای گذاشته است. خطوط لاگ، رنگ‌های کنسول، خروجی پیشرفت: همه اینها برای انسانی است که به طور زنده تماشا می‌کند، و ساعت دو صبح هیچ‌کس نیست. بنابراین قبل از اینکه API را لمس کنید، دایره واژگانِ کد-خروج را ثابت کنید، و آن را خسته‌کننده نگه دارید:

  • 0: هر فایلی با هر پروفایل درخواستی منطبق بود
  • 1: حداقل یک فایل یافته‌های اعتبارسنجی (validation findings) تولید کرد
  • 2: خود ابزار در حداقل یک فایل با شکست مواجه شد (ورودی خراب، قفل، کرش)

تفاوت بین کدهای 1 و 2 همان چیزی است که تیم‌ها آن را نادیده می‌گیرند و بعداً پشیمان می‌شوند. یک PDF خراب که باز نمی‌شود، خطای اعتبارسنجی (validation failure) نیست. آن را در کد 1 تا (fold) کنید و خروارها اسکن آسیب‌دیده به عنوان یک فروپاشی ناگهانی در انطباق در داشبوردهای شما ظاهر می‌شود، و کسی را به دنبال یک پس‌رفت (regression) استاندارد می‌فرستد که هرگز اتفاق نیفتاده است، در حالی که داستان واقعی، یک اسکنر خراب در بالای خط است

دو مورد دیگر به این قرارداد تعلق دارند. اولین مورد تایم‌اوت (timeout) به ازای هر فایل است. یک PDF بیمارگونه (pathological)، با هزاران صفحه و ساختارهای شیء تودرتوی عمیق، می‌تواند یک گذر اعتبارسنجیِ واحد را برای دقایقی نگه دارد، و یک پنجره اجرای شبانه هیچ تحملی برای آن ندارد. در پایانِ مهلت، کار آن فایل را بکشید (kill)، آن را به عنوان خرابی ابزار (tool failure) حساب کنید، و دسته (batch) را در حال حرکت نگه دارید. دومی یک دایرکتوری قرنطینه است: هر ورودیِ تایم‌اوت شده یا غیرقابل‌بازشدن را به جای رها کردن در همان‌جا، کنار بگذارید. در طول چند ماه آن دایرکتوری بی‌صدا بدترین اسنادی را که مشتریان واقعی شما ارسال می‌کنند جمع‌آوری می‌کند، و آن مجموعه (corpus) برای تستِ انتشار (release testing) ارزش بیشتری نسبت به هر نمونه مصنوعی که بتوانید با دست بنویسید دارد

انتخاب استانداردها، و چرا سطح انطباق (conformance level) مهم است

شمارش TPdfPreflightStandard خانواده‌هایی را پوشش می‌دهد که در عمل مطرح می‌شوند: ppsPdfA برای انطباق آرشیوی ISO 19005، ppsPdfUa برای دسترسی‌پذیری ISO 14289، ppsPdfX برای تبادل چاپ، به علاوه ppsPdfE، ppsPdfR، و ppsPdfVT برای کارهای مهندسی، رستر، و داده‌های متغیر. در درون یک خانواده، موتور سطحی از انطباق را که سند ادعا می‌کند می‌خواند و آن را در هر استاندارد در ConformanceName از نتیجه گزارش می‌دهد. نام بردن خانواده به ندرت کافی است، زیرا سطح جایی است که تفاوت واقعی در آن نهفته است. PDF/A-2b نوید بازتولید بصری (visual reproducibility) را می‌دهد و نه هیچ‌چیز دیگر. PDF/A-3a درخواستی برای تگ‌گذاری ساختار منطقی اضافه می‌کند و اجازه جاسازی فایل‌های منبع را می‌دهد، که مانعی بسیار سخت‌تر برای مطالب اسکن شده است که اصلاً درخت تگ ندارند. این را در هر دو جهت اشتباه متوجه شوید و دسته (batch) به شما دروغ می‌گوید. اگر خط‌مشی نگهداری شما واقعاً PDF/A-2b می‌خواهد اما شما فایل‌ها را به دلیل از دست دادن تگ‌های ساختار (structure tags) مردود می‌کنید، گزارش پر از یافته‌هایی می‌شود که هیچ‌کس هرگز آنها را برطرف نخواهد کرد. هر برچسب PDF/A را بدون بررسی سطح (level) بپذیرید و اسنادی را امضا می‌کنید که دارای سطح ضعیف‌تری نسبت به آنچه شما قول داده بودید هستند. دستورات دسترسی‌پذیری از سوی خریداران دولتی به طور فزاینده‌ای PDF/UA را در بالای همه این موارد انباشته می‌کند، که هیچ هزینه‌ای برای اجرا اضافه نمی‌کند زیرا BuildPdfPreflightReport (از یونیت FPdfPreflightReport) مجموعه‌ای از استانداردها را می‌گیرد:

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

یک فراخوانی هر دو استاندارد را ارزیابی می‌کند و یک رکورد گزارش تلفیقی واحد را پس می‌دهد

چرا لیست خالیِ یافته‌ها به معنای قبولی (pass) نیست

این گزارش یافته‌ها را در هر استاندارد برمی‌شمارد، و لیست خالی مشکلات فقط به معنای "هیچ مشکلی در استانداردهایی که واقعاً اجرا شده‌اند یافت نشد" است. این ادعای محدودتری است نسبت به "فایل با استانداردی که شما به آن اهمیت می‌دهید مطابقت دارد"، و فاصله بین این دو جایی است که پیش‌از-پرواز دسته‌ای بی‌صدا می‌پوسد. یک اشتباه تایپی در پیکربندی که ppsPdfA را از مجموعه حذف می‌کند، دقیقاً همان لیست خالیِ مشکلات را به عنوان یک فایل واقعاً تمیز تولید می‌کند. پس سکوت را مشکوک تلقی کنید. در Report.Results پیمایش کنید و برای هر استانداردی که قصد بررسی آن را داشتید دو چیز را ادعا کنید (assert): اینکه اصلاً یک ورودیِ نتیجه (result entry) برای آن وجود داشته باشد، و اینکه پرچم IsCompliant آن، که توسط Status = pfsPass پشتیبانی می‌شود، true باشد. یک کار (job) شبانه که "بدون یافته‌ها" را با "آماده بایگانی" بدون تأیید اینکه کدام استانداردها ارزیابی شده‌اند یکی می‌داند، روشی کلاسیک است که یک پوشه از فایل‌های غیرمنطبق ماه‌ها از آن عبور می‌کند، تا زمانی که یک حسابرس خارجی یکی از آنها را با veraPDF باز می‌کند و کل آرشیو زیر سؤال می‌رود

تله دوم در این است که یک یافته اساساً چیست. هر TPdfPreflightIssue یک Code، یک Category، یک Description، و یک Recommendation به همراه دارد، و نام قانونی را که نقض شده است می‌آورد، نه یک صفحه یا یک شیء. این یک انتخاب طراحی با پیامدهایی برای حلقه بازخورد است. این گزارش به تیم تولیدکننده می‌گوید چه نوع نقصی وجود دارد، یک فونت جاسازی نشده یا یک شناسه XMP گم‌شده، و یافتن شیء متخلفِ خاص، وظیفه ابزار اصلاحی (remediation tool) در ادامه راه است، نه اعتبارسنج. مصرف‌کنندگان گزارش خود را بر اساس مقادیر پایدار Code بسازید، نه هرگز بر اساس متن توضیحاتِ قابل خواندن برای انسان، که ممکن است بدون اخطار بین نسخه‌ها دوباره جمله‌بندی شود

فایل‌های گزارش برای ماشین‌ها و برای شخصِ آن‌کال (on call)

رکورد گزارش همان یافته‌ها را در پنج فرمت می‌نویسد: SaveJsonToFile، SaveCsvToFile، SaveHtmlToFile، SaveTextToFile، و SaveMarkdownToFile، که هر کدام یک تابع منطبق با سبکِ ToJson دارند، زمانی که شما رشته (string) را به جای روی دیسک، در حافظه می‌خواهید. در برابر میل به انتخاب تنها یکی مقاومت کنید. JSON را برای خط لوله (pipeline) بنویسید، تا CI بتواند آن را به رکورد کار (job record) ضمیمه کند و کدهای مشکل و وضعیت‌های هر-استاندارد را بدون اسکرپ کردنِ (scraping) متن تجزیه کند. HTML را برای انسانی بنویسید که پیج می‌شود، زیرا در هر مرورگری بدون هیچ ابزاری باز می‌شود. این دو با هم هزینه یک خط اضافی به ازای هر فایل را دارند و مهندس آن‌کال شما را از بدترین وظیفه ممکن در پردازش دسته‌ای نجات می‌دهند، که همانا مهندسی معکوسِ یک حباب (blob)ِ JSON خام در ساعت دو صبح برای فهمیدن اینکه کدام فایل خراب شده است. یک نظم مهم‌تر از انتخاب فرمت است: هر نام گزارش را از نام فایل ورودی استخراج کنید، نه هرگز از یک مُهر زمانی (timestamp)، در غیر این صورت دو اجرای موازی گزارش‌هایی را در هم می‌آمیزند (interleave) که دیگر نمی‌توانید آنها را به ورودی‌هایشان برگردانید و مطابقت دهید

آستانه‌های شدت (Severity thresholds) به جای کد، به پیکربندی (configuration) تعلق دارند. حاشیه‌نویسی‌ای که توضیحات جایگزین (alternate description) ندارد برای یک پورتال ارسال PDF/UA یک خرابیِ سخت (hard failure) و برای یک آرشیو داخلی یک یادداشت قابل چشم‌پوشی است، با این حال در هر دو یافتهِ یکسانی است. سطحی برای شکست (fail-on) در هر پروفایل ارائه دهید تا خط‌مشی بتواند بدون نیاز به کامپایل مجدد (recompile) تغییر کند، و سطحی را که در حال اجرا بوده در خودِ خلاصه کار (job summary) مُهر بزنید. سه‌ماهه آینده هیچ‌کس به یاد نخواهد آورد که دسته (batch) اکتبر گذشته تحت چه آستانه‌ای اجرا شده است، و خلاصه، تنها جایی است که آن خاطره زنده می‌ماند

جداسازی فایل‌ها تا یک PDF بد نتواند دسته (batch) را غرق کند

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 خطاهای بارگذاری را به جای بالا آوردن (raising)، می‌بلعد؛ این گارد (guard) را رها کنید و یک فایل کوتاه شده (truncated) در فراخوانی اعتبارسنجی سرگردان می‌شود قبل از اینکه جایی در ادامه مسیر با پیامی گمراه‌کننده خراب شود. درونی‌ترین try..except به طور هدفمند در داخل محدوده هر فایل (per-file scope) قرار دارد، بنابراین یک استثنای (exception) منفرد، شمارنده خرابی را بالا می‌برد و حلقه ادامه می‌یابد. شما گزارش‌های تمیزی برای 4999 فایل خوب می‌خواهید حتی زمانی که فایل 5000 خرد شده باشد. و هر دو فرمت گزارش قبل از اینکه رأی (verdict) شمرده شود روی دیسک نوشته می‌شوند، که این یعنی مدارک زنده می‌مانند حتی اگر یک باگ بعداً در منطق خلاصه‌سازی، اشتباه شمارش کند

نگاشت (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.

کاری که پیش‌از-پرواز برای شما انجام نمی‌دهد

موتور تشخیص می‌دهد؛ تعمیر نمی‌کند. یک یافته در مورد یک فونت جاسازی نشده یا یک فضای رنگی وابسته-به-دستگاه، یک سفارش کار برای هر کسی است که فایل‌ها را تولید می‌کند، و اعتبارسنج راهی برای پچ کردنِ در-جای آن ندارد. بنابراین حلقه بازخورد را به طور آگاهانه برنامه‌ریزی کنید. گزارش‌ها باید در جایی قرار گیرند که تیم تولیدکننده واقعاً آنها را می‌خواند، یا اینکه همان یافته‌ها هر شب دوباره ظاهر می‌شوند تا زمانی که در نهایت کسی بپرسد چرا نرخ انطباق (conformance rate) هرگز بهبود نمی‌یابد. همچنین ارزش دارد که نمونه‌ای از احکام را با یک اعتبارسنجِ مستقل تطبیق دهید، veraPDF برای PDF/A یا پیش‌از-پرواز Acrobat برای PDF/X، قبل از اینکه یک حسابرس خارجی آنها را برای شما تطبیق دهد. هنگامی که دو موتور روی فایل واقعی مشتری با یکدیگر اختلاف نظر دارند، این سند یک دردسر نیست؛ بلکه دقیقاً همان موردِ پس‌رفتی است که تست انتشارِ شما آن را از دست داده بود. آن را نگه دارید، نامگذاری کنید، و روی هر بیلد اجرا کنید

یک جفت‌سازی دیگر نیز ارزش دانستن دارد. همین موتورِ اعتبارسنجی، بررسی‌های تعاملی را در یک رابط کاربریِ بازبینی (review UI) به پیش می‌راند، بنابراین این CLIِ بدون-سر (headless) و یک میزکار بررسی و پذیرش PDFِ رو-به-تحلیلگر، می‌توانند واژگان اعتبارسنجی واحدی را به اشتراک بگذارند به جای اینکه به مرور زمان از هم دور شوند. و به دلیل اینکه [ppsPdfA, ppsPdfUa] دسترسی‌پذیری را در یک گذر واحد ارزیابی می‌کند، سمت PDF/UA دسته (batch) به شکلی تمیز با کارهای سمت-نمایشگر مانند ساخت یک خواننده PDF دسترسی‌پذیر در دلفی هم‌راستا می‌شود. پروفایل‌ها، فرمت‌های گزارش، و API کامل پیش‌از-پرواز در صفحه محصول کامپوننت PDFium مستند شده‌اند