นักออกแบบเลือกฟอนต์ที่มีตัว a แบบ single-story สำหรับหัวข้อ หรือเลขศูนย์ที่มีเส้นขีดฆ่าสำหรับตาราง หรือชุดตัวพิมพ์ใหญ่แบบ swash สำหรับหน้าปก glyph เหล่านั้นอยู่ในฟอนต์แล้ว เพียงแต่ไม่ใช่ค่าเริ่มต้น a ค่าเริ่มต้นจาก cmap table ไปยัง glyph หนึ่ง และ alternate อยู่ห่างออกไปไม่กี่ glyph id เข้าถึงได้ผ่าน substitution rule เท่านั้น การสร้าง alternate นั้นใน PDF หมายถึงการอ่าน rule และส่งออก substitute glyph ใน content stream บทความนี้เกี่ยวกับการอ่าน rule เหล่านั้นประเภท single-substitution ใน Object Pascal โดยไม่มี native shaping library ข้างใต้
ขอบเขตนั้นแคบโดยตั้งใจ Stylistic sets และ alternates คือ substitutions แบบ single-glyph-in, single-glyph-out ซึ่งเป็นส่วนหนึ่งของ OpenType layout ที่แก้ไขได้ด้วยการเดินตาราง deterministic ขนาดเล็ก ทำให้เหมาะกับ Pascal engine ที่ต้องการอยู่ห่างจาก C dependencies
เหตุใดจึงเป็น pure Delphi แทน HarfBuzz
HarfBuzz คือคำตอบที่ชัดเจนสำหรับ "shape ข้อความนี้" และสำหรับการ shaping แบบ bidirectional, Indic หรือ Arabic แบบสมบูรณ์ มันคือคำตอบที่ถูกต้อง แต่มันก็เป็น C library การผูกมันเข้ากับผลิตภัณฑ์ Delphi หรือ C++Builder หมายถึงการ ship native object สำหรับทุก target platform และ architecture จับคู่ calling convention ติดตาม release cadence และอ่านเงื่อนไขสัญญาอนุญาตของมันเทียบกับของคุณเอง ทั้งหมดนั้นไม่ยากในตัวมันเอง แต่ทั้งหมดคือแรงเสียดทานที่ไม่หายไปและไม่ได้ซื้ออะไรเมื่อความต้องการจริง ๆ คือ "ให้ฉันรูปแบบ ss01 ของตัวอักษรนี้"
Single substitution ไม่ต้องการ shaping engine มันต้องการ parser สำหรับ GSUB subtable formats เพียงไม่กี่รูปแบบและการค้นหาแบบ binary search สองสามครั้ง การเขียนใน Pascal ทำให้ toolchain ทั้งหมดอยู่ใน compiler เดียว ข้อจำกัดที่ซื่อสัตย์คือแนวทางนี้จัดการ glyph substitution lookups และไม่มีอะไรอื่น มันไม่ใช่ bidi resolution ไม่ใช่ Indic reordering และไม่ใช่ automatic contextual shaping ที่ใดที่จำเป็นต้องใช้สิ่งเหล่านั้น การค้นหา single-substitution แต่ละ glyph จะไม่ใช่ตัวแทนที่เหมาะสม
ลำดับชั้น GSUB จากบนลงล่าง
Glyph Substitution table ถูกจัดระเบียบเป็นห่วงโซ่ของ indirections และ substitution query เดินห่วงโซ่จากบนลงล่าง ที่บนสุดคือ ScriptList script tag เช่น latn เลือก entry และ tag พิเศษ DFLT คือ default script ที่ใช้เมื่อไม่มี script ที่เจาะจงกว่าตรงกัน script entry ชี้ไปที่ LangSys ระบบภาษา โดยมี default LangSys สำหรับกรณีทั่วไปและรายการตั้งชื่อเพิ่มเติมสำหรับภาษาที่ต้องการพฤติกรรมต่างกัน ภาษาตุรกีคือตัวอย่างปกติ ที่ตัว i แบบมีจุดและไม่มีจุดต้องการการจัดการของตัวเอง
LangSys ตั้งชื่อชุด feature indices แต่ละ index ชี้เข้าไปใน FeatureList ที่ feature record มี four-byte tag อย่าง ss01 และรายการ lookup indices indices เหล่านั้นชี้เข้าไปใน LookupList ที่ subtable การ substitution จริง ๆ อยู่ ดังนั้นการแก้ไข ss01 หมายถึง: ค้นหา script ค้นหา LangSys ค้นหา feature ที่มี tag เป็น ss01 รวบรวม lookups ที่มันตั้งชื่อ และนำไปใช้ HotPDF ค่าเริ่มต้นเป็น DFLT script และ default LangSys ซึ่งเป็นสิ่งที่ Latin text designs ส่วนใหญ่ ship มาพร้อมกัน และเปิดเผยวิธีการ override script tag เมื่อฟอนต์เชื่อม features ไว้ใต้ script เฉพาะแทน
Coverage tables ตัดสินว่าใครมีส่วนร่วม
ทุก substitution subtable เริ่มต้นด้วยคำถามเดิม: glyph input นี้มีส่วนร่วมใน rule นี้หรือไม่ และถ้าใช่มันอยู่ที่ใดใน indexing ของ rule เอง คำถามนั้นได้รับคำตอบจาก Coverage table และคำตอบคือ coverage index หมายเลข ordinal ขนาดเล็กที่ส่วนที่เหลือของ subtable ใช้ค้นหาว่า glyph กลายเป็นอะไร
Coverage มีสองรูปแบบ Format 1 คือรายการ glyph id เรียงตามลำดับจากน้อยไปมาก คุณค้นหา glyph ด้วย binary search และตำแหน่งในรายการคือ coverage index Format 2 คือรายการ range records แต่ละ record มี start glyph, end glyph และ coverage index ที่ start glyph จับคู่กัน glyph ภายใน range ได้รับ coverage index โดยการ offset จาก start ของ range Format 1 กระชับเมื่อ glyphs ที่เข้าร่วมกระจายตัว Format 2 เมื่ออยู่ในช่วงต่อเนื่อง ทั้งสองถูกเรียงลำดับจึงค้นหาได้ในเวลา logarithmic และทั้งสองคืนค่า coverage index หรือ "not covered" ที่สะอาดซึ่งช่วยให้ engine ปล่อย glyph ไว้ตามเดิม
Single Substitution สองรูปแบบ
Single Substitution คือ LookupType 1 และจับคู่ glyph หนึ่งกับ replacement เดียว มันยังมีสองรูปแบบและการแบ่งเป็น space optimisation Format 1 เก็บ delta แบบ signed ตัวเดียว output glyph id คือ input glyph id บวก delta นั้น modulo 65536 นี่คือวิธีที่ฟอนต์เข้ารหัส substitution ที่ทุก glyph ที่เข้าร่วมอยู่ห่างจาก alternate ในระยะห่างคงที่เท่ากัน เช่น บล็อกตัวเลข lining ที่วางอยู่ในระยะห่างคงที่จาก oldstyle figures ที่ตรงกัน Coverage table บอกว่า glyphs ใดมีคุณสมบัติและ delta เดียวใช้ได้กับทั้งหมด
Format 2 เก็บ array ของ substitute glyph id อย่างชัดเจน coverage index จาก Coverage table คือ index เข้าสู่ array นั้น ดังนั้น glyph ที่ coverage index 0 กลายเป็น array entry แรก coverage index 1 เป็นอันที่สอง และต่อไปเรื่อย ๆ Format 2 ใช้เมื่อ alternates ไม่อยู่ที่ offset สม่ำเสมอ ซึ่งเป็นกรณีทั่วไปสำหรับ stylistic sets ที่สร้างด้วยมือ query เหมือนกันจากฝั่ง caller ไม่ว่าจะเป็นรูปแบบใด รับ input glyph เรียกผ่าน Coverage และถ้ามี coverage ให้ใช้ delta หรืออ่าน array slot
var
Pdf: THotPDF;
BaseGID, AltGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
Pdf.SetFont('My Stylistic Face', 12, []);
// Default glyph for 'a' through the font's cmap.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID means the feature did not touch this glyph.
if AltGID <> BaseGID then
{ emit AltGID in the content stream };
finally
Pdf.Free;
end;
end;
สัญญาที่ควรสังเกตคือการ pass-through GetSingleSubstituteGlyph คืน input glyph id ไม่เปลี่ยนแปลงทุก miss: ไม่มีฟอนต์ ไม่มี GSUB table ไม่มี feature ที่ตรงกัน ไม่มี coverage hit นั่นหมายความว่าการเรียกนั้นปลอดภัยที่จะทำโดยไม่มีเงื่อนไข คุณขอ alternate และถ้าไม่มี คุณได้รับสิ่งที่คุณส่งเข้ามากลับคืนนั่นเอง ดังนั้น calling code ไม่จำเป็นต้องจัดการกรณีพิเศษสำหรับฟอนต์ที่ขาด feature
ความหมายของ stylistic feature tags
Feature tag คือคำศัพท์ทั้งหมดของ alternate ที่คุณขอ และ tags ที่เกี่ยวข้องกับงาน stylistic เป็นรายการสั้น คู่หัวข้อหลักคือ salt, stylistic alternates ซึ่งเป็น catch-all สำหรับการเข้าถึงรูปแบบ alternate ของ glyph และ ss01 ถึง ss20 twenty numbered stylistic sets ที่ฟอนต์สามารถกำหนดได้ แต่ละชุดเป็น bundle การ substitutions ที่ designer จัดกลุ่มเข้าด้วยกัน ฟอนต์อาจวาง a แบบ single-story และ R แบบขาตรงไว้ใต้ ss03 เช่น การเปิดใช้งาน set นั้นจะเปลี่ยนรูปแบบทั้งสอง
รอบ ๆ สิ่งเหล่านั้นมี single-substitution tags อีกหลายตัว aalt คือ access-all-alternates ซึ่งเป็น union ของ alternate ทุกตัวที่ glyph มี มักนำเสนอเป็น glyph-palette feature titl เลือก titling capitals ที่ออกแบบมาสำหรับขนาดใหญ่ subs และ sups สลับเป็น subscript และ superscript figures จริง ๆ แทนค่าเริ่มต้นที่ scale down ordn สร้างรูปแบบ ordinal ซึ่งเป็นตัวอักษรยกสูงใน 1st และ 2nd frac สร้าง fractions แม้ว่า diagonal fractions แบบเต็มยังต้องพึ่ง ligature และ contextual logic ที่เกินกว่า single substitution แบบธรรมดา สำหรับกรณี single-glyph กลไกเหมือนกับ ss01 ส่ง tag ไปยัง substitution query และอ่าน alternate glyph กลับมา
// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
const PreferredTag: AnsiString): Word;
begin
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
if Result = BaseGID then
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
// Still BaseGID if neither feature covers this glyph.
end;
cmap format 12 และ supplementary planes
ก่อนที่การ substitution ใด ๆ จะทำงานได้ ตัวอักษรต้องกลายเป็น glyph ก่อน และนั่นคืองานของ cmap table substitution query เริ่มต้นจาก glyph id ดังนั้น path จึงเป็นตัวอักษรไปยัง glyph ผ่าน cmap แล้วจาก glyph ไปยัง alternate ผ่าน GSUB ส่วนที่น่าสนใจของ cmap คือขอบเขตการเข้าถึง subtable format 4 ครอบคลุม Basic Multilingual Plane, code points 65536 ตัวแรก และเพียงพอสำหรับข้อความ Latin ส่วนใหญ่ แต่ไม่เพียงพอสำหรับ code points จาก U+10000 ขึ้นไป supplementary planes ซึ่งเป็นที่ที่ mathematical alphanumerics, symbols หลายตัว และ scripts ที่ยังใช้อยู่หลายตัวอาศัยอยู่
Format 12 คือ subtable ที่ครอบคลุมช่วง U+0000 ถึง U+10FFFF ทั้งหมด มันคือรายการ groups เรียงลำดับ แต่ละ group มี start code point, end code point และ start glyph id ดังนั้นช่วงต่อเนื่องของ code points จับคู่กับช่วงต่อเนื่องของ glyphs HotPDF แก้ไข code points ด้วย hybrid strategy ที่ตรงกับรูปแบบข้อมูล Code points ใน BMP ได้รับบริการจาก direct array ที่ indexed โดย code point การค้นหาเดียวไม่ต้องค้นหา Code points ใน supplementary planes ได้รับบริการจาก sparse table เรียงตาม code point และค้นหาด้วย binary search ผลลัพธ์คือ GetUnicodeGlyphForCodepoint รับ Cardinal เต็ม ๆ และตอบได้ถูกต้องในทุกช่วง โดยคืน glyph id 0 ซึ่งเป็น .notdef glyph สำหรับ code point ใด ๆ ที่ฟอนต์ไม่ได้ map
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
Cp := $1D49C;
GID := Pdf.GetUnicodeGlyphForCodepoint(Cp); // format 12 lookup
if GID <> 0 then
StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
else
StyledGID := 0; // font has no glyph for this code point
end;
จุดที่ query เหล่านี้หยุด
single-substitution APIs ตอบคำถามรูปแบบหนึ่ง และควรชัดเจนเกี่ยวกับสิ่งที่มันไม่ตอบ LookupType 1 คือหนึ่งในแปด substitution types query ไม่จัดการ LookupType 2 multiple substitution ที่ glyph หนึ่งกลายเป็นหลายตัว หรือ LookupType 4 ligature substitution ที่หลาย glyphs กลายเป็นหนึ่ง มันไม่จัดการ contextual และ chaining-contextual types อย่าง LookupTypes 5 และ 6 ที่ทำงานเฉพาะเมื่อ glyph ปรากฏในย่านเพื่อนบ้านเฉพาะ หรือ extension และ reverse-chaining types diagonal fraction, Devanagari conjunct หรือ Arabic initial-medial-final cascade คือ sequence problem และ single-substitution lookup แต่ละ glyph ไม่สามารถแสดงออกได้
มันยังไม่ทำ automatic shaping ด้วย ไม่มีอะไรในนี้ตรวจสอบ run ของข้อความ ตัดสินว่าจะเปิด features ใด และนำไปใช้ในลำดับที่ script ต้องการ caller เลือก feature tag และนำไปใช้ทีละ glyph นั่นเป็นเครื่องมือที่ถูกต้องสำหรับ stylistic sets และ alternates ซึ่งเป็น opt-in และ local และเป็นเครื่องมือที่ผิดสำหรับ script ที่ต้องการ reordering การรักษาขอบเขตให้ชัดเจนคือสิ่งที่ทำให้ substitution path ยังคงขนาดเล็กและคาดเดาได้
สำหรับกรณีที่ต้องการงาน sequence-level เรื่องราว complex-script ถูกนำมาพูดถึงใน บทความของเราเกี่ยวกับ complex-script text shaping ใน Delphi ถ้า substitutions ของคุณเป็นส่วนหนึ่งของงาน reporting ขนาดใหญ่ที่ยังวางรูปภาพและฟอนต์อื่น ๆ บนหน้า คู่มือ report output ที่มีฟอนต์และรูปภาพ ครอบคลุมวิธีที่ส่วนต่าง ๆ เหล่านั้นเข้ากัน ทั้งหมดนี้ทำงานบน engine เดียวกัน คือ HotPDF Component สำหรับ Delphi และ C++Builder ซึ่งมี GSUB substitution queries ควบคู่กับ font embedding, subsetting และ text APIs ที่ครอบคลุมในบล็อกนี้