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

ฟังก์ชัน PDF Type 2/3/4 ใน Delphi: Exponential, PostScript

HotPDF คอมโพเนนต์ PDF แบบ VCL เนทีฟสำหรับ Delphi และ C++Builder ประเมินฟังก์ชัน PDF สามชนิดที่สร้างจากสูตรแทนที่จะเป็นตารางตัวอย่าง ได้แก่ Type 2 แบบ exponential interpolation, Type 3 แบบ stitching และ Type 4 แบบ PostScript calculator ซึ่งตรงกับ ISO 32000-1 §7.10.3, §7.10.4 และ §7.10.5 Type 2 ผสมระหว่าง output vector สองตัวตามเส้นโค้ง Type 3 เชื่อม sub-function หลายตัวเข้าด้วยกันข้าม domain ของ input เดียว และ Type 4 รันโปรแกรม PostScript ที่ถูกจำกัดไว้ ซึ่งสามารถแตกกิ่ง เปรียบเทียบ และคำนวณเกือบทุกอย่างที่ content stream ต้องการจาก input ของมัน ผิดพลาดเล็กน้อยในสามชนิดนี้แล้วความล้มเหลวจะไม่มีทางประกาศตัวเองว่าเป็นบั๊กเลย มันจะปรากฏเป็น gradient ที่มีแถบราบตายไปเลย สีสปอตที่ render ออกมาเป็นสีดำล้วน หรือฟังก์ชัน calculator ที่คลาดเคลื่อนไปพอดีหนึ่งหน่วยที่ input ซึ่ง test suite บังเอิญไม่ได้ลอง

ทั้งสามนี้อยู่ข้างๆ ชนิดที่สี่ คือ Type 0 ซึ่งเก็บตารางตัวอย่างแทนที่จะเป็นสูตร และครอบคลุมแยกต่างหากในบทความคู่กันเรื่องตาราง lookup สีของ Type 0 สองตระกูลนี้แก้ปัญหาเดียวกัน คือแม็ป input ไปเป็น output แต่ Type 0 เป็นข้อมูลที่คำนวณครั้งเดียวและอบเข้าไปในไฟล์ ในขณะที่ Type 2, 3 และ 4 เป็นโค้ดที่ตัวอ่านประเมินในทุกครั้งที่เรียก ทั้งสี่ชนิดใช้จุด dispatch เดียวกันใน renderer ของ HotPDF โดย key ด้วย entry /FunctionType ของ function dictionary ดังนั้น shading, tint transform หรือ halftone spot function จึงไม่จำเป็นต้องรู้เลยว่าได้รับชนิดไหนในสี่ชนิดก่อนที่จะขอสี

ฟังก์ชัน PDF Type 2 แบบ exponential ทำงานอย่างไร

ฟังก์ชัน PDF Type 2 คำนวณสูตรเดียว คือ y = C0 + x^N × (C1 − C0) ใช้ทีละ component โดยที่ x คือ input เดียวของฟังก์ชัน ซึ่งถูก normalize เทียบกับ /Domain ของมันก่อนที่สูตรจะรัน (ISO 32000-1 §7.10.3) /C0 และ /C1 คือ output vector ที่ปลายทั้งสองข้างของช่วงนั้น หนึ่งตัวเลขต่อหนึ่ง output component และ /N คือเลขชี้กำลังที่กำหนดรูปร่างของเส้นโค้งระหว่างมัน N = 1 ให้เส้นตรงแบบ linear ที่อยู่เบื้องหลัง gradient stop และการแปลง duotone ส่วนใหญ่ N มากกว่า 1 ดึงเส้นโค้งเข้าหา C0 และ N ระหว่าง 0 กับ 1 ผลักมันเข้าหา C1 RegisterExponentialFunction สร้าง dictionary นั้นจากอาร์กิวเมนต์ห้าตัวและส่งกลับ function object ที่พร้อมเสียบเข้ากับ shading, halftone spot function หรือที่อื่นใดที่สเปครับ key /Function

ความสัมพันธ์เรื่องจำนวน component ระหว่าง C0 กับ C1 สำคัญสองครั้ง ครั้งหนึ่งเมื่อคุณสร้างฟังก์ชัน Type 2 และอีกครั้งเมื่อ HotPDF ต้อง render ฟังก์ชันที่มันไม่ได้สร้างเอง ฝั่งการสร้าง RegisterExponentialFunction ตรวจสอบ C0 กับ C1 เทียบกันและยก exception ถ้าไม่ตรงกัน ดังนั้นการเรียกที่ไปถึง BeginDoc จะได้ function object ที่สอดคล้องในตัวเองอยู่แล้ว แต่ฝั่งการ render ตัวประเมินต้องเชื่อ array /C0 และ /C1 ใดๆ ก็ตามที่ไฟล์ต้นทางประกาศไว้จริง เช่น ไฟล์จากโรงพิมพ์ที่เปิดเพื่อดูตัวอย่าง หรือเอกสารที่เซ็นแล้วซึ่งแสดงกลับให้ผู้ใช้ดู และเวอร์ชันก่อน 2.376.0 อ่าน array เหล่านั้นเข้า buffer ที่มีขนาดสำหรับสี่ component คือกรณี CMYK tint แบบ exponential ของ DeviceGray หรือ DeviceRGB ที่มี /C0 และ /C1 หนึ่งหรือสาม element จะอ่านล้มเหลวเงียบๆ และปล่อยให้ทั้งสอง array เป็นศูนย์ ดังนั้น tint จึงเติมสีดำล้วนแทนที่จะเป็นสีที่ตั้งใจไว้ เวอร์ชัน 2.376.0 ปรับขนาดตัวอ่านให้ตรงกับจำนวน output ที่ประกาศจริงของฟังก์ชัน แทนที่จะเป็น buffer ขนาดคงที่ ซึ่งเป็นบั๊กประเภทที่มีแค่ test case แบบไม่ใช่ CMYK เท่านั้นที่จะเปิดโปงได้ เพราะ suite เดิมรัน CMYK ตลอด ซึ่งสี่เข้าสี่พอดีเสมอ

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Type 3 stitching: การเชื่อม sub-function ข้าม array Bounds

ฟังก์ชัน PDF Type 3 เชื่อม sub-function จำนวน k ตัวเข้าเป็นการแม็ปแบบ piecewise เดียวข้าม /Domain ของ input ตัวเดียว และ array สองตัวที่ทำให้สิ่งนี้ทำงานได้คือ /Bounds กับ /Encode (ISO 32000-1 §7.10.4) /Bounds ถือจุดแบ่งภายใน k − 1 จุดที่แบ่ง /Domain ออกเป็น k ช่วงต่อเนื่องกัน ตัวประเมินเลือกช่วงแรกที่ขอบบนของมันมากกว่า input หรือช่วงสุดท้ายเมื่อ input ถึง bound สุดท้ายแล้ว แล้วส่งต่อไปยัง sub-function ของช่วงนั้น จากนั้น /Encode จะแม็ป input จากตำแหน่งของมันภายในช่วงนั้นใหม่ ไปเป็นช่วง input ใดก็ตามที่ sub-function ที่ถูกเลือกคาดหวังเอง โดยทั่วไปคือ [0, 1] ถ้า sub-function เป็นส่วน exponential อีกส่วนหนึ่ง ก่อนที่การประเมินจะดำเนินต่อไปอีกหนึ่งชั้น เข้าไปใน /Domain และ /Range ของ sub-function นั้นเอง

ตัวประเมิน stitching ของ HotPDF เคยจัดการได้แค่ sub-function สองตัวพอดีเท่านั้น และตัวอ่าน /Bounds ของมันต้องการ array แปด element เต็ม ดังนั้นจุดแบ่งจุดเดียวที่ gradient สองส่วนต้องการจริงๆ หนึ่งตัวเลขใน /Bounds จึง parse ไม่ผ่านเสมอ และฟังก์ชันไม่คืนอะไรกลับมาเลย /Encode ไม่ถูกนำมาใช้เลยด้วยซ้ำ เวอร์ชัน 2.376.0 เขียนการเลือกใหม่เป็นการค้นหา k-sub-function แบบทั่วไปตามที่สเปคอธิบาย และเริ่มอ่าน /Bounds เทียบกับความยาวจริงที่ประกาศไว้ ดังนั้น gradient แบบสาม สี่ หรือห้า stop ที่เชื่อมจาก exponential segment จำนวนเท่านั้น ตอนนี้ resolve ได้แบบเดียวกับที่ segment สองตัวเคยอ้างว่าทำได้เสมอ ตัวอย่างด้านล่างสร้าง ramp สองส่วนแบบดำ-ผ่านแดง-ไปขาว ซึ่งเป็นรูปทรงที่axial หรือ radial shadingต้องพึ่งพาทุกครั้งที่เส้นโค้ง exponential เส้นเดียวไม่สามารถรับทุก color stop ที่การออกแบบต้องการได้

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

ฟังก์ชัน PostScript calculator ของ Type 4 ทำอะไรได้ที่ Type 2 กับ 3 ทำไม่ได้

ฟังก์ชัน PDF Type 4 รันโปรแกรมจริง แม้จะถูกจำกัดไว้โดยตั้งใจก็ตาม เป็น PostScript calculator ที่ push input ของมันเข้า operand stack ประมวลผล operator ด้านคณิตศาสตร์, การเปรียบเทียบ, การจัดการ stack และ boolean พร้อมเงื่อนไข if/ifelse แล้วปล่อย output ของมันไว้บน stack เมื่อจบการทำงาน (ISO 32000-1 §7.10.5, Table 42) ไม่มี loop และไม่มีการเก็บตัวแปรแบบมีชื่อ มีแค่ stack เท่านั้น ซึ่งทำให้โปรแกรมที่เป็นไปตามมาตรฐานให้เหตุผลได้ง่าย แต่ภายในชุด operator ที่จำกัดนี้ Type 4 สามารถแสดงออกสิ่งที่ Type 2 และ Type 3 ทำไม่ได้ เช่น สูตรผสมหมึกหลายสีจริงสำหรับ DeviceN separation หรือ halftone spot function ที่มี threshold แบบมีเงื่อนไข ตัวประเมินของ HotPDF คือ HPDFEvalPostScriptCalculator ทำการ tokenize โปรแกรมครั้งเดียว ได้แก่ตัวเลข, operator และ block ของ procedure แบบ { } แล้วเดินผ่าน operand stack ขนาด 100 entry ซึ่งเป็นความลึกที่ ISO 32000-1 §7.10.5 กำหนดไว้ อยู่หลังเพดานแข็ง 50,000 operator ที่ถูกประเมิน เป็นแนวป้องกันย้อนหลังต่อโปรแกรมที่ผิดปกติหรือเขียนด้วยมือ

operator roll: ทิศทางกลับหลังได้ง่ายๆ

roll เป็น operator ที่มีโอกาสสูงสุดที่จะกลับทิศในการเขียนครั้งแรก เพราะลำดับอาร์กิวเมนต์และทิศทางการหมุนของมันทั้งคู่วิ่งสวนทางกับที่ภาษาอังกฤษอธิบายไว้ n j roll pop จำนวนนับ n และปริมาณการหมุน j แล้วเลื่อน stack entry บนสุด n ตัวแบบวนไปทาง j ตำแหน่ง โดยห่อ item ที่หล่นออกจากปลายด้านหนึ่งกลับไปอีกด้านหนึ่ง ตัวอย่างมาตรฐาน จากสเปคโดยตรง คือ a b c 3 1 roll ให้ผลลัพธ์ c a b item บนสุดย้ายไปด้านล่างสุดของกลุ่ม ไม่ใช่ทางกลับกัน และทุก item อื่นเลื่อนขึ้นทีละหนึ่งเพื่อเปิดที่ว่าง ตัวประเมินของ HotPDF คำนวณตำแหน่งใหม่ของ stack entry i เป็น (i + j) mod n ซึ่งตรงกับตัวอย่างนั้นเป๊ะ แต่มันเป็น loop สองบรรทัดที่เขียนกลับทิศการหมุนได้ง่ายพอกัน และ roll ที่กลับด้านยังคงให้สีที่ดูสมเหตุสมผล แค่ไม่ใช่สีที่ผู้เขียนไฟล์ตั้งใจขอ

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round ไม่ใช่ Round ของ Delphi: half-up เทียบกับ banker's rounding

operator round ของ PostScript แก้เศษ .5 ที่เสมอกันโดยปัดไปทางจำนวนเต็มที่ใหญ่กว่าทุกครั้ง ในขณะที่ฟังก์ชัน Round ในตัวของ Delphi ไม่ทำแบบนั้น มันปัดแบบ half-to-even ซึ่งเป็นข้อตกลง banker's-rounding ที่สลับทิศทางว่าเศษ .5 จะปัดไปทางไหน เพื่อไม่ให้การปัดซ้ำๆ สะสม bias ทั้งสองแบบตรงกันเกือบทุกที่และไม่ตรงกันตรงขอบเขตที่สำคัญตรงนี้พอดี Round(0.5) ของ Delphi คืนค่า 0 และ Round(2.5) คืนค่า 2 ในขณะที่ round ตามสเปค PDF ต้องการ 1 และ 3 สำหรับ input เดียวกันนั้น ดังนั้นความไม่ตรงกันนี้จึงซ่อนอยู่ผ่านการทดสอบผิวเผิน แล้วเกิดซ้ำเป็นความคลาดเคลื่อนหนึ่งหน่วยที่สม่ำเสมอทุกครั้งที่การคำนวณกลางทางของโปรแกรม calculator ลงเอยที่ตัวเลขครึ่งจำนวนเต็มพอดี ISO 32000-1 §7.10.5 Table 42 ระบุชัดว่า round ปัดเศษ .5 ไปทางจำนวนเต็มที่ใหญ่กว่า ดังนั้น HotPDF จึง implement operator นี้เป็น Floor(x + 0.5) แทนที่จะเรียก Round ของ Delphi และโค้ดใดก็ตามที่ implement ซ้ำหรือตรวจสอบคณิตศาสตร์ของโปรแกรม Type 4 ด้วยมือ ก็ต้องใช้การแทนที่แบบเดียวกันนี้

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

การตรวจสอบตอนลงทะเบียนดักโปรแกรม calculator ที่เสียได้ตั้งแต่เนิ่นๆ

โปรแกรม Type 4 ที่ผิดรูปแบบตรวจจับได้ถูกในเวลาสร้าง และแพงที่จะตรวจจับที่อื่นใด ดังนั้น RegisterPostScriptFunction จึงไม่ใช่แค่เก็บ source text เท่านั้น มันประเมินโปรแกรมแบบทดลองครั้งหนึ่งที่จุดกึ่งกลางของ /Domain ที่ประกาศไว้ ก่อนที่ function object จะถูกเขียนเข้าเอกสารเลย block { } ที่ไม่สมดุล, operator ที่ไม่รู้จัก, stack underflow หรือจำนวน output ที่ไม่ตรงกับ /Range ทั้งหมดนี้จะล้มเหลวในการรันทดลองนั้นและยก exception ทันที โดย call stack ชี้ไปที่การเรียก RegisterPostScriptFunction แทนที่จะเป็นสิ่งประหลาดในการ render ที่ค้นพบระหว่าง QA บนไฟล์ที่ส่งออกไปแล้ว การทดลองที่จุดกึ่งกลางไม่ได้พิสูจน์ว่าโปรแกรมถูกต้องตลอดทั้ง /Domain ของมัน สาขาแบบมีเงื่อนไขที่ทำงานผิดพลาดเฉพาะใกล้ขอบหนึ่งของช่วง input ก็ยังคงหลุดผ่านจุดตัวอย่างเดียวไปได้ แต่มันปิดกั้นโปรแกรมทั้งกลุ่มที่เสียในเชิงโครงสร้าง ไม่ใช่แค่ผิดในมุมใดมุมหนึ่งเท่านั้น

ที่ gradient และสีสปอตนำฟังก์ชันเหล่านี้ไปใช้งาน

Type 2, 3 และ 4 ไม่ค่อยปรากฏโดดๆ ใน PDF จริง พวกมันปรากฏทุกที่ที่สเปครับ key /Function และผู้ใช้งานที่พบบ่อยที่สุดสองอย่างคือ shading และ tint transform ของสีสปอต operator sh ของ gradient แบบ axial หรือ radial (ISO 32000-1 §8.7.4.5) ประเมิน /Function ของมันครั้งเดียวต่อตำแหน่งตามแกน gradient ซึ่งเป็นกรณี multi-stop ที่ Type 3 stitching มีไว้เพื่อสิ่งนี้พอดี tint transform ของ color space แบบ Separation หรือ DeviceN เป็นบ้านที่พบบ่อยอีกแห่งสำหรับสามชนิดนี้ และเป็นที่ที่ Type 4 แสดงคุณค่าของมัน หมึกสปอตเดี่ยวมักลดทอนเหลือแค่เส้นโค้ง Type 2 หรือ Type 0 แต่การผสม DeviceN ของหมึกหลายสีที่มีพฤติกรรม trapping และ overprint จริง มักต้องการ logic แบบมีเงื่อนไขที่มีแค่ PostScript calculator เท่านั้นที่แสดงออกได้ ซึ่งเป็นกรณีที่ครอบคลุมในบทความเรื่องการ render สีสปอตแบบ Separation และ DeviceN RegisterSeparationFunc เป็นการเรียกคู่กันฝั่งการสร้าง มันรับชื่อ colorant, color space สำรอง และ object ใดๆ ที่ตระกูล Register*Function คืนกลับมา แล้วต่อสาย tint transform นั้นเข้ากับ resource color-space แบบ Separation ที่ส่วนที่เหลือของหน้าเลือกได้ด้วย scn/SCN

เมื่อรวมกัน ตารางตัวอย่างของ Type 0 กับสามชนิดที่ขับเคลื่อนด้วยสูตรเหล่านี้ครอบคลุมทุก /Function ที่ PDF ประกาศได้ และการเลือกชนิดที่ถูกต้องส่วนใหญ่ขึ้นอยู่กับว่าคุณมีอะไรอยู่แล้ว ตาราง lookup ที่คำนวณที่อื่นกลายเป็น Type 0 การผสมสองจุดปลายกลายเป็น Type 2 การผสมหลายจุดที่เชื่อมข้าม domain กลายเป็น Type 3 และอะไรก็ตามที่มี logic แบบมีเงื่อนไขจริงกลายเป็น Type 4 RegisterExponentialFunction, RegisterStitchingFunction และ RegisterPostScriptFunction เป็นส่วนหนึ่งของHotPDF Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder ควบคู่ไปกับ API ด้านฟังก์ชันและ shading ตาม ISO 32000-1 ที่เหลือ