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

การตรวจสอบ Preflight ของ PDF แบบอัตโนมัติใน Delphi ด้วย HotPDF

ไฟล์เปิดได้ราบรื่นในเครื่องของคุณ Acrobat แสดงผลได้ พรีวิวการพิมพ์ก็ดูถูกต้อง ทุกหน้าอยู่ครบ แล้วมันก็ถูกส่งไปที่โรงพิมพ์ หรือเข้าไปในระบบ archive ที่รับ batch รายเดือนของคุณ แล้วก็ถูกตีกลับมา: รูปภาพ RGB ในงานที่ต้องเป็น CMYK ไม่มีคีย์ /Trapped output intent ที่ไม่ตรงกับแท่นพิมพ์ ไม่มีอะไรผิดปกติกับเอกสารที่ใครก็ตามจะมองเห็นได้ด้วยตา มันผิดเมื่อเทียบกับโปรไฟล์ต่างหาก และโปรไฟล์นั้นถูกตรวจสอบที่ไหนสักแห่งที่คุณไม่ได้อยู่ preflight คือชื่อในวงการ prepress สำหรับการตรวจสอบนั้น และคำถามที่แท้จริงคือมันควรอยู่ตรงไหนเมื่อ PDF มาจากโค้ด Delphi ของคุณเอง ไม่ใช่จากเดสก์ท็อปของนักออกแบบ

HotPDF ไม่ได้มอบฟังก์ชัน preflight ให้คุณเรียกใช้ ตัว component มีหน้าต่างรายงาน preflight อยู่ใน GUI demo ของมัน แต่ไม่มี API อยู่เบื้องหลังที่บริการหรือ build script จะเรียกใช้ได้ และการทำเหมือนว่ามีจะทำให้คุณไปตามหาเมธอดที่ไม่มีอยู่จริง ฟังดูเหมือนเป็นช่องโหว่ จนกว่าคุณจะสังเกตว่าสำหรับไฟล์ที่คุณสร้างขึ้นเอง การเรียก validator กับ output ของตัวเองก็ผิดรูปแบบอยู่แล้ว คุณควบคุม property ทุกตัวที่ validator จะตรวจสอบอยู่แล้วตั้งแต่ต้น การแบ่งงานที่มีประโยชน์คือทำให้ตัวสร้างไฟล์ไม่มีทางสร้างไฟล์ที่ไม่ผ่านมาตรฐานได้เลยตั้งแต่ต้น แล้วพิสูจน์มันด้วยเครื่องมือที่คุณไม่ได้เขียนเอง

แผนภาพไปป์ไลน์ preflight ของ Delphi ที่การตั้งค่าความสอดคล้องของ HotPDF ป้องกัน PDF เสียตั้งแต่ตอนสร้าง และ veraPDF กับ Acrobat Preflight พิสูจน์ผลลัพธ์จากภายนอก
การป้องกันอบกฎ PDF/A และ PDF/X เข้าไว้ในการสร้างของ HotPDF เอง ขณะที่ตัวตรวจภายนอกมอบคำตัดสินซึ่งตัวสร้างให้คะแนนตัวเองไม่ได้

ทำไมการตรวจสอบ output ของตัวเองจึงต้องทำต่างออกไป

preflight แบบดั้งเดิมตั้งสมมติฐานว่าเป็นไฟล์ของคนแปลกหน้า นักออกแบบบางคน แอปพลิเคชันอื่นบางตัว สายการแก้ไขที่ไม่รู้จักบางอย่างเป็นผู้สร้างมันขึ้นมา และคุณตรวจสอบมันเพราะไม่รู้เลยว่าข้างในมีอะไรอยู่ เอกสารที่โค้ดของคุณสร้างขึ้นไม่ใช่คนแปลกหน้า การฝังฟอนต์ color space, output intent, บล็อก metadata: โปรแกรมของคุณตัดสินใจเรื่องเหล่านี้ทั้งหมดไปเพียงไม่กี่มิลลิวินาทีก่อนที่ไฟล์จะถูกเขียนลงดิสก์ การตรวจสอบทีหลังเพื่อค้นพบทางเลือกที่คุณเพิ่งตัดสินใจไปเองเป็นงานเสียเวลาเปล่า วิธีที่ประหยัดกว่าคือการจำกัดทางเลือกเหล่านั้นไว้ตั้งแต่ต้น เพื่อให้ไฟล์ที่ไม่สอดคล้องตามมาตรฐานไม่มีวันมีอยู่ให้จับได้เลย

ยังมีเหตุผลด้านความน่าเชื่อถือที่ทำให้ต้องเก็บการตรวจสอบไว้ภายนอกด้วย ไลบรารีที่รับรอง output ของตัวเองก็เหมือนกับการตรวจข้อสอบของตัวเอง เมื่อระบบ archive ของลูกค้าหรือ RIP ของโรงพิมพ์ปฏิเสธไฟล์ของคุณ คำว่า "component ของเราบอกว่ามันโอเค" ไม่มีน้ำหนักอะไรเลย แต่คำตัดสินจาก veraPDF หรือ Acrobat มีน้ำหนัก เพราะฝั่งตรงข้ามใช้เครื่องมือเดียวกัน

ทำให้ compliance เป็นการตั้งค่า ไม่ใช่ checklist

ชั้นการป้องกันเป็นเพียงแค่การตั้งค่าเท่านั้น ตั้งค่า PDFACompliance หรือ PDFXCompliance ก่อน BeginDoc แล้ว HotPDF จะรักษากฎที่สอดคล้องไว้ตลอดกระบวนการสร้างทั้งหมด: มันจะฝังฟอนต์ เฝ้าดูการใช้งาน DeviceRGB และ DeviceCMYK เทียบกับ output intent ที่คุณประกาศไว้ และปฏิเสธฟีเจอร์ที่โปรไฟล์ห้ามใช้ ความขัดแย้งจะปรากฏขึ้นที่ EndDoc ซึ่งด่าน compliance จะ raise แทนที่จะเงียบ ๆ ส่งมอบสิ่งที่จะล้มเหลวที่ปลายทางออกไป เมื่อไฟล์ถูกบันทึกแล้ว property ตัวเดียวกันนี้จะอ่านค่ากลับมาว่าอะไรบ้างที่ถูกบังคับใช้จริง ซึ่งเป็นข้อเท็จจริงเพียงหนึ่งเดียวที่ pipeline log ของคุณต้องการมากที่สุด:

// หลัง EndDoc: บันทึกโปรไฟล์ที่บังคับใช้ไว้พร้อมกับ metadata ของการรัน
if Pdf.PDFACompliance <> '' then
  Log('Generated as PDF/A level ' + Pdf.PDFACompliance);
if Pdf.PDFXCompliance <> '' then
  Log('Generated as PDF/X profile ' + Pdf.PDFXCompliance);

ให้ใส่ flag เหล่านั้นไว้ในบรรทัด log เดียวกับ hash ของข้อมูลอินพุตและเวอร์ชันของ HotPDF วันที่ validator กับตัวสร้างไฟล์ของคุณเห็นไม่ตรงกันเกี่ยวกับไฟล์หนึ่ง บรรทัดนั้นจะบอกคุณว่า template ไหนสร้างมันขึ้นมาและ build ไหนของไลบรารีที่ถูกโหลดไว้ และการถกเถียงที่ปกติแล้วจะกินเวลาไปทั้งบ่ายก็จะกลายเป็นแค่การ grep เพียงครั้งเดียว output intent, ICC profile และการทำ tagging ที่อยู่เบื้องหลัง flag เหล่านี้อธิบายไว้อย่างละเอียดในคู่มือเรื่อง output แบบ PDF/A, PDF/X และ PDF/UA ด้วย HotPDF

ด่านแรกราคาถูกสำหรับไฟล์ที่คุณไม่ได้สร้างเอง

ไม่ใช่ทุก pipeline ที่เป็นแบบสร้างขึ้นเองล้วน ๆ ลูกค้าอัปโหลด PDF สแกนเนอร์วางไฟล์ไว้ในโฟลเดอร์ พาร์ตเนอร์แนบมันมากับอีเมล การส่งไฟล์เหล่านั้นทุกไฟล์ผ่าน validator เชิงโครงสร้างแบบเต็มรูปแบบจะสิ้นเปลืองเวลาในคิวไปกับไฟล์ที่เปิดไม่ได้ด้วยซ้ำ Direct File API ของ HotPDF อ่านโครงสร้างของไฟล์มากพอที่จะตอบคำถามว่า "นี่คือ PDF ที่ใช้งานได้หรือไม่" โดยไม่ต้องโหลด object tree ทั้งหมด ซึ่งทำให้มันเป็นจุดที่ดีสำหรับการล้มเหลวอย่างรวดเร็ว:

function TriagePdf(Pdf: THotPDF; const FileName: string): Boolean;
var
  Handle, Pages: Integer;
begin
  Result := False;
  Handle := Pdf.DAOpenFileReadOnly(FileName, '');
  if Handle <= 0 then
    Exit;  // อ่านโครงสร้างไม่ได้: กักไว้ ไม่ต้อง validate
  try
    Pages := Pdf.DAGetPageCount(Handle);
    Result := Pages > 0;
  finally
    Pdf.DACloseFile(Handle);
  end;
end;

มีข้อเท็จจริงสองอย่างเกี่ยวกับ API นี้ที่กำหนดว่าคุณควรห่อหุ้มมันอย่างไร ทางลัดหน่วยความจำคงที่ใช้ได้เฉพาะกับอินพุตที่ไม่เข้ารหัสเท่านั้น ถ้าส่งรหัสผ่านให้ DAOpenFileReadOnly มันจะเงียบ ๆ ตกไปใช้ full parse ดังนั้นไฟล์ที่คุณรู้อยู่แล้วว่าเข้ารหัสไว้ควรผ่าน DecryptFile ให้เป็นสำเนาที่ทำงานแบบไม่เข้ารหัสก่อนที่จะคัดกรอง และ DAGetPageCount ไม่มีความหมายอะไรเลยบน handle ที่เปิดไม่สำเร็จ ดังนั้นการตรวจสอบ handle ยังคงต้องเข้มงวด และผลลัพธ์ที่ไม่เป็นบวกคือการปฏิเสธ ไม่ใช่การลองใหม่ รูปแบบเพิ่มเติมแบบนี้อยู่ในบทความ Direct File API สำหรับ workflow ของ PDF ขนาดใหญ่

veraPDF รันเป็นส่วนหนึ่งของ build

สำหรับอะไรก็ตามที่คุณอ้างว่าเป็น PDF/A หรือ PDF/UA veraPDF คือ validator ที่ควรเชื่อมต่อเข้าไป มันรันแบบ headless รับ batch ได้ ส่ง XML หรือ JSON ออกมา และระบุชื่อความล้มเหลวแต่ละอันตามข้อกำหนด ISO ดังนั้นกฎที่ล้มเหลวเทียบกับ ISO 19005-1 clause 6.2.2 จะชี้กลับไปยังการตั้งค่าตัวสร้างไฟล์โดยตรง แทนที่จะปล่อยให้คุณต้องเดาเอาเอง การขับเคลื่อนมันจาก Delphi ก็เป็นแค่การควบคุม process ธรรมดา:

function RunVeraPdf(const PdfFile, ReportFile: string): Cardinal;
var
  Cmd: string;
  SI: TStartupInfo;
  PI: TProcessInformation;
begin
  Cmd := Format('cmd /c verapdf.bat --format xml "%s" > "%s"',
    [PdfFile, ReportFile]);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(SI);
  if not CreateProcess(nil, PChar(Cmd), nil, nil, False,
      CREATE_NO_WINDOW, nil, nil, SI, PI) then
    RaiseLastOSError;
  try
    WaitForSingleObject(PI.hProcess, 120000);  // จำกัดเวลารอต่อไฟล์
    GetExitCodeProcess(PI.hProcess, Result);
  finally
    CloseHandle(PI.hThread);
    CloseHandle(PI.hProcess);
  end;
end;

timeout นี้คุ้มค่ากับสิ่งที่มันทำได้ ไฟล์ที่ผิดรูปแบบสามารถขับ parser ตัวไหนก็ได้ให้ไปติดอยู่ในมุมที่ไม่มีวันออกมาได้ และการรอแบบไม่มีกำหนดสิ้นสุดใน queue worker จะลาก queue ที่เหลือให้ช้าลงไปด้วย จำกัดเวลารอไว้ ให้ timeout มีรหัสความล้มเหลวเป็นของตัวเอง แล้วแยกไฟล์นั้นไว้ให้มนุษย์ตรวจสอบ เมื่ออ่านผลลัพธ์ ให้ parse XML หาตัวระบุกฎ ไม่ใช่หาข้อความที่มนุษย์อ่านเข้าใจ ตัวระบุกฎอยู่รอดผ่านการอัปเกรด validator ได้ ในขณะที่ถ้อยคำของข้อความไม่รอด และรหัสที่คงที่คือสิ่งที่วิศวกรซัพพอร์ตค้นหาเทียบกับ ticket เก่า ๆ ได้

วิธีที่คุณรัน batch สำคัญพอ ๆ กับว่าแต่ละไฟล์ผ่านหรือไม่ ให้ใช้หนึ่ง process ต่อหนึ่งไฟล์ ไม่ใช่หนึ่ง process ต่อหนึ่ง batch เพื่อให้อินพุตที่เป็นพิษทำให้คุณเสียแค่ timeout ของไฟล์นั้นไฟล์เดียว จำกัดจำนวน process ของ validator ไว้ที่จำนวนแกน CPU เพราะการสร้างรายงาน XML เป็นงานที่ผูกกับ CPU และการใช้เกินจะทำให้เครื่องกระตุกไปหมด และตั้งเพดานขนาดไว้ที่จุดรับเข้า เพราะหนังสือที่สแกนขนาดสองกิกะไบต์จะครองคิวไว้ทั้งหมด ไม่ว่า parser จะอดทนแค่ไหนก็ตาม ไม่มีสิ่งใดในนี้ที่เป็น preflight ในความหมายที่เคร่งครัด มันคือความแตกต่างระหว่างด่านที่รอดผ่านปริมาณงานช่วงสิ้นเดือนได้ กับด่านที่ถูกปิดไปในคืนแรกที่มันทำให้ pipeline ค้างตอนตีสอง

แผนภาพด่าน batch ของ Delphi ที่รันโปรเซส veraPDF หนึ่งโปรเซสต่อ PDF ภายใต้ timeout ที่จำกัด, เก็บเกี่ยว rule ID จาก XML แทนข้อความ และเก็บถาวรรายงานแต่ละฉบับเคียงข้างไฟล์ของมัน
ตัวเฝ้ารับของจำกัดภาระคิว ขณะที่ worker veraPDF สดหนึ่งตัวต่อไฟล์กันอินพุตที่ปนพิษมาทำให้บิลด์ค้าง

PDF/X คือจุดที่วิธีนี้ทำได้ไม่ดีพอ veraPDF ไม่ validate มัน ดังนั้นการตรวจสอบที่ใช้งานได้จริงยังคงเป็น Preflight ของ Acrobat ด้วยโปรไฟล์ ISO 15930 ที่โรงพิมพ์ของคุณระบุไว้ Acrobat ต้องการมนุษย์ ซึ่งหมายถึงการสุ่มตรวจแทนการตรวจครอบคลุมทั้งหมด: ไฟล์แรกจาก template ใหม่ บวกกับการสุ่มเล็ก ๆ จากแต่ละ batch ในขณะที่ด่าน automation จัดการทุกอย่างที่จัดการได้โดยไม่ต้องพึ่งมนุษย์ การตรวจแบบสุ่มที่รันได้จริงดีกว่า automation ที่สมบูรณ์แบบแต่ค้างอยู่ครึ่งทางตลอดไป

รายงานที่คุณจะยังต้องการอีกหนึ่งปีให้หลัง

ด่าน preflight คุ้มค่าสองครั้ง ครั้งแรกตอนที่มันหยุดไฟล์ที่ไม่ดีไว้ที่หน้าประตู และอีกครั้งในภายหลังนานมากเมื่อมีใครสักคนถามว่าทำไมไฟล์ตัวหนึ่งถึงผ่านเข้ามาได้ ช่วงเวลาที่สองนี้ต่างหากที่ควรเป็นตัวกำหนดรูปแบบของรายงาน เพราะเป็นช่วงที่รายงานแบบบาง ๆ จะทิ้งให้คุณติดอยู่กลางทาง สำหรับทุกไฟล์ที่ตรวจสอบ ให้เก็บ hash ของอินพุต flag ด้าน compliance ของตัวสร้างไฟล์และเวอร์ชันไลบรารีจากบรรทัด log ข้างต้น ชื่อและเวอร์ชันของ validator โปรไฟล์ที่ใช้ตรวจสอบ ผลผ่านหรือไม่ผ่าน และ rule ID ที่ล้มเหลวพร้อมหมายเลขหน้าทุกครั้งที่ validator ให้มา เก็บรายงานนั้นไว้ข้าง ๆ ไฟล์ที่มันอธิบาย ถ้าเก็บไว้ในระบบแยกต่างหาก ระบบนั้นจะถูกปลดระวางไปก่อนที่ archive ที่มันบันทึกไว้จะยังอยู่เสียอีก

ข้อยกเว้นก็ต้องถูกจดบันทึกไว้ด้วยเช่นกัน เมื่อลูกค้ายืนกรานจะส่งไฟล์ที่ด่านไม่ชอบ คำตอบไม่ใช่การผ่อนคลายกฎให้กับทุกคน ให้บันทึกว่าใครเป็นผู้อนุมัติไฟล์นี้ ด้วยเหตุผลอะไร และมีผลถึงวันที่เท่าไร แล้วแนบข้อยกเว้นนั้นเข้ากับรายงานของมัน ข้อยกเว้นที่มีชื่อและวันหมดอายุคือการตัดสินใจที่มีเจ้าของ ส่วนการตรวจสอบที่ถูก comment ทิ้งไว้แบบ "ชั่วคราว" คือเหตุการณ์ที่รอวันเกิดขึ้นเท่านั้นเอง

มีอีกหนึ่งนิสัยที่คุ้มค่าในตัวมันเอง: เมื่อไฟล์ล้มเหลว ให้คัดลอกมันไปไว้ในโฟลเดอร์ regression ที่ตั้งชื่อไว้ก่อนที่ใครจะไปแตะต้องมัน ปัญหา preflight เกือบทุกอย่างที่คุ้มค่าจะดีบั๊กสามารถย้อนกลับไปหาอินพุตตัวใดตัวหนึ่งได้เสมอ และทีมที่เก็บอินพุตเหล่านั้นไว้จะแก้ปัญหาที่เกิดซ้ำได้ภายในหนึ่งชั่วโมง แทนที่จะรอให้มันโผล่ขึ้นมาอีกครั้งในการใช้งานจริง property ด้าน compliance และ Direct File API ที่แสดงในบทความนี้เป็นส่วนหนึ่งของHotPDF Delphi Componentสำหรับ Delphi และ C++Builder ซึ่งเอกสารประกอบครอบคลุมทุกฟังก์ชันเรียกอย่างครบถ้วน