HotPDF เรนเดอร์ฟอนต์ PDF แบบฝังตัวใน Delphi โดยไม่จำเป็นต้องติดตั้งโปรแกรมหรือฟอนต์ใดๆ ลงบนเครื่องคอมพิวเตอร์เลย: โดยไปป์ไลน์การเรนเดอร์กลิฟ (glyph) แบบฝังตัวใน HPDFGlyphRender.pas จะแยกแยะรหัสคำสั่งฟอนต์ที่จัดเก็บไว้ในตัวไฟล์ PDF เอง — ไม่ว่าจะเป็นโครงร่าง glyf ของ TrueType จาก FontFile2, รหัสอักขระ CFF Type 2 จาก FontFile3 และสตรีมเนื้อหากลิฟของ Type 3 — และแสดงผลข้อมูลเหล่านี้ใหม่ในรูปของเส้นทางเวกเตอร์ GDI แบบถมสี บทความนี้เป็นการเจาะลึกความเที่ยงตรงของฟอนต์เบื้องหลังการเรนเดอร์หน้า PDF เป็น TBitmap ด้วย HotPDF: บทความนั้นครอบคลุมการเรนเดอร์โดยรวม ส่วนบทความนี้จะครอบคลุมถึงวิธีที่ข้อความบนหน้าเอกสารเหล่านั้นได้รูปร่างที่ถูกต้องแม่นยำ
ทำไม PDF จึงเรนเดอร์ออกมาเป็นกล่องสี่เหลี่ยมแทนที่จะเป็นข้อความ?
การเกิดกล่องสี่เหลี่ยม, ช่องว่าง หรืออักขระที่ผิดเพี้ยนไปเล็กน้อยในผลลัพธ์ PDF ที่เรนเดอร์ออกมานั้น เกือบจะหมายความว่าตัวเรนเดอร์ร้องขอฟอนต์จากระบบปฏิบัติการแทนที่จะใช้ฟอนต์ที่ฝังตัวอยู่ในตัวไฟล์ ข้อร้องเรียนมักจะส่งเข้ามาในรูปแบบเดียวกันเสมอ: เอกสารแสดงผลได้อย่างสมบูรณ์แบบบนเครื่องคอมพิวเตอร์ที่ใช้สร้างเอกสาร แต่เมื่อเปิดใช้งานบนเซิร์ฟเวอร์ใหม่หรือเดสก์ท็อปที่จำกัดสิทธิ์ ใบแจ้งหนี้ภาษาญี่ปุ่นกลับแสดงกล่องสี่เหลี่ยมเต้าหู้ หรือฟอนต์อื่นที่นำมาทดแทนไปทำให้การขึ้นบรรทัดใหม่เลื่อนตำแหน่งไปหมด ฟอนต์เหล่านั้นไม่เคยอยู่ในเครื่องเครื่องนั้นเลย — มันอยู่ในตัวไฟล์ PDF เท่านั้น — และตัวเรนเดอร์ที่หยุดแค่การหาฟอนต์ระบบมาทดแทนจะไม่สามารถมองเห็นมันได้ ฟอนต์แบบแบ่งเซตย่อย (subset fonts) ยิ่งทำให้แย่ลง: เซตย่อยอาจบรรจุกลิฟเพียง 40 ตัวภายใต้รหัสอักขระที่กำหนดขึ้นเป็นการส่วนตัวเฉพาะสำหรับไฟล์นั้น ซึ่งเป็นรหัสที่ไม่มีฟอนต์ที่ติดตั้งไว้ในเครื่องใช้งานร่วมด้วย
ข้อกำหนด ISO 32000-1 §9.9 นิยามตัวนำส่ง 3 รูปแบบสำหรับรหัสคำสั่งฟอนต์แบบฝังตัวในตัวบ่งชี้ฟอนต์ (font descriptor): FontFile เก็บโปรแกรม Type 1 ดั้งเดิม, FontFile2 เก็บโปรแกรม TrueType และ FontFile3 เก็บ CFF เปล่า (Type1C หรือ CIDFontType0C) หรือตัวห่อหุ้ม OpenType รูปแบบที่สี่คือฟอนต์ Type 3 ตามข้อกำหนด ISO 32000-1 §9.6.5 ซึ่งไม่มีการฝังข้อมูลไบนารีใดๆ เลย — กลิฟแต่ละตัวเป็นเพียงสตรีมเนื้อหา PDF ขนาดเล็กที่ประมวลผล ณ ตำแหน่งที่ตั้ง ตัวนำส่งทั้งสามแบบนี้มีความแตกต่างกันในด้านคณิตศาสตร์โครงร่าง (B-splines กำลังสอง เทียบกับ charstrings กำลังสาม เทียบกับตัวดำเนินการหน้ากระดาษทั่วไป) ดังนั้นตัวเรนเดอร์ที่ซื่อสัตย์จึงต้องการตัวตีความแยกเฉพาะสำหรับแต่ละแบบ ควบคู่ไปกับเลเยอร์การเข้ารหัสที่ทำหน้าที่แปลงรหัสอักขระให้เป็นดัชนีกลิฟที่ถูกต้องก่อนที่จะเข้าถึงโครงร่างใดๆ
HotPDF แปลงโครงร่าง TrueType glyf ให้เป็นเส้นทาง GDI ได้อย่างไร?
คลาส THPDFEmbeddedTTF ใน HPDFGlyphRender.pas จะอ่านตาราง loca เพื่อระบุตำแหน่งของแต่ละบันทึกกลิฟ เดินตามเส้นขอบ glyf ทีละจุด และปล่อยผลลัพธ์เป็นเส้นทาง GDI ข้อตกลง TrueType สองข้อที่ต้องการการจัดการที่ชัดเจน ข้อแรก จุดนอกเส้นโค้ง (off-curve points) ที่ติดต่อกันจะหมายถึงจุดบนเส้นโค้ง (on-curve point) ที่จุดกึ่งกลางของพวกมัน และเส้นขอบที่ทุกจุดอยู่นอกเส้นโค้งจะเริ่มที่จุดกึ่งกลางของจุดสุดท้ายและจุดแรก — หากข้ามกฎข้อใดข้อหนึ่งไป กลิฟที่โค้งมนจะกลายเป็นเหลี่ยมแบนราบหรือพังทลายลง ข้อสอง เส้นโค้ง TrueType เป็นแบบ Béziers กำลังสอง ขณะที่ฟังก์ชัน PolyBezierTo ของ GDI ใช้ระดับกำลังสาม ดังนั้นแต่ละเซกเมนต์กำลังสองจะถูกยกระดับดีกรีอย่างแม่นยำแทนที่จะแบนเป็นเซกเมนต์เส้นตรงธรรมดา
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
การยกระดับดีกรีเป็นแบบไม่สูญเสียคุณภาพ (lossless): เส้นโค้งกำลังสามจะลากเส้นตามเส้นโค้งเดียวกันทุกประการ ดังนั้นโครงร่างที่เรนเดอร์ออกมาจึงตรงกับสิ่งที่โปรแกรมเปิดอ่านที่ได้มาตรฐานวาดขึ้นมาจากตารางเดียวกัน ในระดับการซูมใดๆ ส่วนงานที่เหลือคือการจัดวางพิกัด กลิฟแต่ละตัวเขียนขึ้นในหน่วยฟอนต์ (โดยทั่วไปคือกริด 1000 หรือ 2048 หน่วยต่อ em) และตัวเรนเดอร์จะนำเมทริกซ์การย่อขยาย, เมทริกซ์ข้อความ และเมทริกซ์การแปลงข้อมูลปัจจุบันมาจัดรวมให้เป็นหนึ่งการแปลงกลิฟสู่เครื่องมือก่อนที่จะถมสีเส้นทาง ลำดับขั้นตอนมีความสำคัญมากกว่าที่คิด: หากนำเมทริกซ์ทั้งสามตัวเดียวกันนี้มาจัดลำดับย้อนกลับ กลิฟทั้งหมดจะพังทลายลงไปยังจุดเริ่มต้น — หน้ากระดาษที่ดูผิดเพี้ยนไปซึ่งจริงๆ แล้วเกิดจากบั๊กของพีชคณิตเมทริกซ์เพียงบรรทัดเดียว
ตัวตีความรหัสอักขระ Type 2 จัดการกับฟอนต์ CFF อย่างไร
คลาส THPDFEmbeddedCFF มอบตัวตีความรหัสอักขระ Type 2 ที่แท้จริงให้กับโปรแกรม FontFile3: โดยจะแยกวิเคราะห์โครงสร้าง CFF INDEX, Top DICT และ Private DICT จากนั้นจึงรันรหัสอักขระแต่ละอันและสร้างเซกเมนต์เส้นทางตรงไปยัง GDI ตัวห่อหุ้ม OpenType (คอนเทนเนอร์ OTTO) จะถูกแกะออกก่อนเพื่อเข้าถึงตาราง CFF เปล่า; สตรีม CIDFontType0C และ Type1C เปล่าๆ จะถูกนำไปใช้งานโดยตรง รหัสอักขระเป็นภาษาสแต็กที่มีความกระชับ และข้อตกลง 3 ข้อของมันจะช่วยตัดสินว่าตัวตีความยังคงซิงค์กับสตรีมไบต์ได้ดีหรือไม่: ส่วนนำหน้าความกว้างที่เป็นตัวเลือกเสริมหมายความว่าตัวดำเนินการล้างสแต็กอันแรกอาจนำตัวถูกดำเนินการนำหน้าพิเศษมาด้วยหนึ่งตัว ตัวดำเนินการ hintmask จะบ่งบอกถึง vstemhm เมื่อตัวถูกดำเนินการยังคงอยู่บนสแต็ก และจำนวนไบต์ของมาสก์ที่จะข้ามไปจะขึ้นอยู่กับจำนวน stem สะสม — หากนับจำนวนพลาดไปเพียงครั้งเดียว รหัสคำสั่งถัดๆ ไปทั้งหมดจะถูกตีความผิดพลาด และการเรียกใช้รูทีนย่อย (subroutine calls) จะเพิ่มค่าไบแอสให้กับดัชนีของมัน (เป็น 107, 1131 หรือ 32768 ขึ้นอยู่กับจำนวนรูทีนย่อย) ก่อนการค้นหา ดังนั้นการเรียกที่ไม่มีไบแอสจะตกไปยังรูทีนย่อยที่ผิดทันที
CFF แบบระบุคีย์ CID (CID-keyed CFF) จะเพิ่มการอ้างอิงทางอ้อมอีกชั้นหนึ่งซึ่งมักจะทำให้กระบวนการที่ขาดความรัดกุมล้มเหลว: รหัสอักขระจะเลือก CID แต่ดัชนีรหัสอักขระคือ GID และตารางชุดอักขระของฟอนต์จะจับคู่ GID ไปยัง CID — ดังนัันตัวเรนเดอร์จะสร้างแผนผังย้อนกลับจาก CID ไปยัง GID ก่อนที่จะวาด และเลือกใช้ Private DICT สำหรับแต่ละกลิฟผ่าน FDSelect สำหรับฟอนต์ที่ระบุคีย์ไว้หลายแบบ ส่วนโปรแกรม Type1C แบบระบุคีย์ชื่อ ซึ่งมักใช้กับฟอนต์ Type 1 ทั่วไป จะแก้รหัสขนาด 1 ไบต์ผ่านการเข้ารหัสในตัวของโปรแกรม CFF หรือผ่านกลไกการเข้ารหัสระดับ PDF ที่จะพูดถึงถัดไป ข้อจำกัดตามจริงอย่างหนึ่งคือ: ตัวตีความจะอ่านตัวดำเนินการบอกใบ้ (hint operators) เพื่อให้สตรีมซิงค์ตรงกันเท่านั้น แต่ไม่ได้รันการคำนวณการบอกใบ้ (hinting) ซึ่งขอบเขตนี้จะคุยกันในช่วงท้าย
ฟอนต์ Type 3 คืออะไรและวาดขึ้นมาอย่างไร?
กลิฟของ Type 3 ไม่ใช่โครงร่างเลย — ข้อกำหนด ISO 32000-1 §9.6.5 นิยามมันไว้ว่าเป็นสตรีมเนื้อหา ดังนั้น HotPDF จึงเรนเดอร์มันโดยการส่งผ่านสถานะกราฟิก จัดวางเมทริกซ์ฟอนต์, ขนาดฟอนต์ และเมทริกซ์ข้อความลงบน CTM และรันขั้นตอนการวาดกลิฟผ่านตัวตีความตัวดำเนินการเดียวกันกับที่ใช้วาดหน้ากระดาษ โดยมี /Resources ของตัวฟอนต์เองอยู่ในขอบเขตใช้งาน รายละเอียดในสเปก 2 ประการที่มีความสำคัญต่อความถูกต้อง ได้แก่: ค่า /Widths ของ Type 3 จะถูกแสดงออกในรูปของพื้นที่กลิฟ (glyph space) มากกว่าพื้นที่ข้อความขนาด 1/1000 ที่ฟอนต์ประเภทอื่นใช้งาน ดังนั้นการก้าวหน้าของตำแหน่งจึงต้องผ่าน /FontMatrix — มิฉะนั้นฟอนต์บาร์โค้ดที่มีเมทริกซ์ 0.01 จะเคลื่อนตำแหน่งผิดพลาดไปอย่างมหาศาล และกระบวนการของกลิฟที่เปิดใช้งานด้วยตัวดำเนินการ d1 จะมีข้อตกลงสองประการที่ตัวเรนเดอร์จะบังคับใช้: การวาดจะถูกจำกัดขอบเขตไว้เฉพาะในกล่องขอบเขต (bounding box) ที่ประกาศไว้ และตามข้อกำหนด ISO 32000-1 §9.6.5.2 กลิฟจะละเว้นตัวดำเนินการระบุสีของตัวเอง และใช้วาดด้วยสีเติมปัจจุบันของผู้เรียกใช้ ดังนั้นตัวกำหนดสี rg, g, k และตัวคู่กันสำหรับระบายเส้นภายในกระบวนการจะถูกระงับชั่วคราวในระหว่างการวาดกลิฟนั้น หากละเลยกฎเรื่องสี ฟอนต์บาร์โค้ด d1 ที่ประทับไว้เป็นสีน้ำเงินในหน้าเอกสารจะกลายเป็นสีดำ; หากละเลยการจำกัดขอบเขต กลิฟที่ผิดเพี้ยนจะไปวาดทับนอกพื้นที่ของมัน
รหัสอักขระแปลงเป็นรหัสกลิฟ (Glyph IDs) ได้อย่างไร
ตัวตีความโครงร่างเป็นเพียงงานครึ่งเดียว เนื่องจากไบต์ต่างๆ ในสตริงข้อความของ PDF คือรหัสอักขระ ไม่ใช่ดัชนีกลิฟ และข้อกำหนด ISO 32000-1 ได้ระบุถึงการจับคู่ไว้ในสองหัวข้อย่อย สำหรับฟอนต์ทั่วไป ข้อกำหนด §9.6.6 ระบุลำดับความสำคัญไว้อย่างเคร่งครัด: อาร์เรย์ /Differences จะมีความสำคัญเหนือกว่าการเข้ารหัสพื้นฐาน (WinAnsiEncoding, MacRomanEncoding หรือ StandardEncoding) ซึ่งจะมีความสำคัญเหนือกว่าแผนผังในตัวของโปรแกรมฟอนต์เอง HotPDF จะแปลงสายสัมพันธ์นี้ให้เป็นตารางรหัสสู่ GID ขนาด 256 รายการ โดยแปลงชื่อกลิฟเป็นดัชนีกลิฟด้วย 3 เส้นทาง: การจับคู่ชุดอักขระที่ตรงกันอย่างสมบูรณ์ภายในโปรแกรม CFF, ชื่อที่เป็นตัวเลข gNN/glyphNN ซึ่งถือเป็นดัชนีข้อความดิบ และการแปลงชื่อเป็น Unicode ของ Adobe Glyph List ตามด้วยการค้นหา cmap สำหรับโปรแกรม TrueType สำหรับฟอนต์คอมโพสิต ข้อกำหนด §9.7 กำหนดให้ CIDToGIDMap ทำหน้าที่: โดยกรณีทั่วไปคือ /Identity แต่รายการอาจเป็นสตรีมของคู่ค่าบิ๊กเอนเดียน (big-endian pairs) ที่มีดัชนีเป็น CID — และผลลัพธ์ Unicode ของ HotPDF เองก็ใช้งานรูปแบบสตรีมดังกล่าวสำหรับเซตย่อยขนาดกะทัดรัด ดังนั้นเส้นทางสตรีมจึงไม่ใช่เรื่องแปลกประหลาดอะไร
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
เมื่อต้องการค้นหา cmap ของ TrueType ทาง HotPDF จะเดินทางตามแผนสำรองแทนที่จะเชื่อถือตารางย่อยเพียงตารางเดียว: โดยเริ่มจากตารางย่อย Windows Unicode (รูปแบบ 4 จากนั้นรูปแบบ 12 สำหรับระนาบเสริม) เป็นอันดับแรก ตามด้วยตารางย่อยสัญลักษณ์ (3,0) ร่วมกับข้อตกลงพื้นที่การใช้งานส่วนตัว F000 ที่สะท้อนมาไปยังไบต์ต่ำ — ซึ่งเป็นสาเหตุที่ฟอนต์สัญลักษณ์อย่าง Wingdings สามารถรองรับรหัส ASCII ทั่วไปได้ — จากนั้นจึงเป็นรูปแบบเก่าคือรูปแบบ 6 และ 0 ส่วนตารางย่อยรูปแบบ 2 จะไม่ถูกนำมาตีความโดยเจตนา: พวกมันจับคู่รหัสรหัสหลายไบต์ในอดีตเช่น Shift-JIS และ Big5 ไม่ใช่ Unicode และฟอนต์ CJK สมัยใหม่มักจะมาพร้อมกับตารางรูปแบบ 4 หรือรูปแบบ 12 อยู่แล้ว รหัสใดๆ ที่ไม่สามารถผ่านเส้นทางเหล่านี้ได้เลยจะถอยกลับไปใช้การวาด GDI สำหรับกลิฟนั้นเพียงตัวเดียว ดังนั้นอักขระตัวเดียวที่จับคู่ไม่ได้จะส่งผลต่อกลิฟตัวเดียวเท่านั้น ไม่ได้ทำให้ข้อความทั้งแถวเสียหายไปทั้งหมด
สิ่งที่คุณสมบัติการฝังตัวไม่ได้ทำ
ข้อจำกัดที่ควรระบุให้ชัดเจน: จะไม่มีการคำนวณการบอกใบ้ (Hinting) — โครงร่างฟอนต์จะถมสีตามที่ระบุไว้ในฟอนต์ ซึ่งในระดับ 150 DPI ขึ้นไปจะไม่มีความแตกต่างจากผลลัพธ์ที่มีการบอกใบ้ แต่อาจมีพิกเซลที่ต่างไปเล็กน้อยจากโปรแกรมเรนเดอร์ที่มีการบอกใบ้ในฟอนต์ขนาดเล็กมากๆ โปรแกรม Type 1 ดั้งเดิมใน FontFile (รหัสอักขระที่เข้ารหัสแบบ eexec) จะไม่ถูกตีความ และจะไม่ใช้งานคุณสมบัติของฟอนต์แปรผัน (variable-font axes) ของ OpenType; ทั้งสองกรณีนี้ รวมถึงกรณีไฟล์โปรแกรมฟอนต์เสียหาย หรือตาราง glyf ที่ไม่มี cmap ที่พร้อมใช้งาน จะถอยกลับไปใช้การวาดฟอนต์ระบบแทนที่จะทำให้การแสดงผลหน้าเอกสารล้มเหลวไป แนวทางที่คำนึงถึงความเที่ยงตรงเป็นอันดับแรกนี้ยังใช้กับส่วนอื่นๆ ของตัวเรนเดอร์ — เช่น รูปแบบการไล่โทนสีตามแนวแกนและรัศมี จะได้รับการจัดการแบบเดียวกับการไล่ระดับสีที่เหมาะสม — และฝั่งสร้างเอกสารก็มีพฤติกรรมฟอนต์เฉพาะตัวตามที่อธิบายไว้ในวิธีที่ EndDoc เรียงลำดับส่วนย่อย of ฟอนต์
การใช้งานระบบท่อส่งข้อมูลนี้ไม่ต้องการโค้ดระบุฟอนต์ใดๆ เลย — ทุกกลิกรอบข้างที่อธิบายไว้ด้านบนจะทำงานโดยอัตโนมัติภายในการเรียกใช้การเรนเดอร์หน้ากระดาษ
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
ผลลัพธ์ในเชิงปฏิบัติคือสิ่งที่ทีมสนับสนุนของคุญให้ความสำคัญ: ไฟล์ PDF ที่มีฟอนต์ในตัวจะเรนเดอร์ด้วยฟอนต์เหล่านั้น ทั้งบนเซิร์ฟเวอร์คอมไพล์, ใน Windows คอนเทนเนอร์ หรือบนเครื่องลูกค้าที่ไม่เคยติดตั้งฟอนต์นั้นมาก่อน ไปป์ไลน์การเรนเดอร์กลิฟแบบฝังตัวจัดส่งมาเป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder — ซึ่งเป็นไลบรารี VCL แบบเนทีฟที่ครอบคลุมการสร้าง, การแก้ไข, การดึงข้อความ และการเรนเดอร์หน้า PDF โดยไม่ต้องพึ่งพาไฟล์ DLL ภายนอกใดๆ