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

การพิมพ์ข้อมูลแปรผัน PDF/VT ใน Delphi ด้วย PDFium VCL

โรงพิมพ์งานธุรกรรมส่งงานแถลงยอด 80,000 หน้า กลับมาพร้อมคำปฏิเสธบรรทัดเดียวว่า "not PDF/VT, RIP cannot cache." ไฟล์เปิดได้ในโปรแกรมดูทุกตัวบนโต๊ะ สีถูกต้อง ข้อมูลที่ผสานก็ถูกต้อง แต่ทั้งหมดนั้นไม่ใช่สิ่งที่แท่นพิมพ์ดิจิทัลต้องการ การพิมพ์ข้อมูลแปรผันความเร็วสูงจะอยู่หรือดับที่แท่นพิมพ์ต้องรู้ให้ได้ว่าบล็อกโลโก้ลูกค้าบนหน้า 1 เป็นออบเจ็กต์เดียวกันแบบไบต์ต่อไบต์กับบนหน้า 40,000 จึงเรนเดอร์เพียงครั้งเดียวแล้วนำกลับมาใช้ซ้ำ PDF/VT คือมาตรฐานที่ทำให้คำสัญญานั้นตรวจสอบได้ด้วยเครื่อง และคำว่า "ดูถูกต้อง" คือกับดักพอดี เพราะโครงสร้างที่ RIP อ่านนั้นมองไม่เห็นบนหน้าจอ

PDFiumPas เปิดเผยโครงสร้างนี้ผ่านเมธอดขนาดเล็กบน TPdf คือ SaveAsPdfVT สำหรับเขียน และ ValidatePdfVT สำหรับตรวจสอบ บทความนี้จะอธิบายว่าเมธอดทั้งสองเขียนลงดิสก์และตรวจอะไรจริง ๆ จุดใดที่ ISO 16612-2 เข้มกว่าที่ดูเหมือนในตอนแรก และส่วนไหนเป็นเพียงสมอเชิงโครงสร้างที่เชื่อถือได้ ไม่ใช่ preflight เต็มรูปแบบที่เอาไปคิดค่ากับลูกค้าได้

สิ่งที่ PDF/VT ทำให้เป็นมาตรฐาน และเหตุผลที่ PDF/X ต้องมาก่อน

PDF/VT (ISO 16612-2:2010) ไม่ใช่ฟอร์แมตไฟล์ใหม่ แต่มันคือชั้นของเมตะดาต้าสำหรับการเพิ่มประสิทธิภาพที่วางทับบนไฟล์ PDF/X และลำดับนี้คือแกนสำคัญ มาตรฐานกำหนดระดับความสอดคล้องไว้สามระดับ แต่มีเพียงสองระดับที่ระบุเป็นไฟล์ PDF คือ PDF/VT-1 ซึ่งเป็นเอกสารเดี่ยวในตัวเอง และ PDF/VT-2 ซึ่งเป็นโมเดลแบบชุดไฟล์ที่หน้าต่าง ๆ อ้างอิงทรัพยากรภายนอกร่วมกัน โทเคนที่สามที่คุณอาจเห็นอย่าง PDF/VT-2s ไม่ได้เป็นค่าระดับไฟล์เลย แต่มันอยู่ในส่วนหัวของสตรีม MIME ที่อธิบายไว้ใน Annex A หากคุณเจอโค้ดที่เขียน GTS_PDFVTVersion = "PDF/VT-2s" ลงใน XMP ของเอกสาร โค้ดนั้นผิด

กฎที่ต่อรองไม่ได้สำหรับไฟล์เดี่ยวคือฐาน PDF/X ISO 16612-2 §6.2.1 กำหนดว่าไฟล์ PDF/VT-1 ทุกไฟล์ต้องเป็น PDF/X-4 ที่ถูกต้องด้วย ส่วนชุดไฟล์ของ PDF/VT-2 ตาม §6.2.2 ต้องวางอยู่บน PDF/X-4p, PDF/X-5g หรือ PDF/X-5pg ด้วยเหตุนี้ผู้เขียน PDF/VT จึงไม่สามารถต่อท้ายแค่คีย์ตัวระบุสองสามตัวได้ แต่ต้องพกชุดตัวบ่งชี้ PDF/X-4 ทั้งชุดมาด้วย ซึ่งหมายถึงต้องมี OutputIntent, โปรไฟล์ ICC ปลายทางที่ฝังอยู่, รายการ XMP และข้อมูล Info ของเอกสารที่สอดคล้องกัน, trailer /ID, และไม่มีการเข้ารหัส หากขาดอย่างใดอย่างหนึ่ง คุณก็จะได้ไฟล์ที่อ้างว่าเป็น PDF/VT แต่พังทันทีเมื่อผู้บริโภคที่เป็นไปตามมาตรฐานตรวจฐาน PDF/X PDFiumPas มองชั้น PDF/X-4 เป็นส่วนหนึ่งของการบันทึก PDF/VT ดังนั้นจึงไม่ต้องเรียก SaveAsPdfX แยกก่อน ตัวฉีดจะเขียนทั้งสองชั้นในรอบเดียว

การเขียนไฟล์ด้วย SaveAsPdfVT

การเรียกขั้นต่ำต้องมีเพียงเอกสารที่เปิดใช้อยู่ เพราะ TPdfVTSaveOptions.Default จัดเตรียมโปรไฟล์ ICC แบบ sRGB ที่ฝังมาให้และค่าความสอดคล้อง pvc1 ไว้แล้ว การบันทึกทำงานภายในสามขั้นตอนคือ เอาความปลอดภัยออกก่อน, เชื่อม Info dictionary และ trailer /ID ที่มีอยู่ของเอกสารเข้าไปในชุด marker เพื่อให้ค่า XMP กับ Info ตรงกัน, แล้วค่อยผนวกออบเจ็กต์ PDF/X-4 และ PDF/VT ผ่านการอัปเดตแบบเพิ่มส่วน

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    if Pdf.LoadFromFile('statements-merged.pdf') then
    begin
      // Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
      if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
        Writeln('PDF/VT-1 written')
      else
        Writeln('Save failed (document not active?)');
    end;
  finally
    Pdf.Free;
  end;
end;

สำหรับงานผลิตจริง คุณแทบจะต้องแทนค่า OutputIntent ด้วยการกำหนดลักษณะของแท่นพิมพ์แทนค่า sRGB สำรองทั่วไปเสมอ ให้ส่งไบต์ของ ICC และตัวระบุเงื่อนไขผ่าน TPdfVTSaveOptions:

var
  Pdf: TPdf;
  Opt: TPdfVTSaveOptions;
  Icc: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('directmail-merged.pdf');
    Icc := LoadIccProfile('GRACoL2013_CRPC6.icc');  // your own loader

    Opt := TPdfVTSaveOptions.Default;
    Opt.Conformance := pvc1;            // pvc2 is normalised to pvc1 on write
    Opt.IccProfileData := Icc;
    Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
    Opt.OutputCondition := 'Commercial print, coated, CRPC6';
    Opt.RegistryName := 'http://www.color.org';
    Opt.Title := 'Spring 2026 Direct Mail Run';
    Opt.Trapped := ptvFalse;           // PDF/X Info /Trapped state

    Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

รายละเอียดหนึ่งในตัวอย่างนั้นเป็นราวกันตกที่ตั้งใจใส่มา ไม่ใช่ข้อจำกัดที่เถียงได้ การตั้ง Opt.Conformance := pvc2 ไม่ได้ทำให้ได้ไฟล์ PDF/VT-2 ตัวเขียนจะปรับคำขอที่ไม่ใช่ pvc1 กลับเป็น pvc1 เพราะ PDF/VT-2 เป็นฟอร์แมตแบบชุดไฟล์ และตัวเขียนสำหรับไฟล์เดี่ยวที่ต่อท้ายเอกสารเอาต์พุตเพียงฉบับเดียวไม่สามารถประกอบชุดทรัพยากรภายนอกตามที่ §6.2.2 ต้องการได้จริง ค่า pvc2 มีไว้สำหรับเส้นทางอ่าน เพื่อให้ ValidatePdfVT รู้จักและรายงานเอกสารแบบชุดไฟล์ที่มีอยู่แล้ว ไม่ใช่เป้าหมายสำหรับการเขียน

ต้นไม้ DPart: โครงสร้างที่ RIP อ่านจริง

หัวใจของ PDF/VT คือลำดับชั้น Document Part หรือ DPart มันคือสิ่งที่ทำให้แท่นพิมพ์แบ่งงานพิมพ์ยาว ๆ ออกเป็นระเบียน รวมระเบียนเข้ากับผู้รับหรือชุดจดหมาย และแนบ Document Part Metadata เพื่อให้อุปกรณ์ปลายทางเส้นทางและคิดค่ากับแต่ละชิ้นได้ ISO 16612-2 §6.5 วางการเชื่อมโยงไว้ชัดเจนว่า catalog ต้องมี /DPartRoot, โหนด DPart ระดับรากต้องมี /DPartRootNode และ /NodeNameList ที่ตั้งชื่อแต่ละระดับของลำดับชั้น, DPart ระดับใบครอบคลุมช่วงของ page tree, และทุกหน้าที่เป็นของ part หนึ่งต้องชี้กลับไปยังใบของมันผ่านรายการ /DPart ระดับหน้า

เมื่อเอกสารต้นทางของคุณมีลำดับชั้นที่ใช้ได้อยู่แล้ว SaveAsPdfVT จะเก็บมันไว้ แต่ถ้าไม่มี ตัวเขียนจะสร้างแบบขั้นต่ำขึ้นมาเอง คือ DPart ระดับเอกสารหนึ่งตัวที่ครอบ page tree ปัจจุบันตามลำดับ พร้อมเพิ่มการอ้างกลับ /DPart ให้กับออบเจ็กต์หน้าที่ยังใช้งานอยู่ทุกตัว และ /NodeNameList [/Document] ระดับเดียว จงซื่อสัตย์กับตัวเองว่า tree ขั้นต่ำนั้นคืออะไร มันเป็นสมอเชิงโครงสร้างที่ตอบโจทย์รูปแบบตาม §6.5 ไม่ใช่เมตะดาต้าทางธุรกิจ มันไม่สามารถสร้างผู้รับ, ขอบเขตชิ้นงานจดหมาย, หรือชุดผลิตภัณฑ์ขึ้นมาเองได้ เพราะข้อมูลนั้นไม่เคยมีอยู่ในต้นทาง ถ้าคุณมีข้อมูลรายผู้รับ คุณคาดว่าจะต้องสร้าง DPart tree ที่ลึกกว่านั้นเอง และขยาย /NodeNameList ให้ตรงกับระดับที่คุณสร้าง

การตรวจสอบที่ไม่หยุดอยู่แค่การมีคีย์

ValidatePdfVT คืนค่า record TPdfVTValidationResult ที่มีอยู่สามอย่างคือ Conformance ที่ตรวจพบ, ชุด Issues, และตัวช่วย IsCompliant ซึ่งจะเป็นจริงก็ต่อเมื่อความสอดคล้องเป็นระดับจริงและชุดปัญหาว่างเปล่า การแจกแจงปัญหาถูกออกแบบให้จำเพาะ จึงทำให้ผลที่ล้มเหลวบอกได้ว่าคุณพลาดข้อไหน แทนที่จะบอกแค่ว่า "ไม่ถูกต้อง":

var
  Pdf: TPdf;
  Res: TPdfVTValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('statements-pdfvt.pdf');
    Res := Pdf.ValidatePdfVT;

    if Res.IsCompliant then
      Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
    else
    begin
      if pvviMissingDPartRoot in Res.Issues then
        Writeln('DPart hierarchy missing or unusable');
      if pvviMissingPdfXIdentifier in Res.Issues then
        Writeln('PDF/X-4 base identifier absent');
      if pvviMissingOutputIntent in Res.Issues then
        Writeln('OutputIntent / ICC profile missing');
      if pvviEncryptionPresent in Res.Issues then
        Writeln('Encrypted - PDF/X forbids this');
    end;
  finally
    Pdf.Free;
  end;
end;

สองการตรวจที่ควรเข้าใจให้ลึกคือการจับคู่ความสอดคล้องกับการไล่ตรวจ DPart เพราะทั้งคู่เคยผ่อนปรนเกินไปและถูกปรับให้เข้มขึ้นตามสเปกแล้ว ฝั่งการจับคู่ ตัวตรวจสอบจะจับคู่แบบตรงตัว ไม่ใช่แนว "PDF/X แบบไหนก็ได้" ไฟล์ PDF/VT-1 จะยอมรับก็ต่อเมื่ออยู่บนฐาน PDF/X-4 และไฟล์ PDF/VT-2 จะยอมรับได้เฉพาะบน PDF/X-4p, PDF/X-5g หรือ PDF/X-5pg เท่านั้น เมอร์กเกอร์ PDF/VT-1 ที่วางอยู่บนฐาน PDF/X-1a จะถูกรายงาน ไม่ได้ปล่อยผ่าน

ส่วนการไล่ตรวจ DPart คือจุดที่ความเข้มงวดส่วนใหญ่อยู่ มันไม่พอให้ catalog มีคีย์ /DPartRoot เพราะออบเจ็กต์เปล่าที่ถูกปลอมขึ้นมาหรือออบเจ็กต์ที่ไม่มีลิงก์ไปยังหน้าใดเลยก็ยังใช้งานไม่ได้อยู่ดี HasValidDPartHierarchy และ ValidateDPartNode แบบเรียกซ้ำจะไล่โครงสร้างทั้งหมด พวกมันตามลิงก์ parent, ปฏิเสธ child ซ้ำและวงวน, บังคับให้ /Start กับ /DParts ไม่เกิดร่วมกัน, และกำหนดให้ช่วงหน้าของ leaf ต้องครอบ page tree ตามลำดับ depth-first โดยให้ /DPart ของแต่ละหน้าชี้ไปยัง leaf ที่ครอบหน้านั้น ทุกความผิดพลาดภายในเหล่านี้จะถูกรวมลงในบิตปัญหาเดียว pvviMissingDPartRoot แทนที่จะขยาย enum สาธารณะ ดังนั้นให้มองแฟลกนั้นว่า "ลำดับชั้น DPart ใช้งานไม่ได้" ไม่ใช่แปลตรงตัวว่า "ไม่มีคีย์ราก"

กับดักไวยากรณ์สามข้อที่ตัวตรวจสอบบังคับแล้ว

การไล่ตรวจตาม §6.5 Table 4 หลายรอบพบรูปแบบที่เวอร์ชันก่อนหน้านี้ยอมรับ แต่สเปกไม่ยอม รูปแบบเหล่านี้เป็นสิ่งที่ tree DPart ที่สร้างด้วยมือมักทำพลาด จึงคุ้มค่าที่จะพูดให้ชัดเจน:

  • /DParts เป็นอาร์เรย์ของอาร์เรย์ ไม่ใช่อาร์เรย์แบน แต่ละสมาชิกของอาร์เรย์ชั้นนอกต้องเป็นอาร์เรย์อ้างอิงแบบ indirect reference เองด้วย /DParts [9 0 R] แบบแบนจะถูกปฏิเสธ ส่วนรูปแบบที่ถูกต้องคือ /DParts [[9 0 R] [10 0 R]] วิธีนี้ช่วยไม่ให้โครงสร้างที่ไม่เป็นลำดับชั้นปลอมตัวเป็นระดับที่ถูกต้องได้
  • /End ใช้ได้เฉพาะเมื่อเป็นช่วงหลายหน้าจริง ๆ เท่านั้น leaf DPart จะมี /End ได้ก็ต่อเมื่อมี /Start ด้วย และ /End ต้องอยู่หลัง /Start ตามลำดับของ page tree ถ้าเป็นกรณีเสื่อมอย่าง /Start 3 0 R /End 3 0 R ลำดับชั้นจะกลายเป็นใช้งานไม่ได้แทนที่จะถูกมองว่าเป็นส่วนที่มีหน้าเดียว
  • ชื่อใน /NodeNameList ต้องผ่านการ unescape ชื่อ PDF แล้วเป็น XML NMTOKEN ได้ ชื่ออย่าง /Bad#20Name จะขยายออกมาเป็นสตริงที่มีช่องว่าง ซึ่งไม่ใช่ token ที่ถูกต้อง การใช้งานจะตรวจ ASCII แบบเบา ๆ คืออักษร ตัวเลข ., -, _, : และไบต์ที่ไม่ใช่ ASCII เพื่อจับความผิดพลาดเรื่องช่องว่างและตัวคั่นโดยไม่ปฏิเสธชื่อท้องถิ่นหรือชื่อเฉพาะของผู้ผลิตที่ถูกต้อง

ตัวบอก XMP: สองวิธีเขียนพร็อพเพอร์ตีเดียวกัน

การระบุ PDF/VT อยู่ใน XMP ภายใต้ namespace pdfvtid โดยเฉพาะ GTS_PDFVTVersion และ GTS_PDFVTModDate ควบคู่ไปกับ xmp:CreateDate และ xmp:ModifyDate มาตรฐาน จุดละเอียดที่ทำให้ผู้อ่านแบบง่ายรายงานว่า "หายไป" แบบผิด ๆ คือค่าพวกนี้เขียนได้สองแบบ คือเป็นข้อความของ element (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) หรือเป็น RDF attribute บน element description เอง PDFiumPas อ่านได้ทั้งสองแบบ ดังนั้นไฟล์ที่เครื่องมืออื่นเขียนด้วยสไตล์ attribute จึงไม่ถูกลงโทษ นอกจากนี้ยังบังคับกฎความสอดคล้องใน §6.3 ว่า GTS_PDFVTModDate ต้องเท่ากับ xmp:ModifyDate หากไม่ตรงกันจะยก pvviModDateMismatch

มีกฎอีกข้อจากข้อเดียวกันคือ ค่า GTS_PDFVTVersion ที่ไม่รู้จักจะถูกเก็บไว้เป็น pvcUnknown แทนที่จะถูกกลืนกลับไปเป็น pvcNone ความต่างนี้สำคัญในการทำงานจริง pvcNone หมายถึง "ไม่มี marker ของ PDF/VT เลย เป็น PDF ธรรมดา" ส่วน pvcUnknown หมายถึง "มีบางอย่างเขียนเวอร์ชันที่ตัวตรวจสอบนี้ไม่รู้จัก" ซึ่งรวมกรณี PDF/VT-2s อยู่ด้วย การปะปนสองค่านี้เข้าด้วยกันจะซ่อนไฟล์ที่ผิดรูปไว้ในกล่องเดียวกับเอกสารธรรมดา

ขอบเขตที่คำรับประกันสิ้นสุดลง

ควรพูดให้ชัดว่าขอบเขตที่เมธอดเหล่านี้รับประกันคืออะไร เพราะการทำให้การพิมพ์ข้อมูลแปรผันเป็นไปตามมาตรฐานมีมูลค่าทางธุรกิจจริง การตรวจ DPart และการจับคู่ระดับความสอดคล้องเป็นการตรวจโครงสร้างระดับไบต์ พวกมันยืนยันว่าโครงสร้างเพิ่มประสิทธิภาพ, ตัวบ่งชี้ฐาน PDF/X-4, OutputIntent และ XMP มีอยู่จริงและสอดคล้องกันภายใน แต่ไม่ใช่ preflight ระดับเนื้อหาของ PDF/X-4 พวกมันไม่ได้ตรวจว่าทุกสีอยู่ในเงื่อนไขเอาต์พุตที่ประกาศไว้, ฟอนต์ทุกตัวฝังอยู่แล้ว, หรือไม่มีกรณีขอบของ transparency-blending ที่ต้องห้ามหลุดเข้ามา หากเป็นงานที่จะส่งเข้าแท่นพิมพ์ตามสัญญา ให้จับการตรวจโครงสร้างของ PDFiumPas คู่กับเอนจิน preflight PDF/X เฉพาะทางและการพิมพ์ทดสอบ เหมือนที่คุณตรวจความสมเหตุสมผลของคำอ้างความสอดคล้องแบบอื่น ๆ ชั้นโครงสร้างจะจับความผิดพลาดที่ทำให้ RIP แคชพังแบบเงียบ ๆ มันคือครึ่งหนึ่งของการตรวจที่สมบูรณ์ ไม่ใช่ทั้งหมด

ถ้าคุณกำลังนำการตรวจเหล่านี้ไปใส่ใน release gate ที่กว้างขึ้น แนวทางสแกนระดับไบต์แบบเดียวกันก็เป็นฐานให้กับงานด้านมาตรฐานอื่นของไลบรารีด้วย รวมถึง การตรวจสอบ object และ cross-reference streams ก่อนที่ไฟล์จะไปถึง preflight และวินัยเรื่อง shared object เบื้องหลัง page stamp แบบใช้ซ้ำได้ด้วย Form XObjects ที่ทำให้เอกสารพร้อมสำหรับ RIP ตั้งแต่ต้น API สำหรับบันทึกและตรวจสอบ PDF/VT กับ PDF/X ที่อธิบายที่นี่เป็นส่วนหนึ่งของ PDFium VCL component สำหรับ Delphi และ C++Builder ซึ่งหน้าผลิตภัณฑ์มีเอกสารอ้างอิงด้านความสอดคล้องครบถ้วน