fontisan 0.4.22 → 0.4.23
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/.rubocop_todo.yml +5 -0
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +3 -33
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +2 -0
- data/lib/fontisan/subset/shared_state.rb +42 -0
- data/lib/fontisan/subset/subset_context.rb +20 -0
- data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
- data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
- data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
- data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +145 -0
- data/lib/fontisan/subset/table_strategy/head.rb +42 -0
- data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
- data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
- data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
- data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
- data/lib/fontisan/subset/table_strategy/name.rb +19 -0
- data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
- data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
- data/lib/fontisan/subset/table_strategy/post.rb +37 -0
- data/lib/fontisan/subset/table_strategy.rb +75 -0
- data/lib/fontisan/subset/table_subsetter.rb +49 -616
- data/lib/fontisan/subset.rb +3 -0
- data/lib/fontisan/tables/cbdt.rb +66 -121
- data/lib/fontisan/tables/cblc.rb +110 -231
- data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
- data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
- data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
- data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
- data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
- data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
- data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
- data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
- data/lib/fontisan/tables.rb +11 -0
- data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
- data/lib/fontisan/ufo/layer.rb +42 -2
- data/lib/fontisan/ufo.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- metadata +35 -2
|
@@ -2,675 +2,108 @@
|
|
|
2
2
|
|
|
3
3
|
module Fontisan
|
|
4
4
|
module Subset
|
|
5
|
-
# Table-specific subsetting
|
|
5
|
+
# Table-specific subsetting dispatcher.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Per-table subsetting logic lives in [TableStrategy] classes (one
|
|
8
|
+
# per OpenType tag). This class:
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# -
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# - name: Pass through (no subsetting needed)
|
|
18
|
-
# - head: Update checksum adjustment (handled by FontWriter)
|
|
19
|
-
# - OS/2: Optionally prune Unicode ranges
|
|
10
|
+
# 1. Holds the inputs to a subsetting run (font, mapping, options)
|
|
11
|
+
# plus the cross-strategy [SharedState] (glyf/loca data, bbox,
|
|
12
|
+
# max advance, color bitmap subsetter cache).
|
|
13
|
+
# 2. Dispatches `subset_table(tag, table)` to the right strategy
|
|
14
|
+
# via [TableStrategy.for(tag)].
|
|
15
|
+
# 3. Keeps `subset_<tag>` wrappers for backward compatibility with
|
|
16
|
+
# specs that exercise a single strategy in isolation.
|
|
20
17
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
18
|
+
# Adding a new subsetting strategy does NOT require editing this
|
|
19
|
+
# file — register it in `lib/fontisan/subset/table_strategy.rb`.
|
|
23
20
|
#
|
|
24
21
|
# @example Subset a single table
|
|
25
22
|
# subsetter = TableSubsetter.new(font, mapping, options)
|
|
26
|
-
# maxp_data = subsetter.
|
|
27
|
-
#
|
|
28
|
-
# @example Subset all tables
|
|
29
|
-
# subsetter = TableSubsetter.new(font, mapping, options)
|
|
30
|
-
# subset_tables = {}
|
|
31
|
-
# profile_tables.each do |tag|
|
|
32
|
-
# table = font.table(tag)
|
|
33
|
-
# subset_tables[tag] = subsetter.subset_table(tag, table) if table
|
|
34
|
-
# end
|
|
23
|
+
# maxp_data = subsetter.subset_table("maxp", font.table("maxp"))
|
|
35
24
|
class TableSubsetter
|
|
36
|
-
#
|
|
37
|
-
# @return [TrueTypeFont, OpenTypeFont]
|
|
25
|
+
# @return [SfntFont]
|
|
38
26
|
attr_reader :font
|
|
39
27
|
|
|
40
|
-
# Glyph ID mapping (old GID → new GID)
|
|
41
28
|
# @return [GlyphMapping]
|
|
42
29
|
attr_reader :mapping
|
|
43
30
|
|
|
44
|
-
# Subsetting options
|
|
45
31
|
# @return [Options]
|
|
46
32
|
attr_reader :options
|
|
47
33
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# @param mapping [GlyphMapping] Glyph ID mapping
|
|
52
|
-
# @param options [Options] Subsetting options
|
|
34
|
+
# @return [SharedState]
|
|
35
|
+
attr_reader :state
|
|
36
|
+
|
|
53
37
|
def initialize(font, mapping, options)
|
|
54
38
|
@font = font
|
|
55
39
|
@mapping = mapping
|
|
56
40
|
@options = options
|
|
57
|
-
@
|
|
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
|
|
41
|
+
@state = SharedState.new
|
|
61
42
|
end
|
|
62
43
|
|
|
63
|
-
# Subset
|
|
44
|
+
# Subset one table by dispatching to its strategy. Unknown tables
|
|
45
|
+
# fall through to [TableStrategy::PassThrough] which preserves the
|
|
46
|
+
# source bytes verbatim.
|
|
64
47
|
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# @param tag [String] Table tag (e.g., "glyf", "hmtx")
|
|
69
|
-
# @param table [Object] Parsed table object
|
|
70
|
-
# @return [String] Binary data of subset table
|
|
48
|
+
# @param tag [String] OpenType table tag (e.g. "glyf", "CBDT")
|
|
49
|
+
# @param table [Object] parsed table object
|
|
50
|
+
# @return [String] subset table binary bytes
|
|
71
51
|
def subset_table(tag, table)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
when "hhea"
|
|
76
|
-
subset_hhea(table)
|
|
77
|
-
when "hmtx"
|
|
78
|
-
subset_hmtx(table)
|
|
79
|
-
when "loca"
|
|
80
|
-
subset_loca(table)
|
|
81
|
-
when "glyf"
|
|
82
|
-
subset_glyf(table)
|
|
83
|
-
when "cmap"
|
|
84
|
-
subset_cmap(table)
|
|
85
|
-
when "post"
|
|
86
|
-
subset_post(table)
|
|
87
|
-
when "name"
|
|
88
|
-
subset_name(table)
|
|
89
|
-
when "head"
|
|
90
|
-
subset_head(table)
|
|
91
|
-
when "OS/2"
|
|
92
|
-
subset_os2(table)
|
|
93
|
-
else
|
|
94
|
-
# Unknown tables pass through unchanged
|
|
95
|
-
font.table_data[tag]
|
|
96
|
-
end
|
|
52
|
+
TableStrategy.for(tag).call(
|
|
53
|
+
context: subset_context, tag: tag, table: table,
|
|
54
|
+
)
|
|
97
55
|
end
|
|
98
56
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# the subset font.
|
|
103
|
-
#
|
|
104
|
-
# @param table [Maxp] Parsed maxp table
|
|
105
|
-
# @return [String] Binary data of subset maxp table
|
|
106
|
-
def subset_maxp(table)
|
|
107
|
-
data = table.to_binary_s.dup
|
|
108
|
-
|
|
109
|
-
# Update numGlyphs field (at offset 4, uint16)
|
|
110
|
-
data[4, 2] = [mapping.size].pack("n")
|
|
57
|
+
# --- Backward-compat wrappers --------------------------------------
|
|
58
|
+
# Specs and external callers may invoke `subset_<tag>(table)`
|
|
59
|
+
# directly. Each wrapper delegates to the corresponding strategy.
|
|
111
60
|
|
|
112
|
-
|
|
61
|
+
def subset_maxp(table)
|
|
62
|
+
subset_table("maxp", table)
|
|
113
63
|
end
|
|
114
64
|
|
|
115
|
-
# Subset hhea table (update numberOfHMetrics + advanceWidthMax)
|
|
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.
|
|
121
|
-
#
|
|
122
|
-
# @param table [Hhea] Parsed hhea table
|
|
123
|
-
# @param hmtx [Hmtx, nil] Optional parsed hmtx table (for calculating metrics)
|
|
124
|
-
# @return [String] Binary data of subset hhea table
|
|
125
65
|
def subset_hhea(table, hmtx = nil)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# Calculate new numberOfHMetrics
|
|
129
|
-
new_num_h_metrics = if hmtx&.h_metrics
|
|
130
|
-
hmtx.h_metrics.size
|
|
131
|
-
else
|
|
132
|
-
calculate_number_of_h_metrics
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# numberOfHMetrics at offset 34 (uint16)
|
|
136
|
-
data[34, 2] = [new_num_h_metrics].pack("n")
|
|
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
|
-
|
|
147
|
-
data
|
|
66
|
+
_ = hmtx # ignored; the strategy recomputes from source hmtx
|
|
67
|
+
subset_table("hhea", table)
|
|
148
68
|
end
|
|
149
69
|
|
|
150
|
-
# Subset hmtx table (subset horizontal metrics)
|
|
151
|
-
#
|
|
152
|
-
# Builds new hmtx table with metrics for subset glyphs only,
|
|
153
|
-
# preserving the order of the glyph mapping.
|
|
154
|
-
#
|
|
155
|
-
# @param table [Hmtx] Parsed hmtx table
|
|
156
|
-
# @return [String] Binary data of subset hmtx table
|
|
157
70
|
def subset_hmtx(table)
|
|
158
|
-
|
|
159
|
-
unless table.parsed?
|
|
160
|
-
hhea = font.table("hhea")
|
|
161
|
-
maxp = font.table("maxp")
|
|
162
|
-
table.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Build new hmtx data
|
|
166
|
-
data = String.new(encoding: Encoding::BINARY)
|
|
167
|
-
|
|
168
|
-
max_advance = 0
|
|
169
|
-
mapping.old_ids.each do |old_id|
|
|
170
|
-
metric = table.metric_for(old_id)
|
|
171
|
-
next unless metric
|
|
172
|
-
|
|
173
|
-
advance = metric[:advance_width]
|
|
174
|
-
max_advance = advance if advance && advance > max_advance
|
|
175
|
-
data << [advance].pack("n")
|
|
176
|
-
data << [metric[:lsb]].pack("n")
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
@subset_max_advance = max_advance
|
|
180
|
-
data
|
|
71
|
+
subset_table("hmtx", table)
|
|
181
72
|
end
|
|
182
73
|
|
|
183
|
-
# Subset glyf table (subset glyph data)
|
|
184
|
-
#
|
|
185
|
-
# Extracts glyph data for subset glyphs and remaps component
|
|
186
|
-
# references in compound glyphs. Also builds loca offsets.
|
|
187
|
-
#
|
|
188
|
-
# @param table [Glyf] Parsed glyf table
|
|
189
|
-
# @return [String] Binary data of subset glyf table
|
|
190
74
|
def subset_glyf(table)
|
|
191
|
-
|
|
192
|
-
build_glyf_and_loca(table)
|
|
193
|
-
@glyf_data
|
|
75
|
+
subset_table("glyf", table)
|
|
194
76
|
end
|
|
195
77
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# Builds new loca table based on subset glyph offsets. Must be
|
|
199
|
-
# called after subset_glyf.
|
|
200
|
-
#
|
|
201
|
-
# @param table [Loca] Parsed loca table
|
|
202
|
-
# @return [String] Binary data of subset loca table
|
|
203
|
-
def subset_loca(_table)
|
|
204
|
-
# Build glyf and loca together if not already done
|
|
205
|
-
glyf = font.table("glyf")
|
|
206
|
-
build_glyf_and_loca(glyf) unless @loca_offsets
|
|
207
|
-
|
|
208
|
-
head = font.table("head")
|
|
209
|
-
format = head.index_to_loc_format
|
|
210
|
-
|
|
211
|
-
data = String.new(encoding: Encoding::BINARY)
|
|
212
|
-
|
|
213
|
-
if format.zero?
|
|
214
|
-
# Short format: offsets / 2 as uint16
|
|
215
|
-
@loca_offsets.each do |offset|
|
|
216
|
-
data << [offset / 2].pack("n")
|
|
217
|
-
end
|
|
218
|
-
else
|
|
219
|
-
# Long format: offsets as uint32
|
|
220
|
-
@loca_offsets.each do |offset|
|
|
221
|
-
data << [offset].pack("N")
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
data
|
|
78
|
+
def subset_loca(table)
|
|
79
|
+
subset_table("loca", table)
|
|
226
80
|
end
|
|
227
81
|
|
|
228
|
-
# Subset cmap table (remap character to glyph mappings)
|
|
229
|
-
#
|
|
230
|
-
# Builds new cmap table with only mappings for glyphs in the subset.
|
|
231
|
-
# Updates glyph IDs to new values from the mapping.
|
|
232
|
-
#
|
|
233
|
-
# @param table [Cmap] Parsed cmap table
|
|
234
|
-
# @return [String] Binary data of subset cmap table
|
|
235
82
|
def subset_cmap(table)
|
|
236
|
-
|
|
237
|
-
old_mappings = table.unicode_mappings
|
|
238
|
-
new_mappings = {}
|
|
239
|
-
|
|
240
|
-
# Remap to new glyph IDs
|
|
241
|
-
old_mappings.each do |char_code, old_gid|
|
|
242
|
-
new_gid = mapping.new_id(old_gid)
|
|
243
|
-
new_mappings[char_code] = new_gid if new_gid
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Build cmap binary with new mappings
|
|
247
|
-
build_cmap_binary(new_mappings)
|
|
83
|
+
subset_table("cmap", table)
|
|
248
84
|
end
|
|
249
85
|
|
|
250
|
-
# Subset post table (optionally drop glyph names)
|
|
251
|
-
#
|
|
252
|
-
# If drop_names option is set, converts to post version 3.0
|
|
253
|
-
# (no glyph names). Otherwise passes through unchanged.
|
|
254
|
-
#
|
|
255
|
-
# @param table [Post] Parsed post table
|
|
256
|
-
# @return [String] Binary data of subset post table
|
|
257
86
|
def subset_post(table)
|
|
258
|
-
|
|
259
|
-
# Build post table version 3.0 (no glyph names)
|
|
260
|
-
build_post_v3(table)
|
|
261
|
-
else
|
|
262
|
-
# Keep as-is
|
|
263
|
-
font.table_data["post"]
|
|
264
|
-
end
|
|
87
|
+
subset_table("post", table)
|
|
265
88
|
end
|
|
266
89
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
# Name table doesn't require subsetting, pass through unchanged.
|
|
270
|
-
#
|
|
271
|
-
# @param table [Name] Parsed name table
|
|
272
|
-
# @return [String] Binary data of subset name table
|
|
273
|
-
def subset_name(_table)
|
|
274
|
-
font.table_data["name"]
|
|
90
|
+
def subset_name(table)
|
|
91
|
+
subset_table("name", table)
|
|
275
92
|
end
|
|
276
93
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
# head table will have checksum updated by FontWriter,
|
|
280
|
-
# no subsetting needed.
|
|
281
|
-
#
|
|
282
|
-
# @param table [Head] Parsed head table
|
|
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
|
|
295
|
-
def subset_head(_table)
|
|
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
|
|
94
|
+
def subset_head(table)
|
|
95
|
+
subset_table("head", table)
|
|
310
96
|
end
|
|
311
97
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
# If unicode_ranges option is set, updates Unicode range bits
|
|
315
|
-
# to reflect only the characters in the subset.
|
|
316
|
-
#
|
|
317
|
-
# @param table [Os2] Parsed OS/2 table
|
|
318
|
-
# @return [String] Binary data of subset OS/2 table
|
|
319
|
-
def subset_os2(_table)
|
|
320
|
-
if options.unicode_ranges
|
|
321
|
-
# TODO: Implement Unicode range pruning
|
|
322
|
-
# For now, pass through
|
|
323
|
-
end
|
|
324
|
-
font.table_data["OS/2"]
|
|
98
|
+
def subset_os2(table)
|
|
99
|
+
subset_table("OS/2", table)
|
|
325
100
|
end
|
|
326
101
|
|
|
327
102
|
private
|
|
328
103
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
# be optimized by finding the last unique advance width.
|
|
333
|
-
#
|
|
334
|
-
# @return [Integer] Number of unique advance widths
|
|
335
|
-
def calculate_number_of_h_metrics
|
|
336
|
-
mapping.size
|
|
337
|
-
end
|
|
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
|
-
|
|
365
|
-
# Build glyf and loca tables together
|
|
366
|
-
#
|
|
367
|
-
# This method extracts glyph data for all glyphs in the mapping,
|
|
368
|
-
# remaps component references in compound glyphs, and builds the
|
|
369
|
-
# loca offset array.
|
|
370
|
-
#
|
|
371
|
-
# @param glyf_table [Glyf] Parsed glyf table
|
|
372
|
-
def build_glyf_and_loca(glyf_table)
|
|
373
|
-
return if @glyf_data && @loca_offsets
|
|
374
|
-
|
|
375
|
-
loca = font.table("loca")
|
|
376
|
-
head = font.table("head")
|
|
377
|
-
|
|
378
|
-
# Ensure loca is parsed
|
|
379
|
-
unless loca.parsed?
|
|
380
|
-
maxp = font.table("maxp")
|
|
381
|
-
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
@glyf_data = String.new(encoding: Encoding::BINARY)
|
|
385
|
-
@loca_offsets = []
|
|
386
|
-
current_offset = 0
|
|
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
|
-
|
|
399
|
-
# Process glyphs in mapping order
|
|
400
|
-
mapping.old_ids.each do |old_id|
|
|
401
|
-
@loca_offsets << current_offset
|
|
402
|
-
|
|
403
|
-
# Get offset and size from original loca
|
|
404
|
-
offset = loca.offset_for(old_id)
|
|
405
|
-
size = loca.size_of(old_id)
|
|
406
|
-
|
|
407
|
-
# Empty glyph
|
|
408
|
-
if size.nil? || size.zero?
|
|
409
|
-
next
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
# Extract glyph data
|
|
413
|
-
glyph_data = glyf_table.raw_data[offset, size]
|
|
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
|
-
|
|
430
|
-
# Check if compound glyph and remap components
|
|
431
|
-
if compound_glyph?(glyph_data)
|
|
432
|
-
glyph_data = remap_compound_glyph(glyph_data)
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
# Add to new glyf data
|
|
436
|
-
@glyf_data << glyph_data
|
|
437
|
-
current_offset += glyph_data.bytesize
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# Add final offset
|
|
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]
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
# Check if glyph data represents a compound glyph
|
|
450
|
-
#
|
|
451
|
-
# @param data [String] Glyph binary data
|
|
452
|
-
# @return [Boolean] True if compound glyph
|
|
453
|
-
def compound_glyph?(data)
|
|
454
|
-
return false if data.length < 2
|
|
455
|
-
|
|
456
|
-
num_contours_raw = data[0, 2].unpack1("n")
|
|
457
|
-
num_contours = to_signed_16(num_contours_raw)
|
|
458
|
-
num_contours == -1
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
# Remap component glyph IDs in compound glyph
|
|
462
|
-
#
|
|
463
|
-
# @param data [String] Original compound glyph data
|
|
464
|
-
# @return [String] Remapped compound glyph data
|
|
465
|
-
def remap_compound_glyph(data)
|
|
466
|
-
# Create a mutable copy
|
|
467
|
-
new_data = data.dup
|
|
468
|
-
offset = 10 # Skip header (10 bytes)
|
|
469
|
-
|
|
470
|
-
loop do
|
|
471
|
-
break if offset >= new_data.length - 4
|
|
472
|
-
|
|
473
|
-
# Read flags and old glyph index
|
|
474
|
-
flags = new_data[offset, 2].unpack1("n")
|
|
475
|
-
old_glyph_index = new_data[offset + 2, 2].unpack1("n")
|
|
476
|
-
|
|
477
|
-
# Remap glyph index
|
|
478
|
-
new_glyph_index = mapping.new_id(old_glyph_index)
|
|
479
|
-
unless new_glyph_index
|
|
480
|
-
raise Fontisan::SubsettingError,
|
|
481
|
-
"Component glyph #{old_glyph_index} not in subset"
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
# Write new glyph index
|
|
485
|
-
new_data[offset + 2, 2] = [new_glyph_index].pack("n")
|
|
486
|
-
|
|
487
|
-
# Move to next component
|
|
488
|
-
offset += 4 # flags + glyph_index
|
|
489
|
-
|
|
490
|
-
# Skip arguments
|
|
491
|
-
offset += if (flags & 0x0001).zero?
|
|
492
|
-
2 # Two 8-bit arguments
|
|
493
|
-
else
|
|
494
|
-
4 # Two 16-bit arguments
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
# Skip transformation
|
|
498
|
-
if (flags & 0x0080) != 0
|
|
499
|
-
offset += 8 # 2x2 matrix
|
|
500
|
-
elsif (flags & 0x0040) != 0
|
|
501
|
-
offset += 4 # X and Y scale
|
|
502
|
-
elsif (flags & 0x0008) != 0
|
|
503
|
-
offset += 2 # Uniform scale
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
# Check if more components
|
|
507
|
-
break unless (flags & 0x0020) != 0
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
new_data
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
# Build cmap binary from mappings
|
|
514
|
-
#
|
|
515
|
-
# Creates a minimal cmap table with format 4 subtable for BMP
|
|
516
|
-
# and format 12 for supplementary planes if needed.
|
|
517
|
-
#
|
|
518
|
-
# @param mappings [Hash<Integer, Integer>] Char code => new glyph ID
|
|
519
|
-
# @return [String] Binary cmap data
|
|
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
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
# Build post table version 3.0 (no glyph names)
|
|
645
|
-
#
|
|
646
|
-
# @param table [Post] Original post table
|
|
647
|
-
# @return [String] Binary post v3.0 data
|
|
648
|
-
def build_post_v3(_table)
|
|
649
|
-
# Post v3.0 header (32 bytes) - same as v2.0 but version = 3.0
|
|
650
|
-
data = String.new(encoding: Encoding::BINARY)
|
|
651
|
-
|
|
652
|
-
# Version 3.0
|
|
653
|
-
data << [0x00030000].pack("N")
|
|
654
|
-
|
|
655
|
-
# Copy italic angle, underline position/thickness from original
|
|
656
|
-
original_data = font.table_data["post"]
|
|
657
|
-
data << if original_data.length >= 32
|
|
658
|
-
# Copy fields from offset 4 to 32
|
|
659
|
-
original_data[4, 28]
|
|
660
|
-
else
|
|
661
|
-
# Use defaults
|
|
662
|
-
[0, 0, 0, 0, 0, 0, 0].pack("N7")
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
data
|
|
666
|
-
end
|
|
667
|
-
|
|
668
|
-
# Convert unsigned 16-bit value to signed
|
|
669
|
-
#
|
|
670
|
-
# @param value [Integer] Unsigned 16-bit value
|
|
671
|
-
# @return [Integer] Signed 16-bit value
|
|
672
|
-
def to_signed_16(value)
|
|
673
|
-
value > 0x7FFF ? value - 0x10000 : value
|
|
104
|
+
def subset_context
|
|
105
|
+
SubsetContext.new(font: font, mapping: mapping, options: options,
|
|
106
|
+
state: state)
|
|
674
107
|
end
|
|
675
108
|
end
|
|
676
109
|
end
|