บทความเทคนิค

รายงาน PDF Preflight แบบ Batch ใน Delphi ด้วย PDFium Component CLI

เครื่องมือ preflight แบบ batch คือโปรแกรม console ที่ไม่มีหน้าต่าง ชี้ไปยังโฟลเดอร์ของ PDF ตรวจสอบแต่ละไฟล์ตามมาตรฐานความสอดคล้องที่ระบุ และทิ้งหลักฐานที่อ่านได้ด้วยเครื่องไว้ว่าพบอะไร ไม่มีใครนั่งดูมัน มันรันตอนตีสองภายใต้ cron หรือ Windows Task Scheduler หรือเป็น gate ใน CI pipeline และคนต่อไปที่สนใจผลลัพธ์คือ scheduler ที่อ่านรหัส exit หรือผู้ตรวจสอบที่เปิดรายงานหลายสัปดาห์ต่อมา สิ่งนั้นเปลี่ยนความหมายของ "ถูกต้อง" เอนจิน preflight ของ PDFium Component ซึ่งเป็น source-code PDF library สำหรับ Delphi, C++Builder และ Lazarus ทำให้การเรียก validation เกือบเป็นเรื่องง่าย งานที่กำหนดว่าเครื่องมือจะมีคุณค่าหรือไม่อยู่รอบ ๆ การเรียกเหล่านั้น: คุณตรวจสอบ profile ใด รหัส exit บอกอะไร scheduler และรายงานที่ควรจับข้อผิดพลาดยังมีอยู่เมื่อใครไปหาหรือเปล่า

สัญญา: สิ่งที่ scheduler มองเห็นได้จริง

CI runner หรือ Windows Task Scheduler เห็นแค่สองสิ่งจากเครื่องมือของคุณ: รหัส exit และไฟล์ที่มันทิ้งไว้ log lines สีใน console และ progress output ทั้งหมดนั้นสำหรับมนุษย์ที่ดูสดและตอนตีสองไม่มีใคร ดังนั้นกำหนด vocabulary รหัส exit ก่อนแตะ API และทำให้น่าเบื่อ:

  • 0: ทุกไฟล์สอดคล้องกับทุก profile ที่ขอ
  • 1: อย่างน้อยหนึ่งไฟล์ผลิต validation findings
  • 2: เครื่องมือเองล้มเหลวกับอย่างน้อยหนึ่งไฟล์ (input เสียหาย, ล็อค, crash)

ความแตกต่างระหว่างรหัส 1 และ 2 คือสิ่งที่ทีมข้ามและเสียใจในภายหลัง PDF เสียหายที่ไม่สามารถเปิดได้ไม่ใช่ validation failure รวมเข้ากับรหัส 1 และ PDF ที่สแกนเสียหายกองใหญ่จะปรากฏในแดชบอร์ดของคุณเป็นการล่มสลายของ conformance กะทันหัน ทำให้ใครบางคนไล่ตาม standards regression ที่ไม่เคยเกิดขึ้น เมื่อเรื่องจริงคือเครื่องสแกนที่เสียขึ้นไป

อีกสองรายการควรอยู่ในสัญญา แรกคือ timeout ต่อไฟล์ PDF ที่มีปัญหา หลายพันหน้าที่มีโครงสร้าง object ซ้อนลึก อาจถือการ validate ผ่านเพียงครั้งเดียวได้นานเป็นนาที และหน้าต่างกลางคืนไม่มีความอดทน หยุด job ของไฟล์นั้นที่กำหนดเวลา นับเป็นความล้มเหลวของเครื่องมือ และให้ batch เดินต่อ ประการที่สองคือ directory กักกัน: ย้ายทุก input ที่ timeout หรือเปิดไม่ได้ออกไปแทนที่จะทิ้งไว้ ในช่วงสองสามเดือน directory นั้นจะเงียบ ๆ สะสมเอกสารที่แย่ที่สุดที่ลูกค้าจริงส่งมา และ corpus นั้นมีค่ากับ release testing มากกว่า sample สังเคราะห์ใด ๆ ที่คุณจะเขียนด้วยมือ

การเลือกมาตรฐาน และเหตุใด conformance level จึงสำคัญ

enumeration TPdfPreflightStandard ครอบคลุมตระกูลที่พบในทางปฏิบัติ: ppsPdfA สำหรับ ISO 19005 archival conformance, ppsPdfUa สำหรับ ISO 14289 accessibility, ppsPdfX สำหรับ print exchange รวมถึง ppsPdfE, ppsPdfR และ ppsPdfVT สำหรับงาน engineering, raster และ variable-data ภายในตระกูล เอนจินจะอ่าน conformance level ที่เอกสารอ้างสิทธิ์และรายงานต่อมาตรฐานใน ConformanceName ของผลลัพธ์ การตั้งชื่อตระกูลแทบไม่เพียงพอ เพราะ level คือจุดที่ความแตกต่างที่แท้จริงอยู่ PDF/A-2b รับประกันความสามารถในการสร้างซ้ำด้านภาพและไม่มีอะไรเพิ่มเติม PDF/A-3a เพิ่มข้อกำหนดสำหรับ structure tagging แบบ logical และอนุญาตให้ฝัง source file ซึ่งเป็นแถบที่ยากกว่ามากสำหรับเอกสารที่สแกนแล้วที่ไม่มี tag tree เลย ทำผิดในทิศทางใดก็ตามและ batch จะโกหกคุณ หากนโยบายการเก็บรักษาของคุณต้องการ PDF/A-2b จริง ๆ แต่คุณ fail ไฟล์เพราะขาด structure tag รายงานจะเต็มไปด้วย findings ที่ไม่มีใครจะแก้ไขเลย ยอมรับ PDF/A label ใด ๆ โดยไม่ตรวจ level และคุณจะอนุมัติเอกสารที่ตรงตามแถบที่อ่อนแอกว่าที่คุณสัญญา ข้อกำหนด accessibility จากผู้ซื้อภาครัฐมักซ้อน PDF/UA ไว้บนสิ่งนี้ซึ่งไม่เพิ่มต้นทุนการรัน เพราะ BuildPdfPreflightReport (จาก unit FPdfPreflightReport) รับเซตของมาตรฐาน:

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

การเรียกครั้งเดียวประเมินทั้งสองมาตรฐานและส่งคืน record รายงานเดียวที่รวบรวม

เหตุใด findings ว่างเปล่าจึงไม่ใช่การผ่าน

รายงานระบุ findings ต่อมาตรฐาน และ issue list ว่างเปล่าหมายความว่า "ไม่พบปัญหาในมาตรฐานที่รันจริง ๆ เท่านั้น" นั่นเป็นการอ้างสิทธิ์ที่แคบกว่า "ไฟล์สอดคล้องกับมาตรฐานที่คุณสนใจ" และช่องว่างระหว่างทั้งสองคือที่ที่ batch preflight เน่าเงียบ ๆ typo ในการตั้งค่าที่ทิ้ง ppsPdfA ออกจากเซตจะผลิต issue list ว่างเปล่าเหมือนกันกับไฟล์ที่สะอาดจริง ๆ ดังนั้น treat ความเงียบว่าน่าสงสัย เดิน Report.Results และยืนยันสองสิ่งสำหรับทุกมาตรฐานที่คุณตั้งใจตรวจ: ว่า result entry สำหรับมันมีอยู่จริง และว่า flag IsCompliant ของมัน ที่รองรับโดย Status = pfsPass เป็น true งาน nightly ที่เทียบ "no findings" กับ "archive ready" โดยไม่ยืนยันว่ามาตรฐานใดถูกประเมินคือวิธีคลาสสิกที่โฟลเดอร์ไฟล์ที่ไม่สอดคล้องผ่านไปเป็นเดือน จนกว่าผู้ตรวจสอบภายนอกจะเปิดด้วย veraPDF และ archive ทั้งหมดตกอยู่ในคำถาม

กับดักที่สองซ่อนอยู่ใน finding คืออะไร TPdfPreflightIssue แต่ละตัวมี Code, Category, Description และ Recommendation และตั้งชื่อกฎที่ถูกละเมิด ไม่ใช่หน้าหรือ object นั่นคือการออกแบบที่มีผลต่อ feedback loop รายงานบอกทีมที่ผลิต คลาสของข้อบกพร่อง ที่มีอยู่ ไม่ว่าจะเป็น font ที่ไม่ embed หรือ XMP identifier ที่หายไป และการค้นหา object ที่ผิดเฉพาะนั้นเป็นงานของเครื่องมือ remediation ปลายน้ำ ไม่ใช่ validator สร้าง report consumer ของคุณตาม Code ที่เสถียร ไม่ใช่ description text ที่มนุษย์อ่านได้ ซึ่งสามารถ reword ระหว่าง release โดยไม่มีคำเตือน

ไฟล์รายงานสำหรับเครื่องและสำหรับคนที่ on call

record รายงานเขียน findings เดียวกันในห้ารูปแบบ: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile และ SaveMarkdownToFile แต่ละอันมีฟังก์ชัน ToJson style เมื่อต้องการ string ในหน่วยความจำแทนที่จะเป็นบนดิสก์ ต้านทานการล่อลวงให้เลือกอันเดียว เขียน JSON สำหรับ pipeline เพื่อให้ CI แนบกับ job record และ parse issue code และ per-standard status โดยไม่ต้อง scrape text เขียน HTML สำหรับมนุษย์ที่ถูก page เพราะมันเปิดในเบราว์เซอร์ใด ๆ โดยไม่ต้องมี tooling ทั้งสองอย่างมีต้นทุนอีกหนึ่งบรรทัดต่อไฟล์และช่วยให้ engineer on call ไม่ต้องทำงานที่แย่ที่สุดใน batch processing ซึ่งคือการ reverse-engineer JSON blob ดิบตอนตีสองเพื่อเรียนรู้ว่าไฟล์ใดพัง วินัยหนึ่งสำคัญกว่าการเลือกรูปแบบ: ให้ชื่อรายงานแต่ละอันมาจากชื่อ input file ไม่ใช่ timestamp หรือสองรันที่ขนานกันจะพันรายงานที่คุณไม่สามารถจับคู่กลับไปยัง input ได้

Severity threshold เป็นของ configuration ไม่ใช่ code annotation ที่ไม่มี alternate description เป็น hard failure สำหรับ PDF/UA submission portal และเป็น note ที่ไม่สำคัญสำหรับ internal archive แต่มันเป็น finding เดียวกันในทั้งสองกรณี เปิดเผย fail-on level ต่อ profile เพื่อให้ policy เปลี่ยนได้โดยไม่ต้อง recompile และประทับ level ที่มีผลลงใน job summary ด้วย ไตรมาสหน้าไม่มีใครจำได้ว่า batch ของเดือนตุลาคมปีที่แล้วรันใน threshold ใด และ summary เป็นที่เดียวที่ memory นั้นรอดชีวิต

แยกไฟล์เพื่อไม่ให้ 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 ใหม่ต่อไฟล์รับประกันว่าเอกสารที่เสียหาย engine state ไม่สามารถทำพิษไฟล์ที่ตามมา การตรวจสอบ Active ที่ชัดเจนได้ตำแหน่งของมัน เพราะ Active := True กลืน load error แทนที่จะ raise มัน ทิ้ง guard ออกและไฟล์ที่ตัดส่วนจะลอยต่อไปยัง validation call ก่อนที่จะล้มเหลวที่ใดสักแห่งปลายน้ำพร้อมข้อความที่ทำให้เข้าใจผิด try..except ด้านในอยู่ภายใน scope ต่อไฟล์โดยเจตนา ดังนั้น exception เดียว bump ตัวนับความล้มเหลวและลูปดำเนินต่อ คุณต้องการรายงานที่สะอาดสำหรับ 4,999 ไฟล์ที่ดีแม้ว่าไฟล์ 5,000 จะถูกฉีก และทั้งสองรูปแบบรายงานถูกเขียนลงดิสก์ก่อนที่จะนับ verdict ซึ่งหมายความว่าหลักฐานยังคงอยู่แม้ว่า bug ในภายหลังใน summary logic จะนับผิด

การ mapping รหัส exit จึงยุบเหลือไม่กี่บรรทัดใน project file:

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.

สิ่งที่ preflight จะไม่ทำให้คุณ

เอนจินตรวจจับ มันไม่ซ่อมแซม finding เกี่ยวกับ font ที่ไม่ embed หรือ color space ที่ขึ้นกับอุปกรณ์คือคำสั่งทำงานสำหรับผู้ที่ผลิตไฟล์ และ validator ไม่มีทางปะมันแทน ดังนั้นวางแผน feedback loop อย่างรอบคอบ รายงานต้องไปลงที่ที่ทีมที่ผลิตอ่านจริง หรือ findings เดียวกันจะปรากฏทุกคืนจนกว่าใครบางคนจะถามว่าทำไม conformance rate ไม่เคยดีขึ้น นอกจากนี้ยังคุ้มค่าที่จะ cross-check sample ของ verdicts กับ validator อิสระ ไม่ว่าจะเป็น veraPDF สำหรับ PDF/A หรือ preflight ของ Acrobat สำหรับ PDF/X ก่อนที่ผู้ตรวจสอบภายนอกจะ cross-check ให้คุณ เมื่อสองเอนจินไม่เห็นด้วยในไฟล์ลูกค้าจริง เอกสารนั้นไม่ใช่ความรำคาญ มันคือ regression case ที่ release testing ของคุณขาดหายไปพอดี เก็บมันไว้ ตั้งชื่อ และรันในทุก build

การจับคู่อีกอย่างที่ควรรู้คือ เอนจิน validation เดียวกันขับ interactive check ใน review UI ดังนั้น headless CLI นี้และ PDF intake review workbench สำหรับ analyst สามารถแชร์ vocabulary validation เดียวแทนที่จะแยกออกจากกันเมื่อเวลาผ่านไป และเนื่องจาก [ppsPdfA, ppsPdfUa] ประเมิน accessibility ในรอบเดียวกัน ด้าน PDF/UA ของ batch ตรงกันดีกับงานฝั่ง viewer อย่าง การสร้าง accessible PDF reader ใน Delphi Profiles, รูปแบบรายงาน และ preflight API เต็มรูปแบบอยู่ในหน้าสินค้าของ PDFium Component