fontisan 0.4.0 → 0.4.1
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 +30 -0
- data/lib/fontisan/stitcher/source.rb +123 -19
- 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: 58c1e2768684af1fcdc55fdc2d5b3e47bbd89b0277794dc64ff627ef8d571c3d
|
|
4
|
+
data.tar.gz: 8d1da959f850b3872acb01e96deb098ea2cbb0d596a0923b5db4960edfd818bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e46507d14358098c09f5ee2a3584c601e3d28e6a762f506962007445fb9fcd4432db7f591601575fac87d9cad93ae7c57d2c464728888e33dd6b242387c5611
|
|
7
|
+
data.tar.gz: 1e2dc941fe06dc9769aad1477c970ac48e799d62f3340a08448c0528290562ce2f9ccfd0e021615ebc57a26bcdd19773fd51135d4d214ef31e2a927576c7d06f
|
data/README.adoc
CHANGED
|
@@ -2475,6 +2475,36 @@ fontisan stitch \
|
|
|
2475
2475
|
--output stitched.ttf
|
|
2476
2476
|
----
|
|
2477
2477
|
|
|
2478
|
+
=== Color emoji (CBDT/CBLC passthrough)
|
|
2479
|
+
|
|
2480
|
+
Sources with CBDT/CBLC tables (e.g. NotoColorEmoji) can be used as
|
|
2481
|
+
Stitcher sources. The bitmap data is propagated byte-for-byte from the
|
|
2482
|
+
source into the output. The Stitcher automatically detects the source's
|
|
2483
|
+
storage mode via `bitmap_mode`:
|
|
2484
|
+
|
|
2485
|
+
[source,ruby]
|
|
2486
|
+
----
|
|
2487
|
+
stitcher = Fontisan::Stitcher.new
|
|
2488
|
+
stitcher.add_source(:text, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
2489
|
+
stitcher.add_source(:emoji, Fontisan::FontLoader.load("NotoColorEmoji.ttf"))
|
|
2490
|
+
|
|
2491
|
+
# All emoji glyphs from the CBDT source are added; the CBLC table
|
|
2492
|
+
# is copied byte-for-byte and references the right GIDs.
|
|
2493
|
+
stitcher.include_range(0x41..0x5A, from: :text)
|
|
2494
|
+
stitcher.include_range(0x1F600..0x1F64F, from: :emoji)
|
|
2495
|
+
|
|
2496
|
+
stitcher.write_to("out.ttf", format: :ttf)
|
|
2497
|
+
# → out.ttf has glyf + CBDT + CBLC, emoji renders correctly
|
|
2498
|
+
----
|
|
2499
|
+
|
|
2500
|
+
**Constraints:**
|
|
2501
|
+
- Only ONE CBDT source per Stitcher (multiple raise
|
|
2502
|
+
`Fontisan::MultipleCbdtSourcesError`)
|
|
2503
|
+
- The CBDT source is processed first (GIDs 0..N-1) so the CBLC's GID
|
|
2504
|
+
references remain valid in the output
|
|
2505
|
+
- Multi-source CBDT merge is tracked as TODO (REVIEW
|
|
2506
|
+
`TODO.full/ufo/23-cbdt-passthrough.md`)
|
|
2507
|
+
|
|
2478
2508
|
|
|
2479
2509
|
== Testing
|
|
2480
2510
|
|
|
@@ -6,10 +6,10 @@ module Fontisan
|
|
|
6
6
|
# extraction API used by the selectors.
|
|
7
7
|
#
|
|
8
8
|
# For UFO sources, glyphs are accessed by name directly. For TTF
|
|
9
|
-
# or OTF sources,
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# or OTF sources, individual glyphs are extracted on demand from
|
|
10
|
+
# the BinData tables (glyf/loca/head for TTF, CFF for OTF). This
|
|
11
|
+
# is O(1) per glyph rather than the previous O(n) full-donor
|
|
12
|
+
# conversion.
|
|
13
13
|
#
|
|
14
14
|
# CBDT/CBLC sources (e.g. NotoColorEmoji) are detected via
|
|
15
15
|
# #bitmap_mode. When a source is :cbdt, the Stitcher propagates
|
|
@@ -20,7 +20,7 @@ module Fontisan
|
|
|
20
20
|
|
|
21
21
|
def initialize(font)
|
|
22
22
|
@font = font
|
|
23
|
-
@
|
|
23
|
+
@bin_data_cache = nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# @return [Symbol] :ufo, :ttf, :otf
|
|
@@ -66,14 +66,19 @@ module Fontisan
|
|
|
66
66
|
|
|
67
67
|
# Extract a glyph by gid.
|
|
68
68
|
#
|
|
69
|
+
# For TTF/OTF sources, this is O(1) per glyph: it parses just
|
|
70
|
+
# the requested glyph from glyf/CFF on demand, not the entire
|
|
71
|
+
# donor. The full-donor conversion is avoided entirely.
|
|
72
|
+
#
|
|
69
73
|
# For CBDT sources, returns a placeholder glyph (no contours)
|
|
70
|
-
# since the glyph data
|
|
74
|
+
# since the glyph data is in the bitmap tables, not outlines.
|
|
75
|
+
#
|
|
71
76
|
# @param gid [Integer]
|
|
72
77
|
# @return [Fontisan::Ufo::Glyph, nil]
|
|
73
78
|
def glyph_for_gid(gid)
|
|
74
79
|
case @font
|
|
75
80
|
when Fontisan::Ufo::Font then ufo_glyph_at(gid)
|
|
76
|
-
else
|
|
81
|
+
else extract_single_glyph_from_bindata(gid)
|
|
77
82
|
end
|
|
78
83
|
end
|
|
79
84
|
|
|
@@ -89,6 +94,15 @@ module Fontisan
|
|
|
89
94
|
nil
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
# Width of a specific glyph (extracted from hmtx).
|
|
98
|
+
# Falls back to 0 if hmtx is missing.
|
|
99
|
+
# @param gid [Integer]
|
|
100
|
+
# @return [Integer]
|
|
101
|
+
def glyph_width(gid)
|
|
102
|
+
widths = bin_data_widths
|
|
103
|
+
widths[gid] || 0
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
private
|
|
93
107
|
|
|
94
108
|
# ---------- UFO source ----------
|
|
@@ -108,7 +122,7 @@ module Fontisan
|
|
|
108
122
|
@font.glyph(name)
|
|
109
123
|
end
|
|
110
124
|
|
|
111
|
-
# ---------- TTF/OTF source ----------
|
|
125
|
+
# ---------- TTF/OTF source: O(1) per-glyph extraction ----------
|
|
112
126
|
|
|
113
127
|
def bin_data_gid_for(codepoint)
|
|
114
128
|
cmap = @font.table("cmap")
|
|
@@ -117,21 +131,111 @@ module Fontisan
|
|
|
117
131
|
cmap.unicode_mappings[codepoint]
|
|
118
132
|
end
|
|
119
133
|
|
|
120
|
-
# Lazily
|
|
121
|
-
#
|
|
122
|
-
def
|
|
123
|
-
|
|
134
|
+
# Lazily parse the relevant BinData tables. Cached so we only
|
|
135
|
+
# pay the parse cost once per source.
|
|
136
|
+
def bin_data_cache
|
|
137
|
+
@bin_data_cache ||= parse_bin_data_tables
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_bin_data_tables
|
|
141
|
+
cache = { head: @font.table("head") }
|
|
142
|
+
|
|
143
|
+
if @font.has_table?("glyf")
|
|
144
|
+
cache[:loca] = @font.table("loca")
|
|
145
|
+
cache[:glyf] = @font.table("glyf")
|
|
146
|
+
# loca needs head's index_to_loc_format to size its offsets
|
|
147
|
+
if cache[:loca].respond_to?(:parse_with_context) && cache[:head]
|
|
148
|
+
cache[:loca].parse_with_context(
|
|
149
|
+
cache[:head].index_to_loc_format,
|
|
150
|
+
@font.table("maxp")&.num_glyphs || 0,
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
cache
|
|
156
|
+
end
|
|
124
157
|
|
|
125
|
-
|
|
158
|
+
# Build {gid → advance_width} from hmtx (cached).
|
|
159
|
+
def bin_data_widths
|
|
160
|
+
@bin_data_widths ||= build_bin_data_widths
|
|
126
161
|
end
|
|
127
162
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
163
|
+
def build_bin_data_widths
|
|
164
|
+
widths = {}
|
|
165
|
+
hmtx = @font.table("hmtx")
|
|
166
|
+
return widths unless hmtx
|
|
167
|
+
|
|
168
|
+
hhea = @font.table("hhea")
|
|
169
|
+
maxp = @font.table("maxp")
|
|
170
|
+
num_h_metrics = hhea&.number_of_h_metrics || 1
|
|
171
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
172
|
+
|
|
173
|
+
if hmtx.respond_to?(:parse_with_context)
|
|
174
|
+
hmtx.parse_with_context(num_h_metrics, num_glyphs)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
num_glyphs.times do |gid|
|
|
178
|
+
metric = hmtx.respond_to?(:metric_for) ? hmtx.metric_for(gid) : nil
|
|
179
|
+
widths[gid] = metric ? metric[:advance_width] : 0
|
|
180
|
+
rescue StandardError
|
|
181
|
+
widths[gid] = 0
|
|
182
|
+
end
|
|
183
|
+
widths
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Extract a single glyph by gid, parsing just the relevant bytes.
|
|
187
|
+
# O(1) per call (after the first call's table-parsing overhead).
|
|
188
|
+
def extract_single_glyph_from_bindata(gid)
|
|
189
|
+
cache = bin_data_cache
|
|
190
|
+
|
|
191
|
+
if cache[:glyf] && cache[:loca] && cache[:head]
|
|
192
|
+
extract_truetype_glyph(gid, cache)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def extract_truetype_glyph(gid, cache)
|
|
197
|
+
simple = cache[:glyf].glyph_for(gid, cache[:loca], cache[:head])
|
|
198
|
+
return nil unless simple
|
|
199
|
+
return nil unless simple.respond_to?(:simple?) && simple.simple?
|
|
200
|
+
|
|
201
|
+
name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
202
|
+
glyph = Fontisan::Ufo::Glyph.new(name: name)
|
|
203
|
+
glyph.width = glyph_width(gid)
|
|
204
|
+
copy_simple_contours(simple, glyph)
|
|
205
|
+
add_cmap_unicodes(gid, glyph)
|
|
206
|
+
glyph
|
|
207
|
+
rescue StandardError
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Copy a SimpleGlyph's contours + points into a Ufo::Glyph.
|
|
212
|
+
def copy_simple_contours(simple, ufo_glyph)
|
|
213
|
+
num_contours = simple.end_pts_of_contours&.size || 0
|
|
214
|
+
return if num_contours.zero?
|
|
215
|
+
|
|
216
|
+
num_contours.times do |ci|
|
|
217
|
+
points = simple.points_for_contour(ci)
|
|
218
|
+
next unless points && !points.empty?
|
|
219
|
+
|
|
220
|
+
ufo_points = points.map do |pt|
|
|
221
|
+
x = pt[:x] || pt["x"]
|
|
222
|
+
y = pt[:y] || pt["y"]
|
|
223
|
+
on_curve = pt[:on_curve].nil? || pt[:on_curve]
|
|
224
|
+
type = on_curve ? "line" : "offcurve"
|
|
225
|
+
Fontisan::Ufo::Point.new(x: x.to_f, y: y.to_f, type: type)
|
|
226
|
+
end
|
|
227
|
+
ufo_glyph.add_contour(Fontisan::Ufo::Contour.new(ufo_points))
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Add Unicode codepoints from the cmap that map to this gid.
|
|
232
|
+
def add_cmap_unicodes(gid, glyph)
|
|
233
|
+
cmap = @font.table("cmap")
|
|
234
|
+
return unless cmap
|
|
133
235
|
|
|
134
|
-
|
|
236
|
+
(cmap.unicode_mappings || {}).each do |cp, g|
|
|
237
|
+
glyph.add_unicode(cp) if g == gid
|
|
238
|
+
end
|
|
135
239
|
end
|
|
136
240
|
end
|
|
137
241
|
end
|
data/lib/fontisan/version.rb
CHANGED