Module: Fontisan::Ufo::Compile::Gsub
- Defined in:
- lib/fontisan/ufo/compile/gsub.rb
Overview
Builds the OpenType GSUB (Glyph Substitution) table from
ligature substitution rules extracted from features.fea via
FeatureCompiler.
Emits a minimal but valid GSUB with:
- ScriptList: DFLT script, default language system
- FeatureList: one feature record per ligature feature tag
(e.g. "liga", "dlig", "clig")
- LookupList: one LigatureSubst lookup per feature tag
Each sub A B C by ABC; rule becomes a LigatureSet on the
first glyph (A), with a Ligature record pointing at ABC and
component count 2 (B, C).
Constant Summary collapse
- SCRIPT_DFLT =
"DFLT"- LANGSYS_DEFAULT =
0
Class Method Summary collapse
-
.build(parsed:, name_to_gid:) ⇒ String?
Build GSUB table bytes from parsed feature rules.
-
.build_coverage(glyphs) ⇒ Object
Build a Coverage table (format 1 = glyph list).
-
.build_feature_list(tags) ⇒ Object
FeatureList: one FeatureRecord per tag.
-
.build_gsub_table(tags, lookups) ⇒ String
Build the complete GSUB table.
-
.build_ligature(lig) ⇒ Object
Build a single Ligature record.
-
.build_ligature_lookup(sets) ⇒ Object
Build a single LigatureSubst lookup (type 4, format 1).
-
.build_ligature_set(ligatures) ⇒ Object
Build a LigatureSet for one first glyph.
-
.build_ligature_sets(rules, name_to_gid) ⇒ Hash{Integer=>Array}
Build ligature sets keyed by the first glyph in each sequence.
-
.build_lookup_list(lookups) ⇒ Object
LookupList: one lookup per feature tag.
-
.build_script_list(feature_count) ⇒ Object
ScriptList: one script (DFLT) with a default LangSys that references all features (featureIndex 0..count-1).
-
.ligature_features(parsed, name_to_gid) ⇒ Hash{String=>Array}
Resolve ligature rules to per-feature lookup data.
Class Method Details
.build(parsed:, name_to_gid:) ⇒ String?
Build GSUB table bytes from parsed feature rules.
33 34 35 36 37 38 39 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 33 def self.build(parsed:, name_to_gid:) features = ligature_features(parsed, name_to_gid) return nil if features.empty? lookups = features.values build_gsub_table(features.keys, lookups) end |
.build_coverage(glyphs) ⇒ Object
Build a Coverage table (format 1 = glyph list).
257 258 259 260 261 262 263 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 257 def self.build_coverage(glyphs) io = +"" io << [1].pack("n") # coverageFormat = 1 io << [glyphs.size].pack("n") glyphs.each { |gid| io << [gid].pack("n") } io end |
.build_feature_list(tags) ⇒ Object
FeatureList: one FeatureRecord per tag. Each points to a Feature table with one lookup index (the lookup for that tag).
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 136 def self.build_feature_list() io = +"" io << [.size].pack("n") # featureCount # FeatureRecords: tag(4) + featureOffset(2) records_size = .size * 6 header_size = 2 + records_size feature_records = +"" feature_tables = +"" .each_with_index do |tag, idx| feature_records << tag.ljust(4, " ")[0, 4] feature_records << [header_size + (idx * 4)].pack("n") feature_tables << [0].pack("n") # featureParams (null) feature_tables << [1].pack("n") # lookupIndexCount feature_tables << [idx].pack("n") # lookupIndex (1:1 feature→lookup) end io << feature_records io << feature_tables io end |
.build_gsub_table(tags, lookups) ⇒ String
Build the complete GSUB table.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 80 def self.build_gsub_table(, lookups) io = StringIO.new(+"") # Header: version (uint32 0x00010000), scriptListOffset, # featureListOffset, lookupListOffset (each uint16) header_size = 2 + 2 + 2 + 4 # version(4) + 3×uint16 # Serialize each list independently, then compute offsets. script_list = build_script_list(.size) feature_list = build_feature_list() lookup_list = build_lookup_list(lookups) script_list_offset = header_size feature_list_offset = script_list_offset + script_list.bytesize lookup_list_offset = feature_list_offset + feature_list.bytesize io << [0x00010000].pack("N") io << [script_list_offset].pack("n") io << [feature_list_offset].pack("n") io << [lookup_list_offset].pack("n") io << script_list io << feature_list io << lookup_list io.string end |
.build_ligature(lig) ⇒ Object
Build a single Ligature record.
247 248 249 250 251 252 253 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 247 def self.build_ligature(lig) io = +"" io << [lig[:result_gid]].pack("n") io << [lig[:components].size + 1].pack("n") # componentCount includes first lig[:components].each { |gid| io << [gid].pack("n") } io end |
.build_ligature_lookup(sets) ⇒ Object
Build a single LigatureSubst lookup (type 4, format 1).
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 185 def self.build_ligature_lookup(sets) io = +"" # Lookup table: lookupType(2)=4, lookupFlag(2)=0, subTableCount(2)=1 io << [4].pack("n") io << [0].pack("n") io << [1].pack("n") # Subtable offset (relative to start of Lookup table) lookup_header_size = 6 io << [lookup_header_size].pack("n") # LigatureSubstFormat1: format(2)=1, coverageOffset(2), # ligatureSetCount(2), ligatureSetOffsets[] first_glyphs = sets.keys.sort set_count = first_glyphs.size subst_header_size = 2 + 2 + 2 + (set_count * 2) # Serialize each LigatureSet to compute offsets serialized_sets = first_glyphs.map { |gid| sets[gid] }.map { |s| build_ligature_set(s) } coverage = build_coverage(first_glyphs) pos = subst_header_size set_offsets = serialized_sets.map do |s| o = pos pos += s.bytesize o end coverage_offset = pos io << [1].pack("n") # substFormat = 1 io << [coverage_offset].pack("n") io << [set_count].pack("n") set_offsets.each { |o| io << [o].pack("n") } serialized_sets.each { |s| io << s } io << coverage io end |
.build_ligature_set(ligatures) ⇒ Object
Build a LigatureSet for one first glyph.
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 226 def self.build_ligature_set(ligatures) io = +"" count = ligatures.size io << [count].pack("n") # Serialize each Ligature record serialized = ligatures.map { |lig| build_ligature(lig) } pos = 2 + (count * 2) offsets = serialized.map do |s| o = pos pos += s.bytesize o end offsets.each { |o| io << [o].pack("n") } serialized.each { |s| io << s } io end |
.build_ligature_sets(rules, name_to_gid) ⇒ Hash{Integer=>Array}
Build ligature sets keyed by the first glyph in each sequence.
59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 59 def self.build_ligature_sets(rules, name_to_gid) sets = Hash.new { |h, k| h[k] = [] } rules.each do |rule| seq_gids = rule[:sequence].map { |n| name_to_gid[n] } result_gid = name_to_gid[rule[:result]] next unless seq_gids.all? && result_gid first = seq_gids.first components = seq_gids[1..] sets[first] << { result_gid: result_gid, components: components } end sets end |
.build_lookup_list(lookups) ⇒ Object
LookupList: one lookup per feature tag. Each lookup is a LigatureSubst (type 4) lookup.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 162 def self.build_lookup_list(lookups) io = +"" count = lookups.size io << [count].pack("n") # lookupCount # Serialize each lookup first, then compute offsets relative # to the start of the LookupList. serialized = lookups.map { |sets| build_ligature_lookup(sets) } offset_table_size = 2 + (count * 2) pos = offset_table_size offsets = serialized.map do |s| o = pos pos += s.bytesize o end offsets.each { |o| io << [o].pack("n") } serialized.each { |s| io << s } io end |
.build_script_list(feature_count) ⇒ Object
ScriptList: one script (DFLT) with a default LangSys that references all features (featureIndex 0..count-1).
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 107 def self.build_script_list(feature_count) io = +"" io << [1].pack("n") # scriptCount = 1 # ScriptRecord: tag(4) + scriptOffset(2) # Script table starts right after the ScriptList header: # scriptCount(2) + ScriptRecord(6) = 8 script_offset = 8 io << SCRIPT_DFLT io << [script_offset].pack("n") # Script table: defaultLangSysOffset(2) + langSysCount(2) = 4 # LangSys follows immediately default_langsys_offset = script_offset + 4 io << [default_langsys_offset].pack("n") io << [0].pack("n") # langSysCount # LangSys: lookupOrder(2)=0, requiredFeatureIndex(2)=0xFFFF, # featureIndexCount(2), featureIndices[] io << [0].pack("n") io << [0xFFFF].pack("n") io << [feature_count].pack("n") feature_count.times { |i| io << [i].pack("n") } io end |
.ligature_features(parsed, name_to_gid) ⇒ Hash{String=>Array}
Resolve ligature rules to per-feature lookup data.
46 47 48 49 50 51 52 |
# File 'lib/fontisan/ufo/compile/gsub.rb', line 46 def self.ligature_features(parsed, name_to_gid) parsed.ligatures.keys.each_with_object({}) do |tag, h| rules = parsed.ligatures_for(tag) sets = build_ligature_sets(rules, name_to_gid) h[tag] = sets unless sets.empty? end end |