fontisan 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +27 -1
- data/Rakefile +53 -39
- data/docs/AFDKO_MIGRATION.adoc +175 -0
- data/docs/FEATURE_PARITY.adoc +263 -0
- data/docs/STITCHER_GUIDE.adoc +100 -6
- data/docs/TTX.adoc +176 -0
- data/docs/UFO_COMPILATION.adoc +281 -3
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +429 -0
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +305 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +2 -0
- data/lib/fontisan/stitcher/source.rb +11 -7
- data/lib/fontisan/subset/table_subsetter.rb +227 -11
- data/lib/fontisan/svg/standalone_glyph.rb +129 -0
- data/lib/fontisan/svg.rb +1 -0
- data/lib/fontisan/tasks/fixture_downloader.rb +162 -0
- data/lib/fontisan/tasks.rb +11 -0
- data/lib/fontisan/ufo/cli.rb +34 -21
- data/lib/fontisan/ufo/compile/feature_writers/base.rb +56 -0
- data/lib/fontisan/ufo/compile/feature_writers/curs.rb +72 -0
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +90 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern.rb +51 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern2.rb +57 -0
- data/lib/fontisan/ufo/compile/feature_writers/mark.rb +45 -0
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +122 -0
- data/lib/fontisan/ufo/compile/feature_writers/mkmk.rb +53 -0
- data/lib/fontisan/ufo/compile/feature_writers.rb +37 -0
- data/lib/fontisan/ufo/compile/filters/propagate_anchors.rb +102 -0
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +94 -0
- data/lib/fontisan/ufo/compile/filters/sort_contours.rb +69 -0
- data/lib/fontisan/ufo/compile/filters/transformations.rb +113 -0
- data/lib/fontisan/ufo/compile/filters.rb +12 -0
- data/lib/fontisan/ufo/compile.rb +1 -0
- data/lib/fontisan/ufo/convert/to_dfont.rb +41 -0
- data/lib/fontisan/ufo/convert/to_otc.rb +37 -0
- data/lib/fontisan/ufo/convert/to_otf.rb +18 -0
- data/lib/fontisan/ufo/convert/to_otf2.rb +20 -0
- data/lib/fontisan/ufo/convert/to_postscript.rb +59 -0
- data/lib/fontisan/ufo/convert/to_ttc.rb +35 -0
- data/lib/fontisan/ufo/convert/to_ttf.rb +21 -0
- data/lib/fontisan/ufo/convert/to_woff.rb +56 -0
- data/lib/fontisan/ufo/convert/to_woff2.rb +45 -0
- data/lib/fontisan/ufo/convert.rb +68 -5
- data/lib/fontisan/ufo/transformation.rb +11 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +1 -0
- metadata +32 -2
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
module PartitionStrategy
|
|
6
|
+
# Partition codepoints by Unicode script property.
|
|
7
|
+
#
|
|
8
|
+
# Each non-empty script becomes one or more partitions. Scripts
|
|
9
|
+
# that overflow +cap+ are chunked (Han has 80k+ codepoints) —
|
|
10
|
+
# chunks are named +:script_<name>_a+, +:script_<name>_b+, etc.,
|
|
11
|
+
# matching ByPlane's sub-split convention.
|
|
12
|
+
#
|
|
13
|
+
# Codepoints whose script is unknown (unassigned blocks) fall
|
|
14
|
+
# into +:script_other+. Codepoints in the +Common+ script
|
|
15
|
+
# (digits, punctuation shared across scripts) and +Inherited+
|
|
16
|
+
# script (combining marks) get their own buckets — they're
|
|
17
|
+
# typically useful as standalone subfonts.
|
|
18
|
+
#
|
|
19
|
+
# The script data is derived from {ByBlock::BLOCKS}: each Unicode
|
|
20
|
+
# block maps to exactly one primary script (with a few exceptions
|
|
21
|
+
# that we resolve explicitly). This keeps the data DRY — the
|
|
22
|
+
# authoritative block list lives in one place.
|
|
23
|
+
class ByScript < Base
|
|
24
|
+
# Maps each Unicode block label to its primary script.
|
|
25
|
+
# Block labels match ByBlock::BLOCKS keys.
|
|
26
|
+
#
|
|
27
|
+
# Special scripts:
|
|
28
|
+
# - +:common+ — shared across scripts (digits, punctuation)
|
|
29
|
+
# - +:inherited+ — combining marks that inherit the base char's script
|
|
30
|
+
# - +:other+ — fallback for unlisted blocks
|
|
31
|
+
SCRIPT_OF_BLOCK = {
|
|
32
|
+
# Latin family
|
|
33
|
+
"Basic_Latin" => :common,
|
|
34
|
+
"Latin-1_Supplement" => :latin,
|
|
35
|
+
"Latin_Extended-A" => :latin,
|
|
36
|
+
"Latin_Extended-B" => :latin,
|
|
37
|
+
"Latin_Extended-C" => :latin,
|
|
38
|
+
"Latin_Extended-D" => :latin,
|
|
39
|
+
"Latin_Extended-E" => :latin,
|
|
40
|
+
"Latin_Extended-F" => :latin,
|
|
41
|
+
"Latin_Extended_Additional" => :latin,
|
|
42
|
+
"Phonetic_Extensions" => :latin,
|
|
43
|
+
"Phonetic_Extensions_Supplement" => :latin,
|
|
44
|
+
"Modifier_Tone_Letters" => :latin,
|
|
45
|
+
"Spacing_Modifier_Letters" => :common,
|
|
46
|
+
"Combining_Diacritical_Marks" => :inherited,
|
|
47
|
+
"Combining_Diacritical_Marks_Extended" => :inherited,
|
|
48
|
+
"Combining_Diacritical_Marks_Supplement" => :inherited,
|
|
49
|
+
"Combining_Diacritical_Marks_for_Symbols" => :inherited,
|
|
50
|
+
"Combining_Half_Marks" => :inherited,
|
|
51
|
+
|
|
52
|
+
# Greek
|
|
53
|
+
"Greek_and_Coptic" => :greek,
|
|
54
|
+
"Greek_Extended" => :greek,
|
|
55
|
+
|
|
56
|
+
# Cyrillic
|
|
57
|
+
"Cyrillic" => :cyrillic,
|
|
58
|
+
"Cyrillic_Supplement" => :cyrillic,
|
|
59
|
+
"Cyrillic_Extended-A" => :cyrillic,
|
|
60
|
+
"Cyrillic_Extended-B" => :cyrillic,
|
|
61
|
+
"Cyrillic_Extended-C" => :cyrillic,
|
|
62
|
+
"Cyrillic_Extended-D" => :cyrillic,
|
|
63
|
+
|
|
64
|
+
# Middle Eastern
|
|
65
|
+
"Armenian" => :armenian,
|
|
66
|
+
"Hebrew" => :hebrew,
|
|
67
|
+
"Arabic" => :arabic,
|
|
68
|
+
"Arabic_Supplement" => :arabic,
|
|
69
|
+
"Arabic_Extended-A" => :arabic,
|
|
70
|
+
"Arabic_Extended-B" => :arabic,
|
|
71
|
+
"Arabic_Extended-C" => :arabic,
|
|
72
|
+
"Arabic_Extended-D" => :arabic,
|
|
73
|
+
"Arabic_Presentation_Forms-A" => :arabic,
|
|
74
|
+
"Arabic_Presentation_Forms-B" => :arabic,
|
|
75
|
+
"Syriac" => :syriac,
|
|
76
|
+
"Syriac_Supplement" => :syriac,
|
|
77
|
+
"Thaana" => :thaana,
|
|
78
|
+
"Samaritan" => :samaritan,
|
|
79
|
+
"Mandaic" => :mandaic,
|
|
80
|
+
|
|
81
|
+
# Indic
|
|
82
|
+
"Devanagari" => :devanagari,
|
|
83
|
+
"Devanagari_Extended" => :devanagari,
|
|
84
|
+
"Bengali" => :bengali,
|
|
85
|
+
"Gurmukhi" => :gurmukhi,
|
|
86
|
+
"Gujarati" => :gujarati,
|
|
87
|
+
"Oriya" => :oriya,
|
|
88
|
+
"Tamil" => :tamil,
|
|
89
|
+
"Tamil_Supplement" => :tamil,
|
|
90
|
+
"Telugu" => :telugu,
|
|
91
|
+
"Kannada" => :kannada,
|
|
92
|
+
"Malayalam" => :malayalam,
|
|
93
|
+
"Sinhala" => :sinhala,
|
|
94
|
+
"Sinhala_Archaic_Numbers" => :sinhala,
|
|
95
|
+
"Vedic_Extensions" => :inherited,
|
|
96
|
+
|
|
97
|
+
# Southeast Asian
|
|
98
|
+
"Thai" => :thai,
|
|
99
|
+
"Lao" => :lao,
|
|
100
|
+
"Tibetan" => :tibetan,
|
|
101
|
+
"Myanmar" => :myanmar,
|
|
102
|
+
"Myanmar_Extended-A" => :myanmar,
|
|
103
|
+
"Myanmar_Extended-B" => :myanmar,
|
|
104
|
+
"Myanmar_Extended-C" => :myanmar,
|
|
105
|
+
"Khmer" => :khmer,
|
|
106
|
+
"Khmer_Symbols" => :khmer,
|
|
107
|
+
"Tagalog" => :tagalog,
|
|
108
|
+
"Hanunoo" => :hanunoo,
|
|
109
|
+
"Buhid" => :buhid,
|
|
110
|
+
"Tagbanwa" => :tagbanwa,
|
|
111
|
+
|
|
112
|
+
# Hangul
|
|
113
|
+
"Hangul_Jamo" => :hangul,
|
|
114
|
+
"Hangul_Compatibility_Jamo" => :hangul,
|
|
115
|
+
"Hangul_Jamo_Extended-A" => :hangul,
|
|
116
|
+
"Hangul_Jamo_Extended-B" => :hangul,
|
|
117
|
+
"Hangul_Syllables" => :hangul,
|
|
118
|
+
|
|
119
|
+
# CJK — Han + native Japanese / Korean
|
|
120
|
+
"CJK_Unified_Ideographs" => :han,
|
|
121
|
+
"CJK_Unified_Ideographs_Extension_A" => :han,
|
|
122
|
+
"CJK_Unified_Ideographs_Extension_B" => :han,
|
|
123
|
+
"CJK_Unified_Ideographs_Extension_C" => :han,
|
|
124
|
+
"CJK_Unified_Ideographs_Extension_D" => :han,
|
|
125
|
+
"CJK_Unified_Ideographs_Extension-E" => :han,
|
|
126
|
+
"CJK_Unified_Ideographs_Extension-F" => :han,
|
|
127
|
+
"CJK_Unified_Ideographs_Extension_G" => :han,
|
|
128
|
+
"CJK_Unified_Ideographs_Extension_H" => :han,
|
|
129
|
+
"CJK_Unified_Ideographs_Extension_I" => :han,
|
|
130
|
+
"CJK_Compatibility_Ideographs" => :han,
|
|
131
|
+
"CJK_Compatibility_Ideographs_Supplement" => :han,
|
|
132
|
+
"CJK_Radicals_Supplement" => :han,
|
|
133
|
+
"Kangxi_Radicals" => :han,
|
|
134
|
+
"CJK_Symbols_and_Punctuation" => :common,
|
|
135
|
+
"CJK_Compatibility" => :han,
|
|
136
|
+
"CJK_Compatibility_Forms" => :common,
|
|
137
|
+
"CJK_Strokes" => :han,
|
|
138
|
+
"Ideographic_Description_Characters" => :common,
|
|
139
|
+
"Enclosed_CJK_Letters_and_Months" => :common,
|
|
140
|
+
"Hiragana" => :hiragana,
|
|
141
|
+
"Katakana" => :katakana,
|
|
142
|
+
"Katakana_Phonetic_Extensions" => :katakana,
|
|
143
|
+
"Kana_Supplement" => :hiragana,
|
|
144
|
+
"Kana_Extended-A" => :hiragana,
|
|
145
|
+
"Kana_Extended-B" => :hiragana,
|
|
146
|
+
"Small_Kana_Extension" => :hiragana,
|
|
147
|
+
"Halfwidth_and_Fullwidth_Forms" => :common,
|
|
148
|
+
"Vertical_Forms" => :common,
|
|
149
|
+
"Ideographic_Symbols_and_Punctuation" => :common,
|
|
150
|
+
|
|
151
|
+
# Other scripts (single-block each)
|
|
152
|
+
"Ethiopic" => :ethiopic,
|
|
153
|
+
"Ethiopic_Supplement" => :ethiopic,
|
|
154
|
+
"Ethiopic_Extended" => :ethiopic,
|
|
155
|
+
"Ethiopic_Extended-A" => :ethiopic,
|
|
156
|
+
"Ethiopian_Extended-B" => :ethiopic,
|
|
157
|
+
"Cherokee" => :cherokee,
|
|
158
|
+
"Cherokee_Supplement" => :cherokee,
|
|
159
|
+
"Unified_Canadian_Aboriginal_Syllabics" => :canadian_aboriginal,
|
|
160
|
+
"Unified_Canadian_Aboriginal_Syllabics_Extended" => :canadian_aboriginal,
|
|
161
|
+
"Unified_Canadian_Aboriginal_Syllabics_Extended-A" => :canadian_aboriginal,
|
|
162
|
+
"Ogham" => :ogham,
|
|
163
|
+
"Runic" => :runic,
|
|
164
|
+
"Glagolitic" => :glagolitic,
|
|
165
|
+
"Glagolitic_Supplement" => :glagolitic,
|
|
166
|
+
"Tifinagh" => :tifinagh,
|
|
167
|
+
"Georgian" => :georgian,
|
|
168
|
+
"Georgian_Supplement" => :georgian,
|
|
169
|
+
"Georgian_Extended" => :georgian,
|
|
170
|
+
"Mongolian" => :mongolian,
|
|
171
|
+
"Mongolian_Supplement" => :mongolian,
|
|
172
|
+
"Limbu" => :limbu,
|
|
173
|
+
"Tai_Le" => :tai_le,
|
|
174
|
+
"New_Tai_Lue" => :new_tai_lue,
|
|
175
|
+
"Tai_Tham" => :tai_tham,
|
|
176
|
+
"Tai_Viet" => :tai_viet,
|
|
177
|
+
"Ol_Chiki" => :ol_chiki,
|
|
178
|
+
"Bopomofo" => :bopomofo,
|
|
179
|
+
"Bopomofo_Extended" => :bopomofo,
|
|
180
|
+
"Yi_Syllables" => :yi,
|
|
181
|
+
"Yi_Radicals" => :yi,
|
|
182
|
+
"Vai" => :vai,
|
|
183
|
+
"Bamum" => :bamum,
|
|
184
|
+
"Bamum_Supplement" => :bamum,
|
|
185
|
+
"Syloti_Nagri" => :syloti_nagri,
|
|
186
|
+
"Phags-pa" => :phags_pa,
|
|
187
|
+
"Saurashtra" => :saurashtra,
|
|
188
|
+
"Kayah_Li" => :kayah_li,
|
|
189
|
+
"Rejang" => :rejang,
|
|
190
|
+
"Javanese" => :javanese,
|
|
191
|
+
"Cham" => :cham,
|
|
192
|
+
"Lepcha" => :lepcha,
|
|
193
|
+
"Meetei_Mayek" => :meetei_mayek,
|
|
194
|
+
"Meetei_Mayek_Extensions" => :meetei_mayek,
|
|
195
|
+
"Lisu" => :lisu,
|
|
196
|
+
"Lisu_Supplement" => :lisu,
|
|
197
|
+
"Sundanese" => :sundanese,
|
|
198
|
+
"Sundanese_Supplement" => :sundanese,
|
|
199
|
+
"Batak" => :batak,
|
|
200
|
+
"Buginese" => :buginese,
|
|
201
|
+
"Ahom" => :ahom,
|
|
202
|
+
"Dogra" => :dogra,
|
|
203
|
+
"Tulu-Tigalari" => :tulu_tigalari,
|
|
204
|
+
"Grantha" => :grantha,
|
|
205
|
+
"Newa" => :newa,
|
|
206
|
+
"Tirhuta" => :tirhuta,
|
|
207
|
+
"Siddham" => :siddham,
|
|
208
|
+
"Modi" => :modi,
|
|
209
|
+
"Sharada" => :sharada,
|
|
210
|
+
"Takri" => :takri,
|
|
211
|
+
"Kaithi" => :kaithi,
|
|
212
|
+
"Mahajani" => :mahajani,
|
|
213
|
+
"Multani" => :multani,
|
|
214
|
+
"Khudawadi" => :khudawadi,
|
|
215
|
+
"Nandinagari" => :nandinagari,
|
|
216
|
+
"Nushu" => :nushu,
|
|
217
|
+
"Wancho" => :wancho,
|
|
218
|
+
"Toto" => :toto,
|
|
219
|
+
"Nag_Mundari" => :nag_mundari,
|
|
220
|
+
|
|
221
|
+
# Numerals / symbols (Common)
|
|
222
|
+
"Superscripts_and_Subscripts" => :common,
|
|
223
|
+
"Number_Forms" => :common,
|
|
224
|
+
"Currency_Symbols" => :common,
|
|
225
|
+
"Letterlike_Symbols" => :common,
|
|
226
|
+
"Arrows" => :common,
|
|
227
|
+
"Mathematical_Operators" => :common,
|
|
228
|
+
"Miscellaneous_Technical" => :common,
|
|
229
|
+
"Control_Pictures" => :common,
|
|
230
|
+
"Optical_Character_Recognition" => :common,
|
|
231
|
+
"Enclosed_Alphanumerics" => :common,
|
|
232
|
+
"Box_Drawing" => :common,
|
|
233
|
+
"Block_Elements" => :common,
|
|
234
|
+
"Geometric_Shapes" => :common,
|
|
235
|
+
"Miscellaneous_Symbols" => :common,
|
|
236
|
+
"Dingbats" => :common,
|
|
237
|
+
"Miscellaneous_Mathematical_Symbols-A" => :common,
|
|
238
|
+
"Miscellaneous_Mathematical_Symbols-B" => :common,
|
|
239
|
+
"Supplemental_Arrows-A" => :common,
|
|
240
|
+
"Supplemental_Arrows-B" => :common,
|
|
241
|
+
"Supplemental_Arrows-C" => :common,
|
|
242
|
+
"Supplemental_Mathematical_Operators" => :common,
|
|
243
|
+
"Miscellaneous_Symbols_and_Arrows" => :common,
|
|
244
|
+
"Braille_Patterns" => :braille,
|
|
245
|
+
"General_Punctuation" => :common,
|
|
246
|
+
"Supplemental_Punctuation" => :common,
|
|
247
|
+
"Alphabetic_Presentation_Forms" => :common,
|
|
248
|
+
"Specials" => :common,
|
|
249
|
+
"Variation_Selectors" => :inherited,
|
|
250
|
+
"Variation_Selectors_Supplement" => :inherited,
|
|
251
|
+
"Tags" => :common,
|
|
252
|
+
}.freeze
|
|
253
|
+
|
|
254
|
+
# @param cp_map [Hash{Integer=>Object}] codepoint → donor label
|
|
255
|
+
# @param cap [Integer] max codepoints per partition
|
|
256
|
+
# @return [Blueprint]
|
|
257
|
+
def call(cp_map, cap: DEFAULT_CAP)
|
|
258
|
+
grouped = group_by_script(cp_map)
|
|
259
|
+
partitions = []
|
|
260
|
+
|
|
261
|
+
grouped.each do |script, entries|
|
|
262
|
+
chunks(entries, cap).each_with_index do |chunk, idx|
|
|
263
|
+
partitions << Partition.new(
|
|
264
|
+
name: partition_name(script, idx),
|
|
265
|
+
cps: chunk.map(&:first),
|
|
266
|
+
donor_map: chunk.to_h,
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
Blueprint.new(partitions: partitions)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
def group_by_script(cp_map)
|
|
277
|
+
cp_map.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(cp, label), h|
|
|
278
|
+
script = script_of_codepoint(cp)
|
|
279
|
+
h[script] << [cp, label]
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def script_of_codepoint(cp)
|
|
284
|
+
block_label = ByBlock::BLOCKS.find { |_label, range| range.cover?(cp) }&.first
|
|
285
|
+
return :other unless block_label
|
|
286
|
+
|
|
287
|
+
SCRIPT_OF_BLOCK[block_label] || :other
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def chunks(entries, cap)
|
|
291
|
+
entries.each_slice(cap).to_a
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# First partition per script has no suffix; subsequent ones
|
|
295
|
+
# get _a, _b, ... matching ByPlane's sub-split convention.
|
|
296
|
+
def partition_name(script, idx)
|
|
297
|
+
return :"script_#{script}" if idx.zero?
|
|
298
|
+
|
|
299
|
+
suffix = ("a".ord + idx - 1).chr
|
|
300
|
+
:"script_#{script}_#{suffix}"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -17,6 +17,8 @@ module Fontisan
|
|
|
17
17
|
autoload :Blueprint, "fontisan/stitcher/partition_strategy/blueprint"
|
|
18
18
|
autoload :Partition, "fontisan/stitcher/partition_strategy/partition"
|
|
19
19
|
autoload :ByPlane, "fontisan/stitcher/partition_strategy/by_plane"
|
|
20
|
+
autoload :ByBlock, "fontisan/stitcher/partition_strategy/by_block"
|
|
21
|
+
autoload :ByScript, "fontisan/stitcher/partition_strategy/by_script"
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -336,10 +336,15 @@ module Fontisan
|
|
|
336
336
|
|
|
337
337
|
# Apply a 2×3 affine matrix [a, b, c, d, e, f] to each point of
|
|
338
338
|
# a simple component, appending the transformed contours.
|
|
339
|
-
#
|
|
340
|
-
#
|
|
339
|
+
# The math is delegated to +Transformation#apply+ so the
|
|
340
|
+
# affine-matrix logic lives in one place (DRY) — same code path
|
|
341
|
+
# the UFO Transformations filter uses.
|
|
341
342
|
def flatten_simple_component(simple, ufo_glyph, matrix)
|
|
342
|
-
|
|
343
|
+
transform = Ufo::Transformation.new(
|
|
344
|
+
a: matrix[0], b: matrix[1],
|
|
345
|
+
c: matrix[2], d: matrix[3],
|
|
346
|
+
e: matrix[4], f: matrix[5]
|
|
347
|
+
)
|
|
343
348
|
num_contours = simple.end_pts_of_contours&.size || 0
|
|
344
349
|
return if num_contours.zero?
|
|
345
350
|
|
|
@@ -350,13 +355,12 @@ module Fontisan
|
|
|
350
355
|
ufo_points = points.map do |pt|
|
|
351
356
|
x = (pt[:x] || pt["x"]).to_f
|
|
352
357
|
y = (pt[:y] || pt["y"]).to_f
|
|
353
|
-
tx =
|
|
354
|
-
ty = b * x + d * y + f
|
|
358
|
+
tx, ty = transform.apply(x, y)
|
|
355
359
|
on_curve = pt[:on_curve].nil? || pt[:on_curve]
|
|
356
360
|
type = on_curve ? "line" : "offcurve"
|
|
357
|
-
|
|
361
|
+
Ufo::Point.new(x: tx, y: ty, type: type)
|
|
358
362
|
end
|
|
359
|
-
ufo_glyph.add_contour(
|
|
363
|
+
ufo_glyph.add_contour(Ufo::Contour.new(ufo_points))
|
|
360
364
|
end
|
|
361
365
|
end
|
|
362
366
|
|
|
@@ -56,6 +56,8 @@ module Fontisan
|
|
|
56
56
|
@options = options
|
|
57
57
|
@glyf_data = nil
|
|
58
58
|
@loca_offsets = nil
|
|
59
|
+
@subset_bbox = nil # [xMin, yMin, xMax, yMax] over actual subset glyphs
|
|
60
|
+
@subset_max_advance = 0 # largest advanceWidth in subset hmtx
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
# Subset a table by tag
|
|
@@ -110,10 +112,12 @@ module Fontisan
|
|
|
110
112
|
data
|
|
111
113
|
end
|
|
112
114
|
|
|
113
|
-
# Subset hhea table (update numberOfHMetrics)
|
|
115
|
+
# Subset hhea table (update numberOfHMetrics + advanceWidthMax)
|
|
114
116
|
#
|
|
115
|
-
# Updates
|
|
116
|
-
#
|
|
117
|
+
# Updates numberOfHMetrics to reflect the subset's glyph count and
|
|
118
|
+
# recomputes advanceWidthMax from the subset's actual hmtx. The
|
|
119
|
+
# source TTC's advanceWidthMax covers every donor font and is far
|
|
120
|
+
# larger than any per-block subset needs.
|
|
117
121
|
#
|
|
118
122
|
# @param table [Hhea] Parsed hhea table
|
|
119
123
|
# @param hmtx [Hmtx, nil] Optional parsed hmtx table (for calculating metrics)
|
|
@@ -128,9 +132,18 @@ module Fontisan
|
|
|
128
132
|
calculate_number_of_h_metrics
|
|
129
133
|
end
|
|
130
134
|
|
|
131
|
-
#
|
|
135
|
+
# numberOfHMetrics at offset 34 (uint16)
|
|
132
136
|
data[34, 2] = [new_num_h_metrics].pack("n")
|
|
133
137
|
|
|
138
|
+
# advanceWidthMax at offset 10 (uint16). Recompute from subset
|
|
139
|
+
# hmtx so a per-block subset doesn't keep the source TTC's
|
|
140
|
+
# max (which can be 4x larger than any glyph in the subset).
|
|
141
|
+
# Tables are processed alphabetically (hhea before hmtx), so
|
|
142
|
+
# we read hmtx directly here rather than relying on a cached
|
|
143
|
+
# value from subset_hmtx.
|
|
144
|
+
new_max = compute_subset_max_advance
|
|
145
|
+
data[10, 2] = [new_max].pack("n") if new_max.positive?
|
|
146
|
+
|
|
134
147
|
data
|
|
135
148
|
end
|
|
136
149
|
|
|
@@ -152,14 +165,18 @@ module Fontisan
|
|
|
152
165
|
# Build new hmtx data
|
|
153
166
|
data = String.new(encoding: Encoding::BINARY)
|
|
154
167
|
|
|
168
|
+
max_advance = 0
|
|
155
169
|
mapping.old_ids.each do |old_id|
|
|
156
170
|
metric = table.metric_for(old_id)
|
|
157
171
|
next unless metric
|
|
158
172
|
|
|
159
|
-
|
|
173
|
+
advance = metric[:advance_width]
|
|
174
|
+
max_advance = advance if advance && advance > max_advance
|
|
175
|
+
data << [advance].pack("n")
|
|
160
176
|
data << [metric[:lsb]].pack("n")
|
|
161
177
|
end
|
|
162
178
|
|
|
179
|
+
@subset_max_advance = max_advance
|
|
163
180
|
data
|
|
164
181
|
end
|
|
165
182
|
|
|
@@ -264,8 +281,32 @@ module Fontisan
|
|
|
264
281
|
#
|
|
265
282
|
# @param table [Head] Parsed head table
|
|
266
283
|
# @return [String] Binary data of subset head table
|
|
284
|
+
# Subset head table (recompute bbox from actual subset glyphs)
|
|
285
|
+
#
|
|
286
|
+
# The source TTC's head.xMin/yMin/xMax/yMax covers every donor
|
|
287
|
+
# font in the collection — far larger than any per-block subset.
|
|
288
|
+
# Browsers and layout engines use head bbox (plus hhea + OS/2)
|
|
289
|
+
# to compute line height and clip text; a too-large bbox makes
|
|
290
|
+
# glyphs render at the wrong visual size.
|
|
291
|
+
#
|
|
292
|
+
# @param _table [Head] Parsed head table (unused; we re-serialize
|
|
293
|
+
# directly from font.table_data so we don't lose other fields)
|
|
294
|
+
# @return [String] Binary data of subset head table
|
|
267
295
|
def subset_head(_table)
|
|
268
|
-
|
|
296
|
+
# Trigger glyf build (which populates @subset_bbox) if not done.
|
|
297
|
+
glyf = font.table("glyf")
|
|
298
|
+
build_glyf_and_loca(glyf) unless @glyf_data
|
|
299
|
+
|
|
300
|
+
data = font.table_data["head"].dup
|
|
301
|
+
|
|
302
|
+
if @subset_bbox
|
|
303
|
+
x_min, y_min, x_max, y_max = @subset_bbox
|
|
304
|
+
# head layout: xMin at offset 36, yMin 38, xMax 40, yMax 42
|
|
305
|
+
# (each int16, big-endian, signed)
|
|
306
|
+
data[36, 8] = [x_min, y_min, x_max, y_max].pack("n4")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
data
|
|
269
310
|
end
|
|
270
311
|
|
|
271
312
|
# Subset OS/2 table (optionally prune Unicode ranges)
|
|
@@ -295,6 +336,32 @@ module Fontisan
|
|
|
295
336
|
mapping.size
|
|
296
337
|
end
|
|
297
338
|
|
|
339
|
+
# Compute the largest advanceWidth across all subset glyphs by
|
|
340
|
+
# reading the source hmtx directly. Called from subset_hhea
|
|
341
|
+
# because hhea is processed before hmtx (alphabetical order).
|
|
342
|
+
#
|
|
343
|
+
# @return [Integer] max advanceWidth, or 0 if no metrics found
|
|
344
|
+
def compute_subset_max_advance
|
|
345
|
+
hmtx = font.table("hmtx")
|
|
346
|
+
return 0 unless hmtx
|
|
347
|
+
|
|
348
|
+
unless hmtx.parsed?
|
|
349
|
+
hhea = font.table("hhea")
|
|
350
|
+
maxp = font.table("maxp")
|
|
351
|
+
hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
max_advance = 0
|
|
355
|
+
mapping.old_ids.each do |old_id|
|
|
356
|
+
metric = hmtx.metric_for(old_id)
|
|
357
|
+
next unless metric
|
|
358
|
+
|
|
359
|
+
advance = metric[:advance_width]
|
|
360
|
+
max_advance = advance if advance && advance > max_advance
|
|
361
|
+
end
|
|
362
|
+
max_advance
|
|
363
|
+
end
|
|
364
|
+
|
|
298
365
|
# Build glyf and loca tables together
|
|
299
366
|
#
|
|
300
367
|
# This method extracts glyph data for all glyphs in the mapping,
|
|
@@ -318,6 +385,17 @@ module Fontisan
|
|
|
318
385
|
@loca_offsets = []
|
|
319
386
|
current_offset = 0
|
|
320
387
|
|
|
388
|
+
# Track union bbox across all subset glyphs. Glyph binary layout:
|
|
389
|
+
# int16 numberOfContours (offset 0)
|
|
390
|
+
# int16 xMin (offset 2)
|
|
391
|
+
# int16 yMin (offset 4)
|
|
392
|
+
# int16 xMax (offset 6)
|
|
393
|
+
# int16 yMax (offset 8)
|
|
394
|
+
bbox_x_min = 1 << 30
|
|
395
|
+
bbox_y_min = 1 << 30
|
|
396
|
+
bbox_x_max = -(1 << 30)
|
|
397
|
+
bbox_y_max = -(1 << 30)
|
|
398
|
+
|
|
321
399
|
# Process glyphs in mapping order
|
|
322
400
|
mapping.old_ids.each do |old_id|
|
|
323
401
|
@loca_offsets << current_offset
|
|
@@ -334,6 +412,21 @@ module Fontisan
|
|
|
334
412
|
# Extract glyph data
|
|
335
413
|
glyph_data = glyf_table.raw_data[offset, size]
|
|
336
414
|
|
|
415
|
+
# Update bbox union. Each glyph has at least 10 bytes of
|
|
416
|
+
# header (numberOfContours + 4 int16 bbox fields).
|
|
417
|
+
if glyph_data.bytesize >= 10
|
|
418
|
+
_n, gx_min, gy_min, gx_max, gy_max = glyph_data[0, 10].unpack("n5")
|
|
419
|
+
# Treat as signed int16
|
|
420
|
+
gx_min = (gx_min ^ 0x8000) - 0x8000
|
|
421
|
+
gy_min = (gy_min ^ 0x8000) - 0x8000
|
|
422
|
+
gx_max = (gx_max ^ 0x8000) - 0x8000
|
|
423
|
+
gy_max = (gy_max ^ 0x8000) - 0x8000
|
|
424
|
+
bbox_x_min = gx_min if gx_min < bbox_x_min
|
|
425
|
+
bbox_y_min = gy_min if gy_min < bbox_y_min
|
|
426
|
+
bbox_x_max = gx_max if gx_max > bbox_x_max
|
|
427
|
+
bbox_y_max = gy_max if gy_max > bbox_y_max
|
|
428
|
+
end
|
|
429
|
+
|
|
337
430
|
# Check if compound glyph and remap components
|
|
338
431
|
if compound_glyph?(glyph_data)
|
|
339
432
|
glyph_data = remap_compound_glyph(glyph_data)
|
|
@@ -346,6 +439,11 @@ module Fontisan
|
|
|
346
439
|
|
|
347
440
|
# Add final offset
|
|
348
441
|
@loca_offsets << current_offset
|
|
442
|
+
|
|
443
|
+
# Stash union bbox if we saw at least one non-empty glyph.
|
|
444
|
+
return if bbox_x_min > bbox_x_max
|
|
445
|
+
|
|
446
|
+
@subset_bbox = [bbox_x_min, bbox_y_min, bbox_x_max, bbox_y_max]
|
|
349
447
|
end
|
|
350
448
|
|
|
351
449
|
# Check if glyph data represents a compound glyph
|
|
@@ -417,12 +515,130 @@ module Fontisan
|
|
|
417
515
|
# Creates a minimal cmap table with format 4 subtable for BMP
|
|
418
516
|
# and format 12 for supplementary planes if needed.
|
|
419
517
|
#
|
|
420
|
-
# @param mappings [Hash<Integer, Integer>] Char code => glyph ID
|
|
518
|
+
# @param mappings [Hash<Integer, Integer>] Char code => new glyph ID
|
|
421
519
|
# @return [String] Binary cmap data
|
|
422
|
-
def build_cmap_binary(
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
|
|
520
|
+
def build_cmap_binary(mappings)
|
|
521
|
+
# Edge case: empty mappings (e.g., block with no covered chars).
|
|
522
|
+
# Emit a minimal valid cmap with one format 4 subtable mapping
|
|
523
|
+
# only U+0000 → .notdef so the table isn't empty.
|
|
524
|
+
mappings = { 0 => 0 } if mappings.empty?
|
|
525
|
+
|
|
526
|
+
bmp = mappings.select { |cp, _| cp <= 0xFFFF }
|
|
527
|
+
supp = mappings.select { |cp, _| cp > 0xFFFF }
|
|
528
|
+
|
|
529
|
+
subtables = []
|
|
530
|
+
records = [] # [platform_id, encoding_id, subtable_index]
|
|
531
|
+
|
|
532
|
+
unless bmp.empty?
|
|
533
|
+
subtables << build_cmap_format_4(bmp)
|
|
534
|
+
idx = subtables.size - 1
|
|
535
|
+
records << [3, 1, idx] # Windows BMP
|
|
536
|
+
records << [0, 3, idx] # Unicode BMP
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
unless supp.empty?
|
|
540
|
+
# Format 12 covers both BMP and supplementary — include all
|
|
541
|
+
# mappings so a single subtable covers the full range.
|
|
542
|
+
subtables << build_cmap_format_12(mappings)
|
|
543
|
+
idx = subtables.size - 1
|
|
544
|
+
records << [3, 10, idx] # Windows UCS-4
|
|
545
|
+
records << [0, 4, idx] # Unicode full
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Header: version (uint16) + numTables (uint16)
|
|
549
|
+
num_tables = records.size
|
|
550
|
+
header = [0, num_tables].pack("nn")
|
|
551
|
+
|
|
552
|
+
# Encoding records start immediately after the header.
|
|
553
|
+
# Each record is 8 bytes; subtables follow.
|
|
554
|
+
subtable_base = 4 + (8 * num_tables)
|
|
555
|
+
|
|
556
|
+
offsets = []
|
|
557
|
+
running = subtable_base
|
|
558
|
+
subtables.each do |st|
|
|
559
|
+
offsets << running
|
|
560
|
+
running += st.bytesize
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
record_bytes = +""
|
|
564
|
+
records.each do |pid, eid, idx|
|
|
565
|
+
record_bytes << [pid, eid, offsets[idx]].pack("nnN")
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
header + record_bytes + subtables.join
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Format 4 subtable: segment-mapping with idDelta, suitable for
|
|
572
|
+
# BMP codepoints (U+0000..U+FFFF). Builds compact segments where
|
|
573
|
+
# consecutive codepoints map to consecutive glyph IDs.
|
|
574
|
+
def build_cmap_format_4(bmp_mappings)
|
|
575
|
+
segments = coalesce_segments(bmp_mappings)
|
|
576
|
+
# Mandatory final segment: U+FFFF → gid 0 (per OpenType spec).
|
|
577
|
+
segments << { start_cp: 0xFFFF, end_cp: 0xFFFF, start_gid: 0 }
|
|
578
|
+
|
|
579
|
+
seg_count = segments.size
|
|
580
|
+
seg_count_x2 = seg_count * 2
|
|
581
|
+
search_range = 2**Math.log2(seg_count).floor * 2
|
|
582
|
+
search_range = 2 if search_range < 2
|
|
583
|
+
entry_selector = Math.log2(search_range / 2).to_i
|
|
584
|
+
range_shift = seg_count_x2 - search_range
|
|
585
|
+
|
|
586
|
+
end_codes = segments.map { |s| s[:end_cp] }
|
|
587
|
+
start_codes = segments.map { |s| s[:start_cp] }
|
|
588
|
+
# idDelta is int16 stored as uint16 (two's complement). For a
|
|
589
|
+
# sequential segment, idDelta = (start_gid - start_cp) & 0xFFFF.
|
|
590
|
+
id_deltas = segments.map { |s| (s[:start_gid] - s[:start_cp]) & 0xFFFF }
|
|
591
|
+
id_range_offsets = [0] * seg_count
|
|
592
|
+
|
|
593
|
+
subtable = +""
|
|
594
|
+
subtable << [4, 0, 0, seg_count_x2,
|
|
595
|
+
search_range, entry_selector, range_shift].pack("n*")
|
|
596
|
+
subtable << end_codes.pack("n*")
|
|
597
|
+
subtable << [0].pack("n") # reservedPad
|
|
598
|
+
subtable << start_codes.pack("n*")
|
|
599
|
+
subtable << id_deltas.pack("n*")
|
|
600
|
+
subtable << id_range_offsets.pack("n*")
|
|
601
|
+
|
|
602
|
+
# Patch the length field (was placeholder 0).
|
|
603
|
+
subtable[2, 2] = [subtable.bytesize].pack("n")
|
|
604
|
+
subtable
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Format 12 subtable: segmented coverage for full Unicode range.
|
|
608
|
+
# Simpler than format 4 — just (start_char, end_char, start_gid)
|
|
609
|
+
# triples with no delta/offset indirection.
|
|
610
|
+
def build_cmap_format_12(all_mappings)
|
|
611
|
+
groups = coalesce_segments(all_mappings)
|
|
612
|
+
num_groups = groups.size
|
|
613
|
+
|
|
614
|
+
subtable = +""
|
|
615
|
+
subtable << [12, 0, 0, 0, num_groups].pack("nnNNN")
|
|
616
|
+
groups.each do |g|
|
|
617
|
+
subtable << [g[:start_cp], g[:end_cp], g[:start_gid]].pack("NNN")
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Patch the length field (was placeholder 0). Total length is
|
|
621
|
+
# 16-byte header + 12 bytes per group.
|
|
622
|
+
subtable[4, 4] = [subtable.bytesize].pack("N")
|
|
623
|
+
subtable
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Group codepoints into consecutive runs where both codepoint AND
|
|
627
|
+
# glyph ID are sequential. Each run becomes one segment/group.
|
|
628
|
+
def coalesce_segments(mappings)
|
|
629
|
+
sorted = mappings.sort_by { |cp, _| cp }
|
|
630
|
+
segments = []
|
|
631
|
+
current = nil
|
|
632
|
+
sorted.each do |cp, gid|
|
|
633
|
+
if current && cp == current[:end_cp] + 1 && gid == current[:start_gid] + (cp - current[:start_cp])
|
|
634
|
+
current[:end_cp] = cp
|
|
635
|
+
else
|
|
636
|
+
segments << current if current
|
|
637
|
+
current = { start_cp: cp, end_cp: cp, start_gid: gid }
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
segments << current if current
|
|
641
|
+
segments
|
|
426
642
|
end
|
|
427
643
|
|
|
428
644
|
# Build post table version 3.0 (no glyph names)
|