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

การตรวจสอบ Preflight ของ PDF/A และ PDF/UA ใน Delphi ด้วย PDF Library for Delphi

PDF/A และ PDF/UA ตอบคำถามสองข้อที่ไม่เกี่ยวข้องกันเลย และการมองทั้งคู่เป็นช่องติ๊กเดียวว่าด้วยการเข้าถึงและการจัดเก็บถาวร คือวิธีที่ทำให้ไฟล์เสีย ๆ เดินเข้าคลังเอกสารพร้อมป้ายรับรองความสอดคล้อง PDF/A ถามว่าไฟล์นี้จะยังแสดงผลได้ตรงตามต้นฉบับในอีกยี่สิบปีข้างหน้าหรือไม่ ส่วน PDF/UA ถามว่าเทคโนโลยีสิ่งอำนวยความสะดวกอ่านมันได้ในวันนี้หรือไม่ เอกสารหนึ่งอาจผ่านข้อหนึ่งอย่างสบายและตกอีกข้อหนึ่ง คำตัดสินที่ซื่อตรงจึงมาจากการรันทั้งสองอย่าง และจากการรันก่อนที่ไฟล์จะถูกเขียนออกไป ไม่ใช่หลังจากที่ระบบปลายทางไปเชื่อตัวระบุความสอดคล้องที่อบไว้ในเมตาดาตาของไฟล์แล้ว ตัวระบุนั้นเป็นเพียงคำประกาศของตัวเอง ไม่มีอะไรในรูปแบบไฟล์บังคับให้มันเป็นจริง และแอปพลิเคชันที่เขียน "PDF/A-1b" ลงใน XMP โดยไม่ตรวจสอบกับมาตรฐาน ย่อมผลิตไฟล์ที่ดูเหมือนสอดคล้องในสายตาของผู้บริโภคทุกรายที่อ่านแค่ป้าย losLab PDF Library (PDF Library for Delphi) ปิดช่องว่างนี้ให้ Delphi และ C++Builder ด้วยการสร้างตัวตรวจสอบทั้งสองไว้ในไลบรารี การตรวจจึงรันในโปรเซสเดียวกันโดยไม่ต้องตั้งบริการภายนอกใด ๆ ขึ้นมา

มาตรฐานสองฉบับที่ตัดไฟล์ตกด้วยเหตุผลตรงข้ามกัน

ISO 19005 (PDF/A) เป็นสัญญาว่าด้วยการผลิตซ้ำ ไฟล์ที่สอดคล้องต้องแสดงผลเหมือนเดิมทุกประการในอีกหลายทศวรรษข้างหน้า บนซอฟต์แวร์ที่ไม่เคยเห็นระบบซึ่งสร้างมันขึ้นมา กฎจึงพุ่งเป้าไปที่การพึ่งพาภายนอก ได้แก่ฟอนต์ต้องฝังทุกตัว สีต้องยึดกับ ICC OutputIntent ที่ฝังไว้หรือแสดงในปริภูมิสีที่ไม่ขึ้นกับอุปกรณ์ ห้ามเข้ารหัสใน PDF/A-1 ห้ามมี JavaScript และเมตาดาตา XMP ต้องสอดคล้องกับ document information dictionary ส่วน ISO 14289 (PDF/UA) เป็นสัญญาว่าด้วยความหมายแทน เทคโนโลยีสิ่งอำนวยความสะดวกต้องเดินสำรวจเอกสารแล้วได้ความหมายกลับออกมา ซึ่งอยู่ในเลเยอร์คนละชั้นโดยสิ้นเชิง ได้แก่ structure tree ที่สมบูรณ์ ข้อความทางเลือกบนรูปภาพ ชื่อเอกสารที่ตั้งไว้สำหรับแสดงผล ระดับหัวข้อที่ไม่ข้ามขั้น และความสัมพันธ์ของหัวตารางที่ยังคงอยู่แม้หน้านั้นจะเลื่อนพ้นจอไปแล้ว

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

การเลือกว่าจะเล็ง PDF/A ภาคไหนสำคัญพอ ๆ กับการเล็งให้เข้าเป้า PDF/A-1 ถูกตรึงไว้ที่ PDF 1.4 และปฏิเสธทั้งความโปร่งใสและ JPEG2000 ซึ่งเอาต์พุตงานรายงานสมัยใหม่หยิบมาใช้โดยไม่ทันคิด ส่วน PDF/A-2 (ISO 19005-2 ที่สร้างบน ISO 32000-1) รับได้ทั้งสองอย่างและเป็นค่าเริ่มต้นที่สมเหตุสมผลสำหรับคลังเอกสารใหม่ ขณะที่ PDF/A-3 ไปไกลกว่านั้นด้วยการอนุญาตให้ฝังไฟล์ชนิดใดก็ได้ ซึ่งเป็นสิ่งที่รูปแบบใบแจ้งหนี้อิเล็กทรอนิกส์ที่มีการกำกับดูแลอาศัยอยู่ ทีมที่ยังยึด PDF/A-1b เป็นมาตรฐานในปี 2026 มักแบกข้อกำหนดที่ใครบางคนเขียนไว้เมื่อสิบห้าปีก่อน และการกลับไปเจรจาเรื่องภาคเป้าหมายใหม่มักถูกกว่าการไล่ถอดความโปร่งใสออกจากทุกแผนภูมิที่ระบบผลิตออกมา

แผนภาพ PDF Library for Delphi เปรียบเทียบสัญญาการทำซ้ำของ PDF/A กับสัญญาความหมายของ PDF/UA พร้อมเมทริกซ์ผ่าน-ตกของเอกสารที่ผ่านข้อหนึ่งแต่ตกอีกข้อ
PDF/A รับประกันการแสดงผลเชื่อถือได้ในอีกหลายทศวรรษข้างหน้า ขณะที่ PDF/UA รับประกันการอ่านด้วยเทคโนโลยีช่วยเหลือในวันนี้ คำตัดสินฝั่งหนึ่งไม่ได้นัยถึงอีกฝั่ง

ผลการตรวจแบบมีโครงสร้างตั้งแต่ตอนรับไฟล์เข้า

จุดเข้าใช้งานฝั่ง flat API คือ CheckFileCompliance โดยมีตัวเลือกการทดสอบเป็น 1 สำหรับ PDF/A และ 2 สำหรับ PDF/UA มันคืนแฮนเดิลของ string list กลับมา ซึ่งแต่ละรายการคือผลการตรวจหนึ่งข้อ บรรทัดละหนึ่งข้อ อันเป็นรูปทรงที่ด่านอัตโนมัติต้องการเดินไล่พอดี:

function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0);  // 1 = PDF/A
  if ListId = 0 then
  begin
    // 0 หมายถึง "ไม่พบปัญหา" หรือ "อ่านไฟล์ไม่ได้" -- ต้องแยกแยะก่อนปล่อยผ่าน
    Result := Pdf.LastErrorCode = 0;
    Exit;
  end;
  for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
    LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
  Pdf.ReleaseStringList(ListId);
  Result := False;
end;

มีรายละเอียดสองข้อที่ตัดสินว่าสิ่งนี้จะทำงานโดยไม่ต้องมีคนเฝ้าได้หรือไม่ ข้อแรกคือค่าคืนกลับที่มีความหมายตรงข้ามกันสองอย่าง CheckFileCompliance คืนค่า 0 เมื่อไฟล์สอดคล้องครบถ้วน และคืน 0 เช่นกันเมื่อเปิดไฟล์ไม่ได้เลย เพราะภายในนั้นรายการผลลัพธ์ที่ว่างเปล่าจะยุบเป็น 0 ทั้งสองกรณี ด่านที่อ่าน 0 ว่าผ่านจะโบกไฟล์อัปโหลดที่เสียหายให้เข้าคลังเอกสารไปตรง ๆ จึงต้องแยกแยะด้วย LastErrorCode ก่อนจะเชื่อค่าศูนย์นั้น อย่างที่ด่านด้านบนทำไว้ ข้อที่สองเกี่ยวกับว่าไฟล์อยู่ตรงไหนของวงจรชีวิตมัน ตัวตรวจทำงานบนตัวอ่านแบบสตรีมมิงของไลบรารีแทนที่จะใช้โมเดลเอกสารเต็มรูปแบบ โดยเปิดไฟล์โดยตรงแบบแบ่งใช้เพื่ออ่าน และไม่เคยเรียก LoadFromFile เลย นี่คือเหตุผลที่มันเคี้ยวอินพุตขนาดหลายกิกะไบต์ได้โดยไม่ต้องสร้าง object tree แต่การเปิดแบบสตรีมมิงเดียวกันนี้ก็ล้มเหลวขณะที่โปรเซสอื่นยังถือไฟล์ไว้เพื่อเขียน และการอัปโหลดที่กำลังดำเนินอยู่ก็คือสถานะนั้นพอดี จงตั้งด่านหลังจากการถ่ายโอนเสร็จสิ้นแล้ว

การออกแบบแบบสตรีมมิงให้ผลตอบแทนอีกครั้งเมื่อรับภาระหนัก การตรวจแต่ละครั้งเปิดอินพุตแบบอ่านอย่างเดียวและแบ่งใช้เพื่ออ่าน การตรวจสอบทั้งคลังเอกสารจึงขยายออกไปตามเธรดหรือโปรเซสของ worker ได้ โดยใช้อินสแตนซ์ TPDFlib หนึ่งตัวต่อหนึ่ง worker และไม่มีการแย่งชิงกัน ทรัพยากรที่ต้องมีวินัยคือตัวแฮนเดิลเอง ผลลัพธ์ที่ไม่ใช่ศูนย์ทุกครั้งจาก CheckFileCompliance จะถูกจองไว้จนกว่าคุณจะเรียก ReleaseStringList และด่านที่รันยาวนานแล้วลืมคืนทรัพยากรเหล่านั้นจะไม่พัง มันแค่ค่อย ๆ เสียหน่วยความจำไปเรื่อย ๆ จนกว่าจะมีใครไปตามหาว่าเพราะอะไร

รายงานสำหรับคน ส่วนผลต่างสำหรับด่านตอนบิลด์

รายการผลการตรวจเป็นรูปทรงที่ถูกต้องสำหรับด่าน แต่เป็นรูปทรงที่ผิดสำหรับอีเมลถึงทีมทำเทมเพลต CreatePreflightReport เรนเดอร์การวิเคราะห์ชุดเดียวกันออกมาเป็นข้อความร้อยแก้วที่อ่านได้ ส่วน CreatePreflightReportEx เพิ่มตัวเลือกรูปแบบรายงาน และ SavePreflightReport เขียนมันลงดิสก์ รายงานจึงเดินทางไปพร้อมกับแพ็กเกจเอกสารที่ส่งมอบได้ สัญญาการจัดเก็บถาวรจำนวนมากกำหนดให้รายงานนั้นเป็นสิ่งส่งมอบในตัวเอง ไม่ใช่แค่ผลผลิตภายใน

สมาชิกในตระกูลนี้ที่พิสูจน์คุณค่าตัวเองอย่างเงียบ ๆ คือ ComparePreflightReports ความสอดคล้องเป็นพื้นผิวของการถดถอยเช่นเดียวกับพฤติกรรมส่วนอื่น การปรับเทมเพลตเล็กน้อย ฟอนต์องค์กรที่เพิ่งซื้อสิทธิ์มา หรือการอัปเกรดไลบรารี ล้วนทำให้เกิดผลการตรวจข้อใหม่ที่ไม่มีในรุ่นก่อนได้ทั้งสิ้น และไม่มีตัวไหนประกาศตัวเอง จงเก็บรายงานต้นแบบของเอกสารตัวแทนชุดหนึ่งไว้ในระบบควบคุมเวอร์ชัน สร้างใหม่ทุกครั้งหลังการเปลี่ยนแปลง แล้วรัน ComparePreflightReports เพื่อคำนวณผลต่าง ผลต่างที่ว่างเปล่าคือผลผลิตของรุ่นที่ควรค่าแก่การเก็บไว้ ส่วนผลการตรวจที่โผล่มาแบบไม่คาดคิดจะทำให้บิลด์ล้มเหลว ซึ่งเป็นที่ที่ค้นพบมันได้ถูกกว่าการตรวจสอบภายหลังอยู่มาก

การสร้างเอาต์พุตที่ผ่านตั้งแต่รอบแรก

การพรีไฟลต์คุ้มค่าที่สุดกับไฟล์ที่เดินทางมาจากที่อื่น ส่วนเอกสารที่โค้ดของคุณเองผลิตขึ้น การไปพบข้อละเมิดหลังสร้างเสร็จแล้วค่อยย้อนกลับมาปะเป็นเส้นทางที่ช้ากว่า PDF Library for Delphi มีโหมดฝั่งการสร้างไว้ให้สำหรับแต่ละมาตรฐาน และคุณเปิดใช้ทั้งสองโหมดกับเอกสารเดียวกันได้:

แผนภาพ PDF Library for Delphi ของการรับงาน preflight ที่ขยายได้ ด้วย instance ไลบรารีแบบสตรีมหนึ่งตัวต่อ worker, การเปิดที่ประตูกั้นหลังอัปโหลดเสร็จ และรายการผลการตรวจที่ถูกปล่อยคืน
ประตูเปิดเมื่อการโอนเสร็จสิ้นเท่านั้น worker แต่ละตัวสตรีมอินพุตแบบอ่านอย่างเดียวผ่านอินสแตนซ์ไลบรารีส่วนตัว และ handle ที่คืนทุกตัวเป็นหนี้การ release หนึ่งครั้ง
var
  Pdf: TPDFlib;
  Diag: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);
    Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
    Pdf.SetPDFUAMode('en-US');
    Pdf.SetInformation(1, 'Quarterly Statement');  // /Title: จำเป็นสำหรับ PDF/UA
    // ... วาดเนื้อหาที่ทำแท็กไว้ตรงนี้ ...
    Diag := Pdf.GetPDFUADiagnostics;
    if Diag <> '' then
      Writeln('fix before shipping: ', Diag);
    Pdf.SaveToFile('statement.pdf');
    // การพรีไฟลต์ที่นับผลจริงรันบนไฟล์ที่บันทึกแล้ว:
    Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
  finally
    Pdf.Free;
  end;
end;

กับดักซ่อนอยู่ที่ตอนบันทึก การซ่อมเพื่อความสอดคล้องหลายรายการเกิดขึ้นขณะที่เอกสารกำลังถูก serialize ไม่ใช่ตอนที่คุณเปิดโหมด ได้แก่การบังคับแฟล็กการพิมพ์บน annotation การเขียนค่า AFRelationship เริ่มต้นให้ไฟล์ที่ฝังใน PDF/A-3 และการปรับลำดับแท็บกับคำบรรยายฟิลด์ฟอร์มให้เป็นมาตรฐานสำหรับ PDF/UA เอกสารที่นั่งอยู่ในหน่วยความจำจึงไม่ได้เหมือนกันทุกไบต์กับเอกสารที่ลงไปอยู่บนดิสก์ คำตัดสินพรีไฟลต์เพียงอย่างเดียวที่มีความหมายจึงเป็นคำตัดสินที่คำนวณจากไฟล์ที่บันทึกแล้ว จงตรวจสอบตัว statement.pdf เอง อย่าอนุมานความสอดคล้องจากออบเจ็กต์ที่ยังอยู่ในหน่วยความจำ เพราะไบต์ที่คุณกำลังตัดสินไม่ใช่ไบต์ที่คุณส่งมอบออกไป

แผนภาพ PDF Library for Delphi ของการแก้ไขให้เป็นไปตามมาตรฐานขณะบันทึกที่ทำระหว่าง serialization นั่นจึงเป็นเหตุผลที่ preflight ควรทำกับไฟล์ PDF ที่บันทึกแล้ว ไม่ใช่โมเดลในหน่วยความจำ
Serialization บังคับฟลากพิมพ์ของ annotation ตั้งค่าเริ่มต้น AFRelationship และทำ tab order ให้เป็นปกติ การสอดส่องจากหน่วยความจำจึงตัดสินไบต์ที่ไม่มีใครส่งออกจริง

สถานการณ์ใบแจ้งหนี้ที่พก XML ซึ่งเครื่องอ่านได้ควบคู่ไปกับเอกสารเชิงภาพ เป็นไปตามรูปแบบของ ZUGFeRD และ Factur-X ซึ่งสร้างบน PDF/A-3 กรณีเหล่านั้นควรกำหนดความสัมพันธ์ของไฟล์แนบไว้อย่างชัดเจนด้วย SetPDFA3DefaultAFRelationship เพราะ ISO 19005-3 กำหนดให้ไฟล์ที่ฝังทุกไฟล์ต้องประกาศบทบาทของตนเทียบกับเอกสาร หากปล่อยไว้ไม่กำหนด XML ที่ฝังไว้ก็เป็นแค่ก้อนข้อมูลที่ไม่ได้ระบุจุดประสงค์ ซึ่งตัวตรวจสอบสังเกตเห็น

กรรมการอิสระ: veraPDF และ Acrobat

ผู้ผลิตไม่ควรเป็นผู้ตัดสินเอาต์พุตของตัวเองเพียงรายเดียว ตัวตรวจของ PDF Library for Delphi ให้คำตัดสินที่รวดเร็วและมีโครงสร้างภายในโปรเซสเดียวกัน ซึ่งเป็นสิ่งที่คุณต้องการบนเส้นทางที่วิ่งบ่อย แต่ด่านปล่อยรุ่นสำหรับงานแบตช์เพื่อการจัดเก็บถาวรก็ยังควรส่งเอาต์พุตผ่านตัวตรวจสอบที่ไม่มีใครในทีมคุณเป็นคนเขียน veraPDF คืออิมพลีเมนเทชันอ้างอิงสำหรับ PDF/A ที่ชุมชนดูแล และเป็นเครื่องมือที่คลังเอกสารส่วนใหญ่ระบุไว้ในเกณฑ์การรับงาน จึงเป็นตัวที่ควรทำให้ตรงกัน ส่วนโปรไฟล์พรีไฟลต์ของ Acrobat เป็นตัวชี้ขาดที่มีประโยชน์เมื่อ veraPDF กับการตรวจในโปรเซสเห็นไม่ตรงกัน จงบันทึกชื่อตัวตรวจสอบและเวอร์ชันของมันไว้ข้างรายงานทุกฉบับที่จัดเก็บ คำกล่าวอ้างว่าไฟล์ผ่าน veraPDF แทบไม่มีความหมายเลยหากไม่มีหมายเลขบิลด์ที่ตรวจผ่าน เพราะเครื่องมือนี้รัดกุมกฎของตนขึ้นเรื่อย ๆ ในแต่ละรุ่น

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

อินพุตที่เข้ารหัสไว้มีทางลัด ตัวตรวจทั้งสองรับอาร์กิวเมนต์รหัสผ่านก็จริง แต่ไฟล์ PDF/A-1 ที่มี encryption dictionary ถือว่าไม่สอดคล้องอยู่แล้ว เพราะ ISO 19005-1 ห้ามการเข้ารหัสไว้อย่างเด็ดขาด งานที่ส่งเข้ามาแบบเข้ารหัสจึงถูกปฏิเสธได้ก่อนจะรันการวิเคราะห์ที่ลึกกว่านั้น ส่วนการไล่ดูว่า encryption dictionary ให้สิทธิ์อะไรบ้างจริง ๆ เป็นงานอีกชิ้นหนึ่ง ซึ่งครอบคลุมไว้ในการตรวจสอบการเข้ารหัสและสิทธิ์ของ PDF

ผลการตรวจ PDF/UA เกือบทั้งหมดสาวกลับไปถึงวิธีที่ structure tree ถูกเขียนขึ้นตั้งแต่แรก และเทคนิคการทำแท็กที่อยู่เบื้องหลังเรื่องนั้นอยู่ในการสร้าง structure tree ของ Tagged PDF ใน Delphi คลังเอกสารที่ต้องการลายเซ็นดิจิทัลด้วยควรจับคู่ด่านนี้เข้ากับเวิร์กโฟลว์ในการลงลายเซ็นและการตรวจสอบ PAdES ส่วนเอกสารอ้างอิง API การพรีไฟลต์ฉบับเต็มอยู่ที่หน้าผลิตภัณฑ์ losLab PDF Library for Delphi