ข้ามไปยังเนื้อหา

ฟอนต์: ส่วนที่ยาก

Evidence: Mixed evidence

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

“มันดูปกติดี” คือประโยคที่อันตรายที่สุดในงาน PDF และฟอนต์คือจุดที่ประโยคนี้สร้างความเสียหายได้มากที่สุด มีสามเรื่องที่เป็นอิสระต่อกันและต้องถูกต้องพร้อมกัน:

  1. การฝัง — โปรแกรมฟอนต์ไปพร้อมกับไฟล์ จึงเรนเดอร์ออกมาเหมือนกันบนเครื่องที่ไม่ได้ติดตั้งฟอนต์นั้น
  2. การทำ subset — มีเฉพาะ glyph ที่ถูกใช้งานจริงเท่านั้นที่ถูกนำไปด้วย ดังนั้นฟอนต์ CJK ขนาด 20 MB จึงไม่ทำให้ทุกเอกสารมีขนาดบวมขึ้น
  3. การเข้ารหัส — มีการแมปที่ถูกต้องจากรหัสอักขระบนหน้ากลับไปยัง Unicode ดังนั้นข้อความจึงสามารถค้นหา คัดลอก ทำดัชนี และอ่านด้วยเทคโนโลยีสิ่งอำนวยความสะดวกได้

การเรนเดอร์เชิงภาพพิสูจน์ข้อแรกได้เพียงบางส่วนเท่านั้น เอกสารหนึ่งสามารถแสดง glyph ได้สมบูรณ์แบบแต่ยังล้มเหลวในข้อที่สามโดยสิ้นเชิง — ข้อความเป็นเพียงภาพของถ้อยคำ ไม่ใช่ถ้อยคำจริง นี่คือความล้มเหลวที่ผ่านการตรวจทานแบบ “ดูปกติดี” ทุกครั้ง แล้วจึงล้มเหลวในการตรวจสอบการปฏิบัติตามข้อกำหนดหรือคำร้องขอเอกสารทางกฎหมาย

  • ฟอนต์ใน PDF โดยทั่วไปคือ dictionary รวมกับสตรีม โปรแกรมฟอนต์ที่ฝังไว้
  • การทำ subset เขียนโปรแกรมนั้นใหม่ให้มีเฉพาะ glyph ที่ถูกใช้งานเท่านั้น ชื่อของฟอนต์ subset จะได้รับ แท็กตัวพิมพ์ใหญ่หกตัวและเครื่องหมาย + เพื่อให้โปรแกรมอ่านถือว่าเป็นฟอนต์คนละตัว
  • การเข้ารหัส คือปัญหาอีกส่วนหนึ่งของการแมปรหัสอักขระไปยัง Unicode CMap แบบ /ToUnicode คือสิ่งที่ทำให้ข้อความค้นหาและคัดลอกได้ — และเป็น อิสระ จากการที่ glyph จะดูถูกต้องหรือไม่
  • ข้อความที่ดูถูกต้องแต่ไม่มี /ToUnicode (หรือมีแต่ผิด) คือความล้มเหลวแบบเงียบงันคลาสสิก: สมบูรณ์แบบบนหน้าจอ แต่ค้นหาไม่ได้ในทางปฏิบัติ
  • NextPDF ทำ subset ฟอนต์ TrueType คงเอกลักษณ์ของ glyph ไว้เพื่อให้เรนเดอร์ถูกต้อง และสร้าง CMap แบบ /ToUnicode เพื่อให้การแยกข้อความทำงานได้ — พร้อมทั้งสามารถ บังคับใช้ กฎการฝังของ PDF 2.0 แทนที่จะเพียงแค่เตือน

การทำ subset FontSubsetter (src/Typography/FontSubsetter.php) แจกแจง table directory ของ TrueType ต้นฉบับและอ่าน cmap เพื่อแมป codepoint ของ Unicode ไปยัง glyph ID โดยรองรับทั้ง BMP รูปแบบ 4 และ full-Unicode รูปแบบ 12 ซึ่ง CJK ต้องใช้ จากนั้นจึงทำขั้นตอนที่ subsetter ทั่วไปมักมองข้าม: การแก้ปัญหา การพึ่งพาของ composite glyph ด้วยการหา transitive closure glyph ที่มีเครื่องหมายกำกับเสียงและสร้างจากตัวอักษรฐานบวกกับ combining mark จะอ้างอิงไปยัง glyph อื่นๆในฐานะองค์ประกอบ หากองค์ประกอบเหล่านั้นถูกตัดทิ้ง glyph จะเรนเดอร์ออกมาผิด subsetter จะเดินไปตามกราฟนั้นจนกว่าจะไม่มีองค์ประกอบใหม่ปรากฏ พร้อมมีกลไกป้องกันวงวนเพื่อไม่ให้ฟอนต์ที่ผิดรูปแบบวนซ้ำไม่รู้จบ

การตัดสินใจเชิงวิศวกรรมสองข้อในไฟล์นั้นควรกล่าวถึง ข้อแรก glyph ID จะถูก คงไว้ ไม่รีแมป — ช่องที่ไม่ได้ใช้จะถูกเติมด้วยศูนย์ใน glyf/loca เพื่อให้ดัชนี glyph ต้นฉบับของ content stream ยังคงใช้ได้ภายใต้ CIDToGIDMap /Identity การรีแมปจะได้ขนาดเล็กกว่า แต่ต้องเขียนการอ้างอิง glyph ทุกรายการใหม่ การคงเอกลักษณ์ไว้นั้นถูกต้องโดยการออกแบบ ข้อสอง การเดินสำรวจเป็นแบบ เรียงลำดับ (gid จากน้อยไปมาก) ดังนั้น subset จึงเป็นแบบ byte-deterministic — ฟอนต์เดียวกันและ glyph ที่ใช้ชุดเดียวกันจะให้ไบต์ของ subset เหมือนกัน ซึ่งเป็นสิ่งที่ reproducible build ต้องการ หากการทำ subset ช่วยลดขนาดไฟล์ได้น้อยกว่า ~10% ระบบจะคืนค่าต้นฉบับโดยไม่เปลี่ยนแปลง ภาระงานที่เพิ่มขึ้นไม่คุ้มกับผลที่ได้เพียงเล็กน้อย

การฝัง นโยบายที่ชัดเจนเป็นตัวตัดสินว่าจะนำโปรแกรมฟอนต์ไปด้วยหรือไม่ — ไม่ใช่การคาดเดา Pdf20FontEmbeddingPolicy (src/Writer/Pdf20FontEmbeddingPolicy.php) มีสองโหมด ภายใต้โปรไฟล์ PDF 2.0 Strict จะปฏิเสธการอ้างอิงฟอนต์มาตรฐาน Type 1 (“Base14”) ที่ไม่ได้ฝัง โดยส่งข้อยกเว้นแบบมีชนิด — ซึ่งเป็นพฤติกรรมที่ถูกต้องตามข้อกำหนด AllowBase14 คงเส้นทางการแจ้งเตือนเชิงคำแนะนำแบบเดิมไว้ ในช่วงย้ายระบบ จะสร้าง font descriptor ขั้นต่ำที่มาตรฐานยังคงกำหนดไว้ และส่งคำเตือนแทนการ throw ผู้เรียกใช้เป็นผู้กำหนดตัวเลือกนี้อย่างชัดเจนบนเอกสาร ตัวเลือกนี้ไม่เคยถูกอนุมานจากฟอนต์

การเข้ารหัส สำหรับฟอนต์แบบ composite (Type 0) EmbeddedTtfFontDictBuilder (src/Writer/EmbeddedTtfFontDictBuilder.php) จะสร้างฟอนต์ลูกแบบ CIDFontType2 ฟอนต์แม่แบบ Type0 และสตรีม CMap แบบ /ToUnicode เพื่อให้รหัสอักขระแปลงกลับไปเป็น Unicode ได้ สตรีม /ToUnicode สามารถละไว้ได้อย่างถูกต้องในกรณีเดียวเท่านั้น: เมื่อ CMap แบบ predefined ของ CJK ที่อธิบายตนเองได้ให้การแมปอักขระไปยัง Unicode แก่โปรแกรมอ่านอยู่แล้ว ในกรณีนั้น CMap คือ การเข้ารหัส ดังนั้นโปรไฟล์ธรรมดาจึงละสตรีม /ToUnicode ที่ซ้ำซ้อนออกไปเพื่อประหยัดไบต์ นอกกรณีนั้น สตรีม /ToUnicode คือสิ่งที่ทำให้ข้อความยังคงเป็นข้อความ

ประเด็นสิ่งที่รับประกันสิ่งที่ ไม่ รับประกันความล้มเหลวแบบเงียบงันหากผิดพลาด
การฝังเรนเดอร์เหมือนกันโดยไม่ต้องติดตั้งฟอนต์ว่าข้อความจะค้นหาได้ฟอนต์ถูกแทนที่ เมตริกผิดพลาดบนเครื่องอื่น
การทำ subsetไฟล์ขนาดเล็ก มีเฉพาะ glyph ที่ใช้งานสิ่งใดๆที่เกี่ยวกับการเข้ารหัสองค์ประกอบ composite ขาดหาย → glyph ที่มีเครื่องหมายกำกับเสียงเสียหาย
การเข้ารหัส (/ToUnicode)ข้อความที่ค้นหาได้ คัดลอกได้ และเข้าถึงได้ว่า glyph จะเรนเดอร์ได้ถูกต้องหน้าที่ดูสมบูรณ์แบบ แต่ค้นหาไม่ได้ / ข้อความเพี้ยนเมื่อคัดลอก

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

กฎการตั้งชื่อ subset เป็นข้อกำหนดเชิงบรรทัดฐานและต้องตรงตามรูปแบบ Spec: ISO 32000-2, §9.9.2 กำหนดว่าชื่อ PostScript ของฟอนต์ subset — ได้แก่ BaseFont และ FontName ของ descriptor — ต้องขึ้นต้นด้วยแท็กที่เป็น ตัวพิมพ์ใหญ่หกตัวพอดี ตามด้วยเครื่องหมายบวก แล้วจึงตามด้วยชื่อ PostScript ของฟอนต์ต้นฉบับ นอกจากนี้ยังกำหนดว่า subset ที่ต่างกัน ของฟอนต์เดียวกันในไฟล์เดียวต้องใช้แท็กที่ต่างกัน กฎนั้นคือสิ่งที่ ทำให้โปรแกรมอ่านแยกแยะ subset สองชุดออกจากกันและรวมเอกสารได้อย่างถูกต้อง Evidence: Standard-backed

การเข้ารหัสเป็นข้อกำหนดที่แยกต่างหากจากการเรนเดอร์ Spec: ISO 32000-2, §9.10.3 นิยาม /ToUnicode ว่าเป็นสตรีมที่บรรจุ CMap ซึ่งแมปรหัสอักขระไปยังค่า Unicode และกระบวนการแยกข้อความใน Spec: ISO 32000-2, §9.10.2 ใช้ CMap นั้นเพื่อ แปลงรหัสอักขระไปเป็น Unicode สำหรับการค้นหาและการทำดัชนี กลไก การวาด glyph ไม่ได้แตะต้อง /ToUnicode — ซึ่งเป็นเหตุผลที่ ข้อความสามารถดูถูกต้องแต่แยกออกมาผิดได้

ในเรื่องการฝัง มาตรฐานระบุว่า font dictionary ส่วนใหญ่มี font descriptor พร้อมสตรีมไฟล์ฟอนต์ฝังไว้ ซึ่งเป็น ตัวเลือกแต่แนะนำอย่างยิ่ง PDF 2.0 เข้มงวดเรื่องนี้มากขึ้นโดยเฉพาะสำหรับฟอนต์มาตรฐาน Type 1 ทั้งสิบสี่ตัว นโยบาย Strict ของ NextPDF คือการตีความที่ถูกต้องตามข้อกำหนดที่เข้มงวดขึ้นนั้น AllowBase14 คือทางออกเพื่อความเข้ากันได้ย้อนหลังที่ชัดเจนและต้องเลือกใช้เอง — เอนจินจะไม่ลดระดับลงอย่างเงียบๆ

Strict PDF 2.0 font-embedding enforcement — edition availability
Edition Availability
Core

พร้อมใช้งาน การทำ subset การสร้าง /ToUnicode และนโยบายการฝังแบบ Strict / AllowBase14 ที่ชัดเจน คือพฤติกรรมหลักของเอนจิน

Pro

เพิ่มการบังคับใช้ข้อกำหนดที่ลึกขึ้นและการรายงานเกี่ยวกับการฝังฟอนต์ ที่ระดับโปรไฟล์

Enterprise

เพิ่มการบังคับใช้ข้อกำหนดแบบเดียวกันภายใต้พื้นผิวการดำเนินงาน ระดับองค์กร

ต่อไปนี้คือสองส่วนของฟอนต์ composite ที่ฝัง ทำ subset และค้นหาได้อย่างถูกต้อง แท็ก subset เป็นไปตามกฎตัวอักษรหกตัวของมาตรฐาน การอ้างอิง /ToUnicode ทำให้ข้อความยังคงแยกออกมาได้

% The Type 0 (composite) font dictionary
20 0 obj
<< /Type /Font /Subtype /Type0
/BaseFont /ABCDEF+NotoSans % six-letter subset tag + '+'
/Encoding /Identity-H
/DescendantFonts [21 0 R]
/ToUnicode 23 0 R >> % the map that makes text searchable
endobj
% The descendant CIDFontType2 (carries the subsetted program)
21 0 obj
<< /Type /Font /Subtype /CIDFontType2
/BaseFont /ABCDEF+NotoSans
/CIDToGIDMap /Identity % glyph IDs preserved, not remapped
/FontDescriptor 22 0 R >>
endobj

ออบเจกต์ 20 ที่มี /ToUnicode 23 0 R คือความแตกต่างระหว่างเอกสารที่ค้นหาได้กับภาพถ่ายของเอกสารนั้น หากตัดส่วนนี้ออก (นอกเหนือจากกรณี predefined-CMap) glyph ทุกตัวก็ยังวาดออกมาได้สมบูรณ์แบบ แต่การค้นหาคำใดๆบนหน้านั้นจะไม่พบสิ่งใดเลย

กล่าวให้ตรงที่สุด: การที่ glyph เรนเดอร์ได้ถูกต้องไม่ได้บอกอะไรเลยว่าข้อความนั้นเป็นข้อความหรือไม่ การเรนเดอร์เป็นไปตามเส้นทางจากการเข้ารหัสไปยัง glyph การค้นหาและการคัดลอกเป็นไปตามเส้นทางจากรหัสไปยัง Unicode (/ToUnicode) ทั้งสองเป็นกลไกที่แตกต่างกันซึ่งอ่านคนละส่วนของ font dictionary ดังนั้นเอกสารหนึ่งจึงมีผลลัพธ์เชิงภาพที่ไร้ที่ติได้ แม้จะขาด /ToUnicode หรือมีแต่ผิด ผลลัพธ์คือหน้าที่ดูน่าเชื่อถือแต่ในทางการใช้งานกลับค้นหาไม่ได้ — ความล้มเหลวที่รอดพ้นจากการตรวจทานเชิงภาพทุกครั้ง เพราะโดยนิยามแล้วไม่มีสิ่งใดให้มองเห็น

กับดักที่เกี่ยวข้อง: การสันนิษฐานว่า “ฟอนต์ถูกฝังไว้แล้ว ดังนั้นการจัดเก็บถาวรจึงไม่มีปัญหา” การฝังเป็นสิ่งจำเป็นแต่ไม่เพียงพอ โปรไฟล์อย่าง PDF/A ยังคาดหวังให้ subset ถูกตั้งชื่อตามกฎตัวอักษรหกตัวและมีการเข้ารหัสที่ถูกต้องด้วย ฝังแล้วแต่ค้นหาไม่ได้ก็ยังถือว่าล้มเหลว

subsetter ของ NextPDF เป็น subsetter สำหรับ TrueType โดยเฉพาะ โดยต้องใช้ table ที่จำเป็นของ TrueType และจะคืนค่าฟอนต์ต้นฉบับโดยไม่เปลี่ยนแปลงเมื่อ table เหล่านั้นขาดหายหรือผลที่ได้ต่ำกว่าเกณฑ์ ~10% การทำ subset และ CMap แบบ /ToUnicode ทำให้ข้อความ แยกออกมาได้ แต่ไม่สามารถกอบกู้ฟอนต์ต้นทางที่ขาดข้อมูลในการแมป glyph กลับไปยังอักขระที่มีความหมายได้ ในที่ที่ไม่สามารถระบุค่า Unicode ได้ การสร้าง CMap ไม่ว่าจะทำมากเพียงใดก็ไม่สามารถสร้างค่านั้นขึ้นมาได้

หน้านี้ว่าด้วยการสร้างโครงสร้างฟอนต์ที่ถูกต้องในเอกสารที่ NextPDF เขียน ไม่ใช่เครื่องมือซ่อมฟอนต์สำหรับ PDF ขาเข้าใดๆ และการสร้าง subset กับการเข้ารหัสที่เป็นไปตามข้อกำหนดนั้นไม่สามารถรับรองเอกสารตามโปรไฟล์การจัดเก็บถาวรฉบับเต็มได้ด้วยตัวมันเอง — นั่นเป็นการตรวจสอบที่แยกต่างหากและกว้างกว่า

ทำไมต้องใช้แท็กตัวอักษรหกตัว — ทำไมไม่ใช้ชื่อฟอนต์? เพื่อให้โปรแกรมอ่านสามารถแยกแยะ subset ที่ต่างกันสองชุดของฟอนต์เดียวกัน และรวมเอกสารโดยไม่ทำให้ชุด glyph ของแต่ละชุดชนกัน subset ต่างกัน แท็กก็ต่างกัน ตามกฎ

เมื่อใดจึงยอมรับได้ที่จะไม่มี /ToUnicode? เมื่อ CMap แบบ predefined ของ CJK ที่อธิบายตนเองได้ให้การแมปอักขระไปยัง Unicode ไว้แล้ว ในกรณีนั้น CMap คือการเข้ารหัส /ToUnicode ที่แยกต่างหากจะซ้ำซ้อน นอกเหนือจากนั้น การไม่มี /ToUnicode ถือเป็นข้อบกพร่อง

การทำ subset ก่อให้เกิดผลเสียได้หรือไม่? เฉพาะเมื่อทำผิดวิธีเท่านั้น การตัดองค์ประกอบของ composite glyph ทิ้งทำให้ glyph ที่มีเครื่องหมายกำกับเสียงเสียหาย การรีแมป glyph ID โดยไม่เขียนการอ้างอิงใหม่ทำให้การเรนเดอร์เสียหาย NextPDF หลีกเลี่ยงทั้งสองอย่างด้วยการแก้ปัญหา component closure และการคงเอกลักษณ์ของ glyph ไว้

  • Embedded font program — ไฟล์ฟอนต์จริง (TrueType/CFF/Type 1) ที่ถูกฝังไว้ภายใน PDF ในรูปของสตรีม ดังนั้นการเรนเดอร์จึงไม่ขึ้นกับฟอนต์ที่ติดตั้งในโปรแกรมอ่าน
  • Subsetting — การเขียนโปรแกรมฟอนต์ใหม่ให้มีเฉพาะ glyph ที่เอกสารใช้งานเท่านั้น เพื่อลดขนาด
  • Subset tag — คำนำหน้าบังคับที่เป็นตัวพิมพ์ใหญ่หกตัว บวกกับ + บนชื่อของฟอนต์ subset (ตัวอย่างเช่น ABCDEF+NotoSans)
  • /ToUnicode — สตรีม CMap ที่แมปรหัสอักขระไปยังค่า Unicode เป็นสิ่งที่ทำให้ข้อความ PDF ค้นหาได้ คัดลอกได้ และเข้าถึงได้
  • Composite glyph — glyph ที่สร้างขึ้นโดยอ้างอิงไปยัง glyph อื่นๆในฐานะองค์ประกอบ องค์ประกอบเหล่านั้นต้องถูกเก็บไว้เมื่อทำ subset
  • CIDToGIDMap /Identity — โหมดที่ดัชนี glyph ของ content stream คือ glyph ID ของฟอนต์เองโดยไม่เปลี่ยนแปลง NextPDF คงเอกลักษณ์ของ glyph ไว้เพื่อให้สิ่งนี้ยังคงใช้ได้
  • Base14 — ฟอนต์มาตรฐาน Type 1 ทั้งสิบสี่ตัว PDF 2.0 คาดหวังให้ฟอนต์ถูกฝังไว้แทนที่จะอ้างอิงด้วยชื่อ