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

การเรนเดอร์สี Separation และ DeviceN Spot Colors ใน Delphi

HotPDF เรนเดอร์สีพิเศษ Separation และ DeviceN บนหน้าเอกสาร PDF ที่โหลดขึ้นมาโดยการแปลงพื้นที่สีผ่าน HPDFResolveColorSpace, ประมวลผลฟังก์ชันแปลงโทนสี (tint-transform) ด้วย HPDFEvalTintTransform และแปลงผลลัพธ์ผ่านพื้นที่สีทางเลือกไปเป็น RGB เพื่อแสดงผลบนหน้าจอ ตั้งแต่เวอร์ชัน v2.375.0 ไปป์ไลน์ดังกล่าวจะประมวลผลฟังก์ชัน PDF ทั้ง 4 ประเภท รวมถึงเครื่องคำนวณ PostScript Type 4 ดังนั้นไฟล์ที่พร้อมส่งโรงพิมพ์ที่ใช้หมึก Pantone จะแสดงสีจริงแทนการแสดงตัวทดแทน บทความนี้จะพาทุกคนไปเจาะลึกการทำงานของไปป์ไลน์นี้ และอธิบายว่าทำไมระบบจึงเคยทำงานล้มเหลวในระหว่างที่เราสร้างมันขึ้นมา

กรณีที่เป็นตัวทริกเกอร์มักจะเหมือนกันเสมอ: ลูกค้าได้รับ PDF จากร้านพิมพ์ โดยหัวข้อเรื่องใช้หมึกสีพิเศษระบุชื่อ (named spot ink), งานศิลป์บรรจุภัณฑ์ใช้การผสมผสานสี DeviceN แบบหมึกสองสี และข้อความเนื้อหาเป็นสีดำล้วน ใน Acrobat แฟ้มนี้แสดงผลได้อย่างสมบูรณ์แบบ แต่ในแอปพลิเคชัน Delphi ของคุณ หัวข้อเรื่องกลับเรนเดอร์ออกมาเป็นสีดำ หรือที่แย่กว่านั้นคือไม่แสดงผลเลย และลูกค้าก็รายงานบั๊กกับซอฟต์แวร์ของคุณแทนที่จะเป็นรายงานกับตัวไฟล์ จริงๆ แล้วไฟล์ทำงานได้ถูกต้อง แต่ตัวเรนเดอร์ของคุณไม่สามารถตีความพื้นที่สีที่ไฟล์ใช้งานได้เท่านั้นเอง

ทำไมสีพิเศษจึงเรนเดอร์ออกมาเป็นสีดำในโปรแกรมอ่าน PDF?

ข้อกำหนด ISO 32000-1 §8.6 นิยามกลุ่มพื้นที่สีไว้ 3 กลุ่ม: พื้นที่สีอุปกรณ์ (DeviceGray, DeviceRGB, DeviceCMYK), พื้นที่สีอิง CIE (CalGray, CalRGB, Lab, ICCBased) และพื้นที่สีพิเศษ (Indexed, Separation, DeviceN, Pattern) ทุกสิ่งภายนอกกลุ่มอุปกรณ์จะถูกเลือกใช้งานด้วยตัวดำเนินการทั่วไป: cs และ CS จะเลือกพื้นที่สีตามชื่อจากพจนานุกรมทรัพยากรของหน้า จากนั้น sc, SC, scn และ SCN จะส่งมอบค่าส่วนประกอบสี ตัวเรนเดอร์ที่ข้ามตัวดำเนินการเหล่านี้จะคงใช้ค่าสีใดๆ ก็ตามที่ถูกกำหนดไว้ล่าสุด ซึ่งสำหรับหน้าเอกสารที่เปิดขึ้นมาด้วยหัวเรื่องสีพิเศษ ค่าสีดังกล่าวก็คือสีดำเริ่มต้นของ DeviceGray

HotPDF ได้เพิ่มชุดตัวดำเนินการทั่วไปให้กับตัวเรนเดอร์หน้าในเวอร์ชัน v2.333.0 ควบคู่ไปกับเส้นทางการวิเคราะห์ที่รวมเป็นหนึ่งเดียว: ทุกรายการทรัพยากร /ColorSpace ไม่ว่าจะเป็นชื่อปกติ, อาร์เรย์แบบอินไลน์ หรือการอ้างอิงทางอ้อม จะถูกแยกแยะออกเป็นหนึ่งบันทึกข้อมูล THPDFColorSpace และการร้องขอสีถมหรือสีเส้นทุกครั้งจะถูกรวมเข้าสู่การเรียกใช้ HPDFResolveColor เพียงหนึ่งเดียว การแจกแจงประเภทตระกูลช่วยให้เห็นความครอบคลุมได้ในทันที

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

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

การแปลงโทนสี (tint transform) เปลี่ยนค่าหมึกค่าหนึ่งให้เป็นสีจริงได้อย่างไร?

พื้นที่สี Separation จะบรรจุข้อมูล 3 ส่วน: ชื่อหมึก, พื้นที่สีทางเลือก และฟังก์ชันแปลงโทนสี อาร์เรย์ [/Separation /PANTONE485 /DeviceCMYK f] ระบุว่า: เมื่อสตรีมเนื้อหาเขียนรหัส 0.8 scn ให้ส่งค่าโทนสี 0.8 ไปยังฟังก์ชัน f และวาดสีตามค่า CMYK ที่ได้กลับมา ส่วน DeviceN จะขยายความสามารถนี้ไปสู่หมึกจำนวน N สีร่วมกับฟังก์ชันอินพุตจำนวน N รายการ ชื่อหมึกนั้นมีไว้เพื่อเป็นคำแนะนำบนหน้าจอเท่านั้น แต่ฟังก์ชันแปลงโทนสีคือหัวใจสำคัญของการเรนเดอร์ทั้งหมด ดังนั้นตัวเรนเดอร์ที่แกะพื้นที่สีได้แต่ข้ามขั้นตอนฟังก์ชันไป จะไม่ได้ผลลัพธ์ที่เป็นประโยชน์เลย

ฟังก์ชัน HPDFEvalTintTransform คือกลไกขับเคลื่อนเบื้องหลังขั้นตอนนั้น ซึ่งเริ่มใช้งานในเวอร์ชัน v2.334.0 โดยทำหน้าที่ประมวลผลฟังก์ชันเอกซ์โพเนนเชียล Type 2 (C0 + x^N * (C1 - C0) ร่วมกับการจำกัดขอบเขต Domain และ Range), ฟังก์ชันประสาน Type 3 (การเรียกซ้ำฟังก์ชันย่อยที่เลือกตามขอบเขต ร่วมกับแผนผัง Encode ใหม่) และฟังก์ชันตัวอย่าง Type 0 ขนาดตัวอย่าง 8, 16 และ 32 บิต ฟังก์ชัน Type 0 นี้คือกลไกตารางค้นหาข้อมูล (lookup-table) แบบเดียวกับที่เราครอบคลุมไว้จากฝั่งการสร้างในบทความเกี่ยวกับการสร้างตารางค้นหาสี Type 0; ฝั่งเรนเดอร์จะสำรวจโครงสร้างเดียวกันนี้ในทิศทางย้อนกลับ จากไบต์ตัวอย่างที่ถอดรหัสแล้วกลับไปสู่ค่าองค์ประกอบสี

ฟังก์ชันเครื่องคำนวณ PostScript Type 4 เคยเป็นส่วนสุดท้ายที่ยังค้างอยู่ จนกระทั่งในเวอร์ชัน v2.375.0 พวกมันถูกลดระดับไปยังภาพจำลองที่เป็นกลางอย่างละมุนละไม; นับตั้งแต่เวอร์ชัน v2.375.0 ฟังก์ชัน HPDFEvalPostScriptCalculator จะรันชุดตัวดำเนินการตามข้อกำหนด ISO 32000-1 §7.10.5 เต็มรูปแบบบนสแต็กตัวถูกดำเนินการที่จำกัด: ได้แก่ คณิตศาสตร์, การเปรียบเทียบ, ตัวดำเนินการบูลีนและบิตไวด์, การจัดการสแต็กซึ่งรวมถึงคำสั่ง roll และเงื่อนไข if/ifelse ไวยากรณ์ในมุมพิเศษมีความเข้มงวดมากกว่าที่คิด: คำสั่ง round ของ PostScript จะปัดเศษครึ่งหนึ่งไปยังค่าที่มากกว่า ดังนั้นฟังก์ชันปัดเศษ Round แบบดั้งเดิมของ Delphi (banker's rounding) จึงไม่สามารถนำมาใช้งานได้; ตัวดำเนินการตรีโกณมิติจะทำงานในหน่วยองศา โดยที่ atan จะส่งกลับค่าในช่วง [0, 360) และ exp คือการยกกำลังที่มีตัวถูกดำเนินการ 2 ตัว ไม่ใช่เอกซ์โพเนนเชียลธรรมชาติ ตัวประมวลผลนี้ยังควบคุมการถมสีไล่ระดับตามฟังก์ชัน ซึ่งเป็นสาเหตุที่การเรนเดอร์การไล่ระดับสีตามแนวแกนและรัศมี มีความสามารถในการประมวลผลการไล่ระดับที่ควบคุมด้วยเครื่องคำนวณเพิ่มเข้ามาในรุ่นปรับปรุงเดียวกัน

// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

การปรับแต่งสี CIE จะทำตามมาตรฐานโดยไม่มี CMM ของบุคคลภายนอก CalGray, CalRGB, Lab และ ICCBased ต่างก็ถูกแปลงไปเป็น sRGB สำหรับหน้าจอ คลาส Lab แปลง XYZ ผ่านทาง Matrix ของ Bradford ที่แคชไว้ และ ICCBased แปลงผ่านทาง /Alternate ที่กำหนด หรือใช้ CMYK/RGB/Gray แทนตามจํานวน component /N

CalGray, CalRGB, Lab และ ICCBased โดยไม่มีเอนจิน ICC

ตระกูลสีอิง CIE จะใช้เส้นทางอีกฝั่งหนึ่งของตัวคลี่คลายตัวเดียวกัน HotPDF จะแปลงค่า Lab ผ่านขั้นตอนมาตรฐานคือจาก Lab ไปยัง XYZ ไปยัง sRGB ซึ่งรวมถึง breakpoint cube ขนาด 6/29 ในฟังก์ชันการส่งกลับค่า และจัดการ CalRGB ร่วมกับแกมมาแต่ละช่องบวกกับเมทริกซ์เชิงเส้น 3x3 และ CalGray ร่วมกับค่าแกมมาเดี่ยว รายละเอียดที่มีความสำคัญต่อประสิทธิภาพการทำงานคือการจัดการจุดขาว (white point): เมทริกซ์การแปลงจาก XYZ ไปยัง sRGB จะได้รับการปรับตามทฤษฎีของ Bradford ให้เข้ากับจุดขาวที่ประกาศไว้ในพื้นที่สีและแคชไว้ในบันทึกข้อมูล THPDFColorSpace record ดังนั้นงานระดับพิกเซลจึงยังคงเป็นการคูณแบบ 3x3 เพียงครั้งเดียว ไม่ว่าแหล่งกำเนิดแสงที่ประกาศไว้จะมีความพิเศษเพียงใดก็ตาม

พื้นที่สี ICCBased จะได้รับการจัดการในทางปฏิบัติอย่างเหมาะสม ข้อกำหนดของ PDF กำหนดให้ทุกสตรีมของ ICCBased ต้องประกาศพื้นที่สี /Alternate หรือชี้เป็นนัยผ่านทางจำนวนส่วนประกอบ /N เพื่อให้โปรแกรมอ่านที่ไม่มีเอนจินจัดการสีสามารถเรนเดอร์สีที่สมเหตุสมผลออกมาได้ HotPDF คลี่คลาย ICCBased ผ่านทางเลือกดังกล่าว หรือคาดเดา DeviceGray, DeviceRGB หรือ DeviceCMYK จาก /N ที่เป็น 1, 3 หรือ 4 เมื่อไม่พบรายการ และจะไม่แกะข้อมูลไบต์โปรไฟล์เลย สิ่งนี้ทำให้ไม่มีการพึ่งพากับ lcms และไม่มีต้นทุนการค้นหาโปรไฟล์ แลกมาด้วยความถูกต้องแม่นยำของสีสัน: พื้นที่สี ICCBased ที่ตัวโปรไฟล์จริงมีความคลาดเคลื่อนอย่างมากจากทางเลือกทดแทนจะแสดงผลสีของพื้นที่สีทดแทนแทน สำหรับการอ่านบนหน้าจอและภาพตัวอย่างขนาดเล็ก นี่เป็นข้อตกลงแบบเดียวกับที่โปรแกรมอ่านขนาดเล็กทั่วไปใช้ และเป็นขอบเขตที่ต้องชี้แจงให้ชัดเจนในเอกสารของคุณเอง

บั๊กที่ทำให้การค้นหาพื้นที่สีระบุชื่อล้มเหลวทุกครั้ง

ส่วนเชื่อมโยงตัวดำเนินการในรุ่น v2.333.0 จัดส่งมาพร้อมกับข้อบกพร่องที่ซ่อนอยู่อย่างเงียบเชียบถึง 42 รุ่น: โดยตัวจัดการ cs และ CS ค้นหาตัวถูกดำเนินการโดยยังคงมีเครื่องหมายทับนำหน้า (เช่น /CS0) เทียบกับคีย์พจนานุกรมทรัพยากรที่จัดเก็บไว้โดยไม่มีเครื่องหมายทับ (เช่น CS0) ส่งผลให้การค้นหาพื้นที่สีระบุชื่อล้มเหลว 100% เสมอมา และตัวโค้ดจะถอยกลับไปใช้ DeviceGray เริ่มต้นอย่างเงียบเชียบ อาการที่ปรากฏนั้นมีความซ่อนเร้นในแบบที่แย่ที่สุด: สีถม Separation ของ 1 scn กลายเป็น DeviceGray 1.0 ซึ่งระบายเป็นสีขาว และการทาสีขาวบนหน้ากระดาษสีขาวก็ไม่ใช่ข้อผิดพลาดการเรนเดอร์ที่มีใครแคปหน้าจอมาบอก วิธีแก้ไขในรุ่น v2.375.0 คือการเพิ่มระบบช่วยปรับแต่งชื่อร่วม (name-normalization helper) ที่นำมาใช้กับการค้นหาตัวถูกดำเนินการเทียบกับทรัพยากรทุกครั้ง

ข้อบกพร่องคู่กัน 2 จุดที่ถูกตรวจพบจากการสืบสวนเดียวกัน จุดแรก การอ้างอิงทางอ้อมไปยังวัตถุประเภทอาร์เรย์ถูกส่งคืนกลับมาโดยไม่ได้คลี่คลาย: ตัวเข้าถึงเอกสารของตัวเรนเดอร์มีตัวจัดการที่คลี่คลายเฉพาะประเภทสตรีมและพจนานุกรมเท่านั้น ดังนั้น /CS0 5 0 R ที่ชี้ไปยังอาร์เรย์อิสระ [/Separation ...] จึงถูกส่งคืนมาเป็นข้อมูลอ้างอิงดิบและพื้นที่สีถูกประมวลผลเป็นแบบไม่รองรับ จุดที่สอง ฟังก์ชัน HPDFReadNumericArray บังคับใช้ไวยากรณ์ความยาวอย่างเคร่งครัด โดยกำหนดให้อาร์เรย์ของ PDF ต้องมีความยาวอย่างน้อยเท่ากับบัฟเฟอร์ที่ส่งมอบให้ การอ่านค่า /C0 และ /C1 ของฟังก์ชัน Type 2 ลงในบัฟเฟอร์ขนาด 4 ส่วนประกอบจึงล้มเหลวสำหรับพื้นที่ทางเลือกขนาด 1 และ 3 ส่วนประกอบ ส่งผลให้อาร์เรย์ทั้งสองมีค่าเป็นศูนย์ และทุกโทนสีเอกซ์โพเนนเชียลที่ไม่ใช่ CMYK จะเรนเดอร์ออกมาเป็นสีดำ จนกระทั่งในรุ่น v2.376.0 ได้นำระบบอ่านแบบผ่อนปรน HPDFReadNumericArrayUpTo มาใช้งาน บทเรียนสำคัญคือ: คีย์ PDF ใดๆ ที่ระบุว่าเก็บบันทึกอาร์เรย์ตัวเลขที่มีความยาวแปรผัน จะต้องอ่านด้วยระบบอ่านที่ถมข้อมูลเท่าที่มีอยู่จริง เนื่องจากบัฟเฟอร์ที่มีขนาดคงที่ร่วมกับไวยากรณ์ตรวจสอบที่เข้มงวดจะแปลงไฟล์ที่ถูกต้องให้กลายเป็นค่าศูนย์อย่างเงียบๆ

จะทดสอบการเรนเดอร์สีพิเศษโดยไม่หลอกตัวเองได้อย่างไร?

คำถามที่น่าอึดอัดใจคือทำไมชุดทดสอบจึงยังคงผ่าน (แสดงสีเขียว) ตลอดช่วงเวลานั้น การทดสอบดั้งเดิม SeparationRendersDistinguishable ยืนยัน (assert) เพียงว่าบิตแมปที่เรนเดอร์ออกมาไม่ได้เป็นสีดำทั้งหมด ไปป์ไลน์ที่ลดระดับสีพิเศษทุกสีลงไปเป็น DeviceGray จะสร้างผลลัพธ์ที่เป็นสีเทาและสีขาวซึ่งไม่ใช่สีดำล้วน ดังนั้นคำยืนยันจึงผ่านตลอดเวลาทั้งที่ฟีเจอร์ทั้งหมดเสียอยู่ คำยืนยันที่อ่อนแออย่าง "ไม่ใช่หน้าว่าง", "ไม่ใช่สีดำทั้งหมด" หรือ "ค่าไดเจสต์ไม่เป็นศูนย์" ไม่สามารถแยกแยะตัวเรนเดอร์ที่ทำงานได้ปกติออกจากตัวเรนเดอร์ที่เสียหายได้ เนื่องจากเกือบทุกโหมดความล้มเหลวยังคงสร้างพิกเซลสีบางอย่างออกมาอยู่ดี

รูปแบบคำยืนยันที่สามารถจับความล้มเหลวเหล่านี้ได้จริงคือการล็อกสีที่คาดหวัง: โดยเรนเดอร์ไฟล์ PDF ขนาดเล็กที่สร้างขึ้นด้วยมือซึ่งมีสี Separation ที่วิเคราะห์ออกมาเป็นเฉดสีที่ระบุ จากนั้นนับพิกเซลที่มีเฉดสีนั้นเป็นหลัก การเรนเดอร์ลงสู่บิตแมปเพื่อตรวจสอบจะใช้จุดเข้าใช้งาน RenderLoadedPageToBitmap แบบเดียวกับที่อธิบายไว้ในคู่มือการเรนเดอร์หน้ากระดาษเป็นบิตแมป

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Lock the expected ink: demand a real area of red-dominant
        // pixels, never settle for "not all black".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

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

การเรนเดอร์สีพิเศษและพื้นที่สี CIE เป็นส่วนหนึ่งของไปป์ไลน์เอกสารที่โหลดขึ้นมาใน HotPDF Component สำหรับ Delphi และ C++Builder ควบคู่ไปกับเอนจินฟังก์ชัน, การเรนเดอร์ไล่ระดับสี และเส้นทางการส่งออกบิตแมปที่แสดงไว้ด้านบน