fontisan 0.4.9 → 0.4.11
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/Rakefile +53 -39
- data/docs/STITCHER_GUIDE.adoc +12 -2
- data/lib/fontisan/converters/svg_generator.rb +30 -82
- data/lib/fontisan/sfnt_font.rb +12 -2
- data/lib/fontisan/stitcher/format_metadata.rb +51 -0
- data/lib/fontisan/stitcher/partition_strategy/base.rb +32 -3
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +429 -0
- data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +59 -29
- 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 +89 -8
- data/lib/fontisan/stitcher.rb +9 -27
- data/lib/fontisan/subset/table_subsetter.rb +123 -5
- data/lib/fontisan/svg/font_generator.rb +2 -2
- data/lib/fontisan/svg/glyph_generator.rb +44 -9
- data/lib/fontisan/tasks/fixture_downloader.rb +162 -0
- data/lib/fontisan/tasks.rb +11 -0
- data/lib/fontisan/unicode/plane.rb +0 -12
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +7 -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
|
|
@@ -15,14 +15,49 @@ module Fontisan
|
|
|
15
15
|
# #bitmap_mode. When a source is :cbdt, the Stitcher propagates
|
|
16
16
|
# the raw CBDT/CBLC tables into the output instead of extracting
|
|
17
17
|
# outlines. The glyph data lives in the bitmap tables, not in glyf.
|
|
18
|
+
#
|
|
19
|
+
# = Codepoint remap
|
|
20
|
+
#
|
|
21
|
+
# Some donor fonts ship glyphs at non-canonical codepoints — a
|
|
22
|
+
# keyboard-mapped font whose "A" is at U+0041, or a PUA-allocated
|
|
23
|
+
# pre-Unicode font. Pass a +remap:+ hash at construction to expose
|
|
24
|
+
# those glyphs at their canonical codepoints without mutating the
|
|
25
|
+
# donor font's cmap:
|
|
26
|
+
#
|
|
27
|
+
# Source.new(font, remap: { 0x41 => 0x11DB0 })
|
|
28
|
+
#
|
|
29
|
+
# With a remap set, the source answers +gid_for_codepoint+ for the
|
|
30
|
+
# *target* codepoints (0x11DB0 in the example) by translating them
|
|
31
|
+
# to the donor's *source* codepoints (0x41). Source codepoints not
|
|
32
|
+
# listed in the remap are hidden — only the remapped coverage is
|
|
33
|
+
# exposed. This matches the typical "use this donor for exactly
|
|
34
|
+
# these output characters" intent, and avoids leaking the donor's
|
|
35
|
+
# ASCII/PUA noise into the output.
|
|
18
36
|
class Source
|
|
19
37
|
MAX_COMPOUND_DEPTH = 32
|
|
20
38
|
|
|
21
|
-
attr_reader :font
|
|
39
|
+
attr_reader :font, :remap
|
|
22
40
|
|
|
23
|
-
|
|
41
|
+
# @param font [TrueTypeFont, OpenTypeFont, Ufo::Font] donor font
|
|
42
|
+
# @param remap [Hash{Integer=>Integer}, nil] optional
|
|
43
|
+
# {source_codepoint => target_codepoint}. When present (even
|
|
44
|
+
# empty), source codepoints not listed are hidden from
|
|
45
|
+
# lookups. Pass +nil+ (the default) for the passthrough
|
|
46
|
+
# behavior — every donor codepoint is exposed as-is.
|
|
47
|
+
def initialize(font, remap: nil)
|
|
24
48
|
@font = font
|
|
25
49
|
@bin_data_cache = nil
|
|
50
|
+
|
|
51
|
+
# Distinguish "no remap kwarg" (nil → passthrough) from
|
|
52
|
+
# "explicit empty remap" ({} → drop everything). The latter
|
|
53
|
+
# still flips the source into remapped mode, just with no
|
|
54
|
+
# entries — every donor codepoint gets filtered out.
|
|
55
|
+
return unless remap
|
|
56
|
+
|
|
57
|
+
@remap = remap.to_h.transform_keys(&:to_i).transform_values(&:to_i)
|
|
58
|
+
@inverse_remap = @remap.each_with_object({}) do |(src, target), h|
|
|
59
|
+
h[target] = src
|
|
60
|
+
end
|
|
26
61
|
end
|
|
27
62
|
|
|
28
63
|
# @return [Symbol] :ufo, :ttf, :otf
|
|
@@ -57,12 +92,21 @@ module Fontisan
|
|
|
57
92
|
end
|
|
58
93
|
|
|
59
94
|
# Find the gid for a Unicode codepoint in this source.
|
|
95
|
+
#
|
|
96
|
+
# When +remap+ was set at construction, +codepoint+ is treated as
|
|
97
|
+
# a *target* codepoint — the source codepoint that maps to it via
|
|
98
|
+
# +remap+ is what actually gets looked up. Returns +nil+ for
|
|
99
|
+
# codepoints the remap does not cover.
|
|
100
|
+
#
|
|
60
101
|
# @param codepoint [Integer]
|
|
61
102
|
# @return [Integer, nil]
|
|
62
103
|
def gid_for_codepoint(codepoint)
|
|
104
|
+
src_cp = source_codepoint_for(codepoint)
|
|
105
|
+
return nil unless src_cp
|
|
106
|
+
|
|
63
107
|
case @font
|
|
64
|
-
when Fontisan::Ufo::Font then ufo_gid_for(
|
|
65
|
-
else bin_data_gid_for(
|
|
108
|
+
when Fontisan::Ufo::Font then ufo_gid_for(src_cp)
|
|
109
|
+
else bin_data_gid_for(src_cp)
|
|
66
110
|
end
|
|
67
111
|
end
|
|
68
112
|
|
|
@@ -317,14 +361,51 @@ module Fontisan
|
|
|
317
361
|
end
|
|
318
362
|
|
|
319
363
|
# Add Unicode codepoints from the cmap that map to this gid.
|
|
364
|
+
#
|
|
365
|
+
# When +remap+ was set at construction, the source's raw codepoints
|
|
366
|
+
# are translated through the remap (source→target), and source
|
|
367
|
+
# codepoints not in the remap are dropped. Otherwise the raw cmap
|
|
368
|
+
# codepoints are attached as-is.
|
|
320
369
|
def add_cmap_unicodes(gid, glyph)
|
|
321
|
-
|
|
322
|
-
|
|
370
|
+
effective_codepoints_for_gid(gid).each { |cp| glyph.add_unicode(cp) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Translate a caller-facing codepoint to the donor's own codepoint.
|
|
374
|
+
# Without remap, the two are identical. With remap, the caller's
|
|
375
|
+
# codepoint is the *target*; look up the source codepoint that
|
|
376
|
+
# maps to it. Returns +nil+ if no remap entry covers the target.
|
|
377
|
+
def source_codepoint_for(codepoint)
|
|
378
|
+
return codepoint unless @remap
|
|
323
379
|
|
|
324
|
-
|
|
325
|
-
|
|
380
|
+
@inverse_remap[codepoint]
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Codepoints that should be attached to a glyph at the given gid,
|
|
384
|
+
# after applying the remap (if any).
|
|
385
|
+
def effective_codepoints_for_gid(gid)
|
|
386
|
+
raw_cmap = self.raw_cmap
|
|
387
|
+
raw_cmap.each_with_object([]) do |(src_cp, g), cps|
|
|
388
|
+
next unless g == gid
|
|
389
|
+
|
|
390
|
+
if @remap
|
|
391
|
+
target = @remap[src_cp]
|
|
392
|
+
cps << target if target
|
|
393
|
+
else
|
|
394
|
+
cps << src_cp
|
|
395
|
+
end
|
|
326
396
|
end
|
|
327
397
|
end
|
|
398
|
+
|
|
399
|
+
# The raw cmap hash from the donor's table. Always {src_cp => gid};
|
|
400
|
+
# never remapped. UFO sources return {} (no cmap table).
|
|
401
|
+
def raw_cmap
|
|
402
|
+
cmap = @font.table("cmap")
|
|
403
|
+
return {} unless cmap
|
|
404
|
+
|
|
405
|
+
cmap.unicode_mappings || {}
|
|
406
|
+
rescue StandardError
|
|
407
|
+
{}
|
|
408
|
+
end
|
|
328
409
|
end
|
|
329
410
|
end
|
|
330
411
|
end
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Fontisan
|
|
|
32
32
|
autoload :GlyphLimit, "fontisan/stitcher/glyph_limit"
|
|
33
33
|
autoload :CollectionResult, "fontisan/stitcher/collection_result"
|
|
34
34
|
autoload :SubfontStats, "fontisan/stitcher/collection_result"
|
|
35
|
+
autoload :FormatMetadata, "fontisan/stitcher/format_metadata"
|
|
35
36
|
autoload :PartitionStrategy, "fontisan/stitcher/partition_strategy"
|
|
36
37
|
|
|
37
38
|
# Internal: pairs a compiled loaded font with its stats so
|
|
@@ -51,8 +52,8 @@ module Fontisan
|
|
|
51
52
|
@deduplicate = deduplicate
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
def add_source(label, font)
|
|
55
|
-
@sources[label.to_sym] = Source.new(font)
|
|
55
|
+
def add_source(label, font, remap: nil)
|
|
56
|
+
@sources[label.to_sym] = Source.new(font, remap: remap)
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def include_range(range, from:, into:)
|
|
@@ -94,8 +95,8 @@ module Fontisan
|
|
|
94
95
|
target = build_target_for(subfont)
|
|
95
96
|
GlyphLimit.check!(target.glyphs.size, format: format)
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
metadata = FormatMetadata.resolve(format)
|
|
99
|
+
metadata.compiler_class.new(target).compile(output_path: path)
|
|
99
100
|
|
|
100
101
|
propagate_cbdt_tables(path) if cbdt_source
|
|
101
102
|
path
|
|
@@ -109,7 +110,7 @@ module Fontisan
|
|
|
109
110
|
end
|
|
110
111
|
fonts = compiled.map(&:font)
|
|
111
112
|
|
|
112
|
-
collection_format =
|
|
113
|
+
collection_format = FormatMetadata.resolve(format).collection_format
|
|
113
114
|
Collection::Builder.new(fonts, format: collection_format,
|
|
114
115
|
optimize: true).build_to_file(path)
|
|
115
116
|
|
|
@@ -138,20 +139,6 @@ module Fontisan
|
|
|
138
139
|
cbdts.first
|
|
139
140
|
end
|
|
140
141
|
|
|
141
|
-
def compiler_for(format)
|
|
142
|
-
case format.to_sym
|
|
143
|
-
when :ttf then Ufo::Compile::TtfCompiler
|
|
144
|
-
when :otf then Ufo::Compile::OtfCompiler
|
|
145
|
-
when :otf2 then Ufo::Compile::Otf2Compiler
|
|
146
|
-
else
|
|
147
|
-
raise ArgumentError, "unknown format: #{format.inspect}"
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def collection_format_for(subfont_format)
|
|
152
|
-
subfont_format == :ttf ? :ttc : :otc
|
|
153
|
-
end
|
|
154
|
-
|
|
155
142
|
def build_target_for(subfont_name)
|
|
156
143
|
bindings = @subfonts[subfont_name] || []
|
|
157
144
|
target = Ufo::Font.new
|
|
@@ -161,19 +148,14 @@ module Fontisan
|
|
|
161
148
|
target
|
|
162
149
|
end
|
|
163
150
|
|
|
164
|
-
def compile_subfont_to_loaded_font(subfont_name, format:)
|
|
165
|
-
compile_subfont_with_stats(subfont_name, format: format).font
|
|
166
|
-
end
|
|
167
|
-
|
|
168
151
|
def compile_subfont_with_stats(subfont_name, format:)
|
|
169
152
|
target = build_target_for(subfont_name)
|
|
170
153
|
GlyphLimit.check!(target.glyphs.size, format: format)
|
|
171
154
|
|
|
172
|
-
|
|
155
|
+
metadata = FormatMetadata.resolve(format)
|
|
173
156
|
Dir.mktmpdir do |dir|
|
|
174
|
-
sub_path = File.join(dir, "sub#{subfont_name}#{
|
|
175
|
-
|
|
176
|
-
compiler.new(target).compile(output_path: sub_path)
|
|
157
|
+
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
158
|
+
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
177
159
|
propagate_cbdt_tables(sub_path) if cbdt_source
|
|
178
160
|
|
|
179
161
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
@@ -417,12 +417,130 @@ module Fontisan
|
|
|
417
417
|
# Creates a minimal cmap table with format 4 subtable for BMP
|
|
418
418
|
# and format 12 for supplementary planes if needed.
|
|
419
419
|
#
|
|
420
|
-
# @param mappings [Hash<Integer, Integer>] Char code => glyph ID
|
|
420
|
+
# @param mappings [Hash<Integer, Integer>] Char code => new glyph ID
|
|
421
421
|
# @return [String] Binary cmap data
|
|
422
|
-
def build_cmap_binary(
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
|
|
422
|
+
def build_cmap_binary(mappings)
|
|
423
|
+
# Edge case: empty mappings (e.g., block with no covered chars).
|
|
424
|
+
# Emit a minimal valid cmap with one format 4 subtable mapping
|
|
425
|
+
# only U+0000 → .notdef so the table isn't empty.
|
|
426
|
+
mappings = { 0 => 0 } if mappings.empty?
|
|
427
|
+
|
|
428
|
+
bmp = mappings.select { |cp, _| cp <= 0xFFFF }
|
|
429
|
+
supp = mappings.select { |cp, _| cp > 0xFFFF }
|
|
430
|
+
|
|
431
|
+
subtables = []
|
|
432
|
+
records = [] # [platform_id, encoding_id, subtable_index]
|
|
433
|
+
|
|
434
|
+
unless bmp.empty?
|
|
435
|
+
subtables << build_cmap_format_4(bmp)
|
|
436
|
+
idx = subtables.size - 1
|
|
437
|
+
records << [3, 1, idx] # Windows BMP
|
|
438
|
+
records << [0, 3, idx] # Unicode BMP
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
unless supp.empty?
|
|
442
|
+
# Format 12 covers both BMP and supplementary — include all
|
|
443
|
+
# mappings so a single subtable covers the full range.
|
|
444
|
+
subtables << build_cmap_format_12(mappings)
|
|
445
|
+
idx = subtables.size - 1
|
|
446
|
+
records << [3, 10, idx] # Windows UCS-4
|
|
447
|
+
records << [0, 4, idx] # Unicode full
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Header: version (uint16) + numTables (uint16)
|
|
451
|
+
num_tables = records.size
|
|
452
|
+
header = [0, num_tables].pack("nn")
|
|
453
|
+
|
|
454
|
+
# Encoding records start immediately after the header.
|
|
455
|
+
# Each record is 8 bytes; subtables follow.
|
|
456
|
+
subtable_base = 4 + (8 * num_tables)
|
|
457
|
+
|
|
458
|
+
offsets = []
|
|
459
|
+
running = subtable_base
|
|
460
|
+
subtables.each do |st|
|
|
461
|
+
offsets << running
|
|
462
|
+
running += st.bytesize
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
record_bytes = +""
|
|
466
|
+
records.each do |pid, eid, idx|
|
|
467
|
+
record_bytes << [pid, eid, offsets[idx]].pack("nnN")
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
header + record_bytes + subtables.join
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Format 4 subtable: segment-mapping with idDelta, suitable for
|
|
474
|
+
# BMP codepoints (U+0000..U+FFFF). Builds compact segments where
|
|
475
|
+
# consecutive codepoints map to consecutive glyph IDs.
|
|
476
|
+
def build_cmap_format_4(bmp_mappings)
|
|
477
|
+
segments = coalesce_segments(bmp_mappings)
|
|
478
|
+
# Mandatory final segment: U+FFFF → gid 0 (per OpenType spec).
|
|
479
|
+
segments << { start_cp: 0xFFFF, end_cp: 0xFFFF, start_gid: 0 }
|
|
480
|
+
|
|
481
|
+
seg_count = segments.size
|
|
482
|
+
seg_count_x2 = seg_count * 2
|
|
483
|
+
search_range = 2**Math.log2(seg_count).floor * 2
|
|
484
|
+
search_range = 2 if search_range < 2
|
|
485
|
+
entry_selector = Math.log2(search_range / 2).to_i
|
|
486
|
+
range_shift = seg_count_x2 - search_range
|
|
487
|
+
|
|
488
|
+
end_codes = segments.map { |s| s[:end_cp] }
|
|
489
|
+
start_codes = segments.map { |s| s[:start_cp] }
|
|
490
|
+
# idDelta is int16 stored as uint16 (two's complement). For a
|
|
491
|
+
# sequential segment, idDelta = (start_gid - start_cp) & 0xFFFF.
|
|
492
|
+
id_deltas = segments.map { |s| (s[:start_gid] - s[:start_cp]) & 0xFFFF }
|
|
493
|
+
id_range_offsets = [0] * seg_count
|
|
494
|
+
|
|
495
|
+
subtable = +""
|
|
496
|
+
subtable << [4, 0, 0, seg_count_x2,
|
|
497
|
+
search_range, entry_selector, range_shift].pack("n*")
|
|
498
|
+
subtable << end_codes.pack("n*")
|
|
499
|
+
subtable << [0].pack("n") # reservedPad
|
|
500
|
+
subtable << start_codes.pack("n*")
|
|
501
|
+
subtable << id_deltas.pack("n*")
|
|
502
|
+
subtable << id_range_offsets.pack("n*")
|
|
503
|
+
|
|
504
|
+
# Patch the length field (was placeholder 0).
|
|
505
|
+
subtable[2, 2] = [subtable.bytesize].pack("n")
|
|
506
|
+
subtable
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Format 12 subtable: segmented coverage for full Unicode range.
|
|
510
|
+
# Simpler than format 4 — just (start_char, end_char, start_gid)
|
|
511
|
+
# triples with no delta/offset indirection.
|
|
512
|
+
def build_cmap_format_12(all_mappings)
|
|
513
|
+
groups = coalesce_segments(all_mappings)
|
|
514
|
+
num_groups = groups.size
|
|
515
|
+
|
|
516
|
+
subtable = +""
|
|
517
|
+
subtable << [12, 0, 0, 0, num_groups].pack("nnNNN")
|
|
518
|
+
groups.each do |g|
|
|
519
|
+
subtable << [g[:start_cp], g[:end_cp], g[:start_gid]].pack("NNN")
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Patch the length field (was placeholder 0). Total length is
|
|
523
|
+
# 16-byte header + 12 bytes per group.
|
|
524
|
+
subtable[4, 4] = [subtable.bytesize].pack("N")
|
|
525
|
+
subtable
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Group codepoints into consecutive runs where both codepoint AND
|
|
529
|
+
# glyph ID are sequential. Each run becomes one segment/group.
|
|
530
|
+
def coalesce_segments(mappings)
|
|
531
|
+
sorted = mappings.sort_by { |cp, _| cp }
|
|
532
|
+
segments = []
|
|
533
|
+
current = nil
|
|
534
|
+
sorted.each do |cp, gid|
|
|
535
|
+
if current && cp == current[:end_cp] + 1 && gid == current[:start_gid] + (cp - current[:start_cp])
|
|
536
|
+
current[:end_cp] = cp
|
|
537
|
+
else
|
|
538
|
+
segments << current if current
|
|
539
|
+
current = { start_cp: cp, end_cp: cp, start_gid: gid }
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
segments << current if current
|
|
543
|
+
segments
|
|
426
544
|
end
|
|
427
545
|
|
|
428
546
|
# Build post table version 3.0 (no glyph names)
|
|
@@ -28,7 +28,7 @@ module Fontisan
|
|
|
28
28
|
# @return [TrueTypeFont, OpenTypeFont] Font instance
|
|
29
29
|
attr_reader :font
|
|
30
30
|
|
|
31
|
-
# @return [Hash] Glyph data map (glyph_id => {outline,
|
|
31
|
+
# @return [Hash] Glyph data map (glyph_id => {outline, codepoints, name, advance})
|
|
32
32
|
attr_reader :glyph_data
|
|
33
33
|
|
|
34
34
|
# @return [Hash] Generation options
|
|
@@ -179,7 +179,7 @@ module Fontisan
|
|
|
179
179
|
|
|
180
180
|
glyph_generator.generate_glyph_xml(
|
|
181
181
|
data[:outline],
|
|
182
|
-
|
|
182
|
+
codepoints: data[:codepoints],
|
|
183
183
|
glyph_name: data[:name],
|
|
184
184
|
advance_width: data[:advance],
|
|
185
185
|
indent: " ",
|