fontisan 0.3.1 → 0.4.0
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/lib/fontisan/error.rb +7 -0
- data/lib/fontisan/stitcher/source.rb +41 -0
- data/lib/fontisan/stitcher.rb +117 -12
- data/lib/fontisan/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz: '
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '020862878eef5569b82edb88989afdc3b7c21d3f01d19b605132af70cabe524f'
|
|
4
|
+
data.tar.gz: d8d685a687c94f987835e440619aca344a615368ea78d6caf6a882c34c2900d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a98055b753907dedfd44fd89c61fd3d5d797d405d702e65730bae1e3299665354f385b135529ca6b48708672e37cfa0e10a7b518ce0b624ccb46fae9459cfb22
|
|
7
|
+
data.tar.gz: 46af4e780f4a1786501e2b874d4b439614c212b839b0b398316806a2baa9348b67c8e4db3e63b85142d79ae015df3f093db957f055ddbd3f4955268caa2c1dfa
|
data/lib/fontisan/error.rb
CHANGED
|
@@ -199,6 +199,13 @@ module Fontisan
|
|
|
199
199
|
end
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
# Multiple CBDT sources detected in Stitcher
|
|
203
|
+
#
|
|
204
|
+
# Raised when more than one CBDT/CBLC source is registered with the
|
|
205
|
+
# Stitcher. Only single-source CBDT passthrough is supported; merging
|
|
206
|
+
# CBDT/CBLC across multiple sources requires a dedicated rebuild.
|
|
207
|
+
class MultipleCbdtSourcesError < Error; end
|
|
208
|
+
|
|
202
209
|
# Variation data corrupted (for use in data_extractor)
|
|
203
210
|
#
|
|
204
211
|
# Raised when extracted variation data appears corrupted.
|
|
@@ -10,6 +10,11 @@ module Fontisan
|
|
|
10
10
|
# via Ufo::Convert::FromBinData on first glyph access, then cached.
|
|
11
11
|
# This is O(n) in donor glyph count but amortized across all
|
|
12
12
|
# codepoint extractions from that donor.
|
|
13
|
+
#
|
|
14
|
+
# CBDT/CBLC sources (e.g. NotoColorEmoji) are detected via
|
|
15
|
+
# #bitmap_mode. When a source is :cbdt, the Stitcher propagates
|
|
16
|
+
# the raw CBDT/CBLC tables into the output instead of extracting
|
|
17
|
+
# outlines. The glyph data lives in the bitmap tables, not in glyf.
|
|
13
18
|
class Source
|
|
14
19
|
attr_reader :font
|
|
15
20
|
|
|
@@ -28,6 +33,27 @@ module Fontisan
|
|
|
28
33
|
end
|
|
29
34
|
end
|
|
30
35
|
|
|
36
|
+
# Detect how this source stores glyph data.
|
|
37
|
+
#
|
|
38
|
+
# - :glyf — TrueType outlines (glyf table present)
|
|
39
|
+
# - :cbdt — Color bitmaps (CBDT + CBLC tables, no glyf)
|
|
40
|
+
# - :mixed — Both glyf and CBDT
|
|
41
|
+
# - :none — UFO source or neither table present
|
|
42
|
+
#
|
|
43
|
+
# @return [Symbol]
|
|
44
|
+
def bitmap_mode
|
|
45
|
+
return :none if @font.is_a?(Fontisan::Ufo::Font)
|
|
46
|
+
return :none unless @font.respond_to?(:has_table?)
|
|
47
|
+
|
|
48
|
+
has_cbdt = @font.has_table?("CBDT") && @font.has_table?("CBLC")
|
|
49
|
+
has_glyf = @font.has_table?("glyf") || @font.has_table?("CFF ")
|
|
50
|
+
return :mixed if has_cbdt && has_glyf
|
|
51
|
+
return :cbdt if has_cbdt
|
|
52
|
+
return :glyf if has_glyf
|
|
53
|
+
|
|
54
|
+
:none
|
|
55
|
+
end
|
|
56
|
+
|
|
31
57
|
# Find the gid for a Unicode codepoint in this source.
|
|
32
58
|
# @param codepoint [Integer]
|
|
33
59
|
# @return [Integer, nil]
|
|
@@ -39,6 +65,9 @@ module Fontisan
|
|
|
39
65
|
end
|
|
40
66
|
|
|
41
67
|
# Extract a glyph by gid.
|
|
68
|
+
#
|
|
69
|
+
# For CBDT sources, returns a placeholder glyph (no contours)
|
|
70
|
+
# since the glyph data lives in the bitmap tables, not outlines.
|
|
42
71
|
# @param gid [Integer]
|
|
43
72
|
# @return [Fontisan::Ufo::Glyph, nil]
|
|
44
73
|
def glyph_for_gid(gid)
|
|
@@ -48,6 +77,18 @@ module Fontisan
|
|
|
48
77
|
end
|
|
49
78
|
end
|
|
50
79
|
|
|
80
|
+
# Raw table bytes from the loaded font (for passthrough).
|
|
81
|
+
# @param tag [String] 4-byte table tag (e.g. "CBDT", "CBLC")
|
|
82
|
+
# @return [String, nil] raw bytes or nil if table not present
|
|
83
|
+
def raw_table_bytes(tag)
|
|
84
|
+
sfnt_table = @font.table(tag)
|
|
85
|
+
return nil unless sfnt_table
|
|
86
|
+
|
|
87
|
+
sfnt_table.raw_data
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
51
92
|
private
|
|
52
93
|
|
|
53
94
|
# ---------- UFO source ----------
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -70,12 +70,23 @@ module Fontisan
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Write the stitched font to disk.
|
|
73
|
+
#
|
|
74
|
+
# For CBDT/CBLC sources (e.g. NotoColorEmoji), the raw CBDT and
|
|
75
|
+
# CBLC tables are copied byte-for-byte into the output. This works
|
|
76
|
+
# because CBDT-mode glyphs are processed first (GIDs 0..N-1),
|
|
77
|
+
# matching the source's GID layout. Only one CBDT source is
|
|
78
|
+
# supported; multiple CBDT sources raise MultipleCbdtSourcesError.
|
|
79
|
+
#
|
|
73
80
|
# @param path [String] output file path
|
|
74
81
|
# @param format [Symbol] :ttf or :otf
|
|
75
82
|
def write_to(path, format: :ttf)
|
|
76
83
|
build_target_font
|
|
77
84
|
compiler = compiler_for(format)
|
|
78
|
-
compiler.new(@target)
|
|
85
|
+
compiler_instance = compiler.new(@target)
|
|
86
|
+
compiler_instance.compile(output_path: path)
|
|
87
|
+
|
|
88
|
+
propagate_cbdt_tables(path) if cbdt_source
|
|
89
|
+
|
|
79
90
|
path
|
|
80
91
|
end
|
|
81
92
|
|
|
@@ -96,6 +107,53 @@ module Fontisan
|
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
109
|
|
|
110
|
+
# Find the single CBDT source among registered sources, if any.
|
|
111
|
+
# Raises if more than one CBDT source is present (merge not supported).
|
|
112
|
+
# @return [Source, nil]
|
|
113
|
+
def cbdt_source
|
|
114
|
+
cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
|
|
115
|
+
if cbdts.size > 1
|
|
116
|
+
raise MultipleCbdtSourcesError,
|
|
117
|
+
"multiple CBDT sources not supported (found #{cbdts.size})"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
cbdts.first
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Copy raw CBDT + CBLC table bytes from the CBDT source into the
|
|
124
|
+
# compiled output file. The GIDs must match (CBDT glyphs are at
|
|
125
|
+
# the same GIDs in both source and output because they were added
|
|
126
|
+
# first during build_target_font).
|
|
127
|
+
def propagate_cbdt_tables(path)
|
|
128
|
+
source = cbdt_source
|
|
129
|
+
return unless source
|
|
130
|
+
|
|
131
|
+
compiled = Fontisan::FontLoader.load(path)
|
|
132
|
+
|
|
133
|
+
tables = {}
|
|
134
|
+
compiled.table_names.each do |tag|
|
|
135
|
+
raw = extract_raw_table(compiled, tag)
|
|
136
|
+
tables[tag] = raw if raw
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
cbdt_bytes = source.raw_table_bytes("CBDT")
|
|
140
|
+
cblc_bytes = source.raw_table_bytes("CBLC")
|
|
141
|
+
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
142
|
+
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
143
|
+
|
|
144
|
+
sfnt = tables.key?("CFF ") ? 0x4F54544F : 0x00010000
|
|
145
|
+
Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def extract_raw_table(font, tag)
|
|
149
|
+
sfnt_table = font.table(tag)
|
|
150
|
+
return nil unless sfnt_table
|
|
151
|
+
|
|
152
|
+
sfnt_table.raw_data
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
99
157
|
def compiler_for(format)
|
|
100
158
|
case format.to_sym
|
|
101
159
|
when :ttf then Ufo::Compile::TtfCompiler
|
|
@@ -107,26 +165,28 @@ module Fontisan
|
|
|
107
165
|
|
|
108
166
|
# Walk bindings in codepoint order, assign sequential new gids,
|
|
109
167
|
# copy each glyph into the target font's default layer.
|
|
168
|
+
#
|
|
169
|
+
# When a CBDT source is present, its glyphs are added FIRST (in
|
|
170
|
+
# source GID order) so that the CBLC's GID references remain valid.
|
|
171
|
+
# Glyf-source bindings are processed AFTER, appending new glyphs.
|
|
110
172
|
def assign_gids_and_copy_glyphs
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
|
|
115
|
-
source: notdef_binding[:source],
|
|
116
|
-
donor_gid: 0)
|
|
173
|
+
cbdt = cbdt_source
|
|
174
|
+
|
|
175
|
+
if cbdt
|
|
176
|
+
add_all_cbdt_glyphs(cbdt)
|
|
117
177
|
else
|
|
118
|
-
|
|
119
|
-
@target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
178
|
+
add_notdef_from_bindings
|
|
120
179
|
end
|
|
121
180
|
|
|
122
181
|
sorted_bindings.each do |binding|
|
|
123
|
-
next if binding[:donor_gid].zero?
|
|
182
|
+
next if binding[:donor_gid].zero?
|
|
183
|
+
|
|
184
|
+
# Skip bindings from the CBDT source — its glyphs are already added.
|
|
185
|
+
next if cbdt && binding[:source].equal?(cbdt)
|
|
124
186
|
|
|
125
187
|
glyph = binding[:source].glyph_for_gid(binding[:donor_gid])
|
|
126
188
|
next unless glyph
|
|
127
189
|
|
|
128
|
-
# If multiple codepoints map to the same glyph, only the first
|
|
129
|
-
# binding creates the glyph; subsequent ones add unicode entries.
|
|
130
190
|
if @target.glyphs.key?(glyph.name)
|
|
131
191
|
add_extra_unicode(glyph.name, binding[:codepoint])
|
|
132
192
|
else
|
|
@@ -159,6 +219,51 @@ module Fontisan
|
|
|
159
219
|
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
160
220
|
end
|
|
161
221
|
|
|
222
|
+
# Add .notdef at GID 0 from the first binding that references gid 0.
|
|
223
|
+
# Falls back to a synthesized empty .notdef if none found.
|
|
224
|
+
def add_notdef_from_bindings
|
|
225
|
+
notdef_binding = @bindings.find { |b| b[:donor_gid].zero? }
|
|
226
|
+
if notdef_binding
|
|
227
|
+
copy_glyph_into(@target, name: ".notdef",
|
|
228
|
+
source: notdef_binding[:source],
|
|
229
|
+
donor_gid: 0)
|
|
230
|
+
else
|
|
231
|
+
@target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Add ALL glyphs from a CBDT source in source GID order. This
|
|
236
|
+
# ensures the CBLC's GID references remain valid in the output
|
|
237
|
+
# without rewriting the table. Each glyph gets a placeholder
|
|
238
|
+
# (no contours) since the bitmap data is in CBDT, not glyf.
|
|
239
|
+
def add_all_cbdt_glyphs(source)
|
|
240
|
+
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
241
|
+
if ufo
|
|
242
|
+
ufo.glyphs.each_value { |g| @target.layers.default_layer.add(clone_glyph(g, name: g.name)) }
|
|
243
|
+
return
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# For loaded TTF/OTF sources, iterate via cmap to get glyph names.
|
|
247
|
+
# CBDT fonts (like NotoColorEmoji) may have thousands of glyphs;
|
|
248
|
+
# we add them all as placeholders.
|
|
249
|
+
maxp = source.font.table("maxp")
|
|
250
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
251
|
+
cmap = source.font.table("cmap")
|
|
252
|
+
mappings = cmap&.unicode_mappings || {}
|
|
253
|
+
|
|
254
|
+
# Build gid → [codepoints] from cmap
|
|
255
|
+
gid_cps = Hash.new { |h, k| h[k] = [] }
|
|
256
|
+
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
257
|
+
|
|
258
|
+
num_glyphs.times do |gid|
|
|
259
|
+
name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
260
|
+
glyph = Ufo::Glyph.new(name: name)
|
|
261
|
+
glyph.width = 0
|
|
262
|
+
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
263
|
+
@target.layers.default_layer.add(glyph)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
162
267
|
# Deep-copy a glyph with a new name. Used so multiple target
|
|
163
268
|
# glyphs can share the same source outline without aliasing.
|
|
164
269
|
def clone_glyph(original, name:)
|
data/lib/fontisan/version.rb
CHANGED