fontisan 0.2.3 → 0.2.4
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 +92 -40
- data/README.adoc +262 -3
- data/Rakefile +20 -7
- data/lib/fontisan/commands/base_command.rb +2 -19
- data/lib/fontisan/commands/convert_command.rb +16 -13
- data/lib/fontisan/commands/info_command.rb +88 -0
- data/lib/fontisan/config/conversion_matrix.yml +58 -20
- data/lib/fontisan/converters/outline_converter.rb +6 -3
- data/lib/fontisan/converters/svg_generator.rb +45 -0
- data/lib/fontisan/converters/woff2_encoder.rb +106 -13
- data/lib/fontisan/models/bitmap_glyph.rb +123 -0
- data/lib/fontisan/models/bitmap_strike.rb +94 -0
- data/lib/fontisan/models/color_glyph.rb +57 -0
- data/lib/fontisan/models/color_layer.rb +53 -0
- data/lib/fontisan/models/color_palette.rb +60 -0
- data/lib/fontisan/models/font_info.rb +26 -0
- data/lib/fontisan/models/svg_glyph.rb +89 -0
- data/lib/fontisan/open_type_font.rb +6 -0
- data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
- data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
- data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
- data/lib/fontisan/pipeline/output_writer.rb +2 -2
- data/lib/fontisan/tables/cbdt.rb +169 -0
- data/lib/fontisan/tables/cblc.rb +290 -0
- data/lib/fontisan/tables/cff.rb +6 -12
- data/lib/fontisan/tables/colr.rb +291 -0
- data/lib/fontisan/tables/cpal.rb +281 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
- data/lib/fontisan/tables/sbix.rb +379 -0
- data/lib/fontisan/tables/svg.rb +301 -0
- data/lib/fontisan/true_type_font.rb +6 -0
- data/lib/fontisan/validation/woff2_header_validator.rb +278 -0
- data/lib/fontisan/validation/woff2_table_validator.rb +270 -0
- data/lib/fontisan/validation/woff2_validator.rb +248 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +40 -11
- data/lib/fontisan/woff2/table_transformer.rb +506 -73
- data/lib/fontisan/woff2_font.rb +29 -9
- data/lib/fontisan/woff_font.rb +17 -4
- data/lib/fontisan.rb +12 -0
- metadata +17 -2
|
@@ -8,15 +8,9 @@ module Fontisan
|
|
|
8
8
|
# handles table transformations that improve compression in WOFF2.
|
|
9
9
|
# The WOFF2 spec defines transformations for glyf/loca and hmtx tables.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
# -
|
|
13
|
-
# -
|
|
14
|
-
# - Tables are copied as-is without transformation
|
|
15
|
-
# - This allows valid WOFF2 generation while leaving room for optimization
|
|
16
|
-
#
|
|
17
|
-
# Future milestones will implement:
|
|
18
|
-
# - glyf/loca transformation (combined stream, delta encoding)
|
|
19
|
-
# - hmtx transformation (compact representation)
|
|
11
|
+
# Transformations implemented:
|
|
12
|
+
# - glyf/loca: Combined stream format with specialized encoding
|
|
13
|
+
# - hmtx: Delta encoding with 255UInt16 compression
|
|
20
14
|
#
|
|
21
15
|
# Reference: https://www.w3.org/TR/WOFF2/#table_tranforms
|
|
22
16
|
#
|
|
@@ -36,12 +30,8 @@ module Fontisan
|
|
|
36
30
|
|
|
37
31
|
# Transform a table for WOFF2 encoding
|
|
38
32
|
#
|
|
39
|
-
# For Milestone 2.1, this returns the original table data
|
|
40
|
-
# without transformation. The architecture supports future
|
|
41
|
-
# implementation of actual transformations.
|
|
42
|
-
#
|
|
43
33
|
# @param tag [String] Table tag
|
|
44
|
-
# @return [String, nil] Transformed
|
|
34
|
+
# @return [String, nil] Transformed table data
|
|
45
35
|
def transform_table(tag)
|
|
46
36
|
case tag
|
|
47
37
|
when "glyf"
|
|
@@ -66,87 +56,92 @@ module Fontisan
|
|
|
66
56
|
|
|
67
57
|
# Determine transformation version for a table
|
|
68
58
|
#
|
|
69
|
-
# For Milestone 2.1, always returns TRANSFORM_NONE since
|
|
70
|
-
# we don't implement transformations yet.
|
|
71
|
-
#
|
|
72
59
|
# @param tag [String] Table tag
|
|
73
|
-
# @return [Integer] Transformation version
|
|
74
|
-
def transformation_version(
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
# @return [Integer] Transformation version
|
|
61
|
+
def transformation_version(tag)
|
|
62
|
+
case tag
|
|
63
|
+
when "glyf", "loca"
|
|
64
|
+
Directory::TRANSFORM_GLYF_LOCA
|
|
65
|
+
when "hmtx"
|
|
66
|
+
Directory::TRANSFORM_HMTX
|
|
67
|
+
else
|
|
68
|
+
Directory::TRANSFORM_NONE
|
|
69
|
+
end
|
|
77
70
|
end
|
|
78
71
|
|
|
79
72
|
private
|
|
80
73
|
|
|
81
74
|
# Transform glyf table
|
|
82
75
|
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
76
|
+
# Implements WOFF2 glyf transformation by splitting glyph data into 8 streams:
|
|
77
|
+
# 1. nContour stream - number of contours per glyph
|
|
78
|
+
# 2. nPoints stream - end points of contours (255UInt16)
|
|
79
|
+
# 3. Flag stream - point flags with run-length encoding
|
|
80
|
+
# 4. Glyph stream - x/y coordinates (delta-encoded)
|
|
81
|
+
# 5. Composite stream - composite glyph data
|
|
82
|
+
# 6. Bbox stream - bounding boxes
|
|
83
|
+
# 7. Instruction stream - hinting instructions
|
|
84
|
+
# 8. Composite bbox stream - not used in current implementation
|
|
85
85
|
#
|
|
86
|
-
#
|
|
87
|
-
# For now, returns original table data.
|
|
88
|
-
#
|
|
89
|
-
# @return [String, nil] Transformed glyf data
|
|
86
|
+
# @return [String] Transformed glyf data
|
|
90
87
|
def transform_glyf
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
88
|
+
glyf_data = get_table_data("glyf")
|
|
89
|
+
loca_data = get_table_data("loca")
|
|
90
|
+
|
|
91
|
+
return glyf_data unless glyf_data && loca_data
|
|
92
|
+
|
|
93
|
+
# Get number of glyphs from maxp table
|
|
94
|
+
maxp_table = font.table("maxp")
|
|
95
|
+
return glyf_data unless maxp_table
|
|
96
|
+
|
|
97
|
+
num_glyphs = maxp_table.num_glyphs
|
|
98
|
+
|
|
99
|
+
# Get head table to determine loca format
|
|
100
|
+
head_table = font.table("head")
|
|
101
|
+
return glyf_data unless head_table
|
|
102
|
+
|
|
103
|
+
index_format = head_table.index_to_loc_format
|
|
104
|
+
|
|
105
|
+
# Parse glyphs from glyf/loca tables
|
|
106
|
+
glyphs = parse_glyphs(glyf_data, loca_data, num_glyphs, index_format)
|
|
107
|
+
|
|
108
|
+
# Build transformed streams
|
|
109
|
+
build_transformed_glyf(glyphs, num_glyphs, index_format)
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
# Transform loca table
|
|
110
113
|
#
|
|
111
114
|
# In WOFF2, loca is combined with glyf during transformation.
|
|
112
|
-
#
|
|
115
|
+
# Return nil to indicate loca should be omitted from output.
|
|
113
116
|
#
|
|
114
|
-
#
|
|
115
|
-
# For now, returns original table data.
|
|
116
|
-
#
|
|
117
|
-
# @return [String, nil] Transformed loca data
|
|
117
|
+
# @return [nil]
|
|
118
118
|
def transform_loca
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
# 1. Combined into the transformed glyf stream
|
|
122
|
-
# 2. Reconstructed during decompression
|
|
123
|
-
# 3. Not present as separate table in WOFF2
|
|
124
|
-
|
|
125
|
-
get_table_data("loca")
|
|
119
|
+
# loca is combined into transformed glyf, so return nil
|
|
120
|
+
nil
|
|
126
121
|
end
|
|
127
122
|
|
|
128
123
|
# Transform hmtx table
|
|
129
124
|
#
|
|
130
|
-
#
|
|
131
|
-
# by exploiting redundancy (many glyphs have same advance width).
|
|
125
|
+
# Implements WOFF2 hmtx transformation using delta encoding and 255UInt16.
|
|
132
126
|
#
|
|
133
|
-
#
|
|
134
|
-
# For now, returns original table data.
|
|
135
|
-
#
|
|
136
|
-
# @return [String, nil] Transformed hmtx data
|
|
127
|
+
# @return [String] Transformed hmtx data
|
|
137
128
|
def transform_hmtx
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
129
|
+
hmtx_data = get_table_data("hmtx")
|
|
130
|
+
return hmtx_data unless hmtx_data
|
|
131
|
+
|
|
132
|
+
# Get required metadata
|
|
133
|
+
hhea_table = font.table("hhea")
|
|
134
|
+
maxp_table = font.table("maxp")
|
|
135
|
+
return hmtx_data unless hhea_table && maxp_table
|
|
136
|
+
|
|
137
|
+
num_h_metrics = hhea_table.number_of_h_metrics
|
|
138
|
+
num_glyphs = maxp_table.num_glyphs
|
|
148
139
|
|
|
149
|
-
|
|
140
|
+
# Parse hmtx table
|
|
141
|
+
advance_widths, lsbs = parse_hmtx_table(hmtx_data, num_h_metrics, num_glyphs)
|
|
142
|
+
|
|
143
|
+
# Build transformed hmtx table
|
|
144
|
+
build_transformed_hmtx(advance_widths, lsbs, num_h_metrics, num_glyphs)
|
|
150
145
|
end
|
|
151
146
|
|
|
152
147
|
# Get raw table data from font
|
|
@@ -156,7 +151,445 @@ module Fontisan
|
|
|
156
151
|
def get_table_data(tag)
|
|
157
152
|
return nil unless font.respond_to?(:table_data)
|
|
158
153
|
|
|
159
|
-
font.table_data
|
|
154
|
+
font.table_data[tag]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Parse glyphs from glyf and loca tables
|
|
158
|
+
#
|
|
159
|
+
# @param glyf_data [String] glyf table data
|
|
160
|
+
# @param loca_data [String] loca table data
|
|
161
|
+
# @param num_glyphs [Integer] Number of glyphs
|
|
162
|
+
# @param index_format [Integer] Loca format (0=short, 1=long)
|
|
163
|
+
# @return [Array<Hash>] Array of glyph hashes
|
|
164
|
+
def parse_glyphs(glyf_data, loca_data, num_glyphs, index_format)
|
|
165
|
+
# Parse loca offsets
|
|
166
|
+
offsets = parse_loca_offsets(loca_data, num_glyphs, index_format)
|
|
167
|
+
|
|
168
|
+
glyphs = []
|
|
169
|
+
num_glyphs.times do |i|
|
|
170
|
+
start_offset = offsets[i]
|
|
171
|
+
end_offset = offsets[i + 1]
|
|
172
|
+
|
|
173
|
+
if start_offset == end_offset
|
|
174
|
+
# Empty glyph
|
|
175
|
+
glyphs << { type: :empty, data: nil }
|
|
176
|
+
else
|
|
177
|
+
glyph_data = glyf_data[start_offset...end_offset]
|
|
178
|
+
glyphs << parse_glyph(glyph_data)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
glyphs
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Parse loca table to get glyph offsets
|
|
186
|
+
#
|
|
187
|
+
# @param loca_data [String] loca table data
|
|
188
|
+
# @param num_glyphs [Integer] Number of glyphs
|
|
189
|
+
# @param index_format [Integer] Format (0=short, 1=long)
|
|
190
|
+
# @return [Array<Integer>] Glyph offsets
|
|
191
|
+
def parse_loca_offsets(loca_data, num_glyphs, index_format)
|
|
192
|
+
offsets = []
|
|
193
|
+
io = StringIO.new(loca_data)
|
|
194
|
+
|
|
195
|
+
(num_glyphs + 1).times do
|
|
196
|
+
offsets << if index_format.zero?
|
|
197
|
+
# Short format (uint16, actual offset = value * 2)
|
|
198
|
+
(io.read(2)&.unpack1("n") || 0) * 2
|
|
199
|
+
else
|
|
200
|
+
# Long format (uint32)
|
|
201
|
+
io.read(4)&.unpack1("N") || 0
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
offsets
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Parse a single glyph
|
|
209
|
+
#
|
|
210
|
+
# @param data [String] Glyph data
|
|
211
|
+
# @return [Hash] Glyph information
|
|
212
|
+
def parse_glyph(data)
|
|
213
|
+
io = StringIO.new(data)
|
|
214
|
+
|
|
215
|
+
num_contours = io.read(2)&.unpack1("n") || 0
|
|
216
|
+
num_contours = num_contours > 0x7FFF ? num_contours - 0x10000 : num_contours
|
|
217
|
+
|
|
218
|
+
if num_contours.zero?
|
|
219
|
+
{ type: :empty, data: nil }
|
|
220
|
+
elsif num_contours.positive?
|
|
221
|
+
parse_simple_glyph(io, num_contours, data)
|
|
222
|
+
else
|
|
223
|
+
parse_composite_glyph(io, data)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Parse simple glyph
|
|
228
|
+
#
|
|
229
|
+
# @param io [StringIO] Data stream
|
|
230
|
+
# @param num_contours [Integer] Number of contours
|
|
231
|
+
# @param data [String] Full glyph data
|
|
232
|
+
# @return [Hash] Glyph information
|
|
233
|
+
def parse_simple_glyph(io, num_contours, _data)
|
|
234
|
+
# Read bounding box
|
|
235
|
+
x_min = read_int16(io)
|
|
236
|
+
y_min = read_int16(io)
|
|
237
|
+
x_max = read_int16(io)
|
|
238
|
+
y_max = read_int16(io)
|
|
239
|
+
|
|
240
|
+
# Read end points of contours
|
|
241
|
+
end_pts = []
|
|
242
|
+
num_contours.times { end_pts << io.read(2)&.unpack1("n") }
|
|
243
|
+
|
|
244
|
+
total_points = end_pts.last + 1
|
|
245
|
+
|
|
246
|
+
# Read instruction length and instructions
|
|
247
|
+
inst_length = io.read(2)&.unpack1("n") || 0
|
|
248
|
+
instructions = inst_length.positive? ? io.read(inst_length) : ""
|
|
249
|
+
|
|
250
|
+
# Read flags
|
|
251
|
+
flags = []
|
|
252
|
+
while flags.size < total_points
|
|
253
|
+
flag = io.read(1)&.unpack1("C")
|
|
254
|
+
flags << flag
|
|
255
|
+
|
|
256
|
+
if (flag & 0x08) != 0 # REPEAT_FLAG
|
|
257
|
+
repeat_count = io.read(1)&.unpack1("C")
|
|
258
|
+
repeat_count.times { flags << flag }
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Read x-coordinates
|
|
263
|
+
x_coords = read_coordinates(io, flags, 0x02, 0x10)
|
|
264
|
+
|
|
265
|
+
# Read y-coordinates
|
|
266
|
+
y_coords = read_coordinates(io, flags, 0x04, 0x20)
|
|
267
|
+
|
|
268
|
+
{
|
|
269
|
+
type: :simple,
|
|
270
|
+
num_contours: num_contours,
|
|
271
|
+
bbox: [x_min, y_min, x_max, y_max],
|
|
272
|
+
end_pts: end_pts,
|
|
273
|
+
instructions: instructions,
|
|
274
|
+
flags: flags,
|
|
275
|
+
x_coords: x_coords,
|
|
276
|
+
y_coords: y_coords,
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Parse composite glyph
|
|
281
|
+
#
|
|
282
|
+
# @param io [StringIO] Data stream
|
|
283
|
+
# @param data [String] Full glyph data
|
|
284
|
+
# @return [Hash] Glyph information
|
|
285
|
+
def parse_composite_glyph(io, data)
|
|
286
|
+
# Read bounding box at start
|
|
287
|
+
x_min = read_int16(io)
|
|
288
|
+
y_min = read_int16(io)
|
|
289
|
+
x_max = read_int16(io)
|
|
290
|
+
y_max = read_int16(io)
|
|
291
|
+
|
|
292
|
+
# Read composite components
|
|
293
|
+
components = []
|
|
294
|
+
instructions = ""
|
|
295
|
+
|
|
296
|
+
loop do
|
|
297
|
+
start_pos = io.pos
|
|
298
|
+
flags = io.read(2)&.unpack1("n")
|
|
299
|
+
glyph_index = io.read(2)&.unpack1("n")
|
|
300
|
+
|
|
301
|
+
component = { flags: flags, glyph_index: glyph_index }
|
|
302
|
+
|
|
303
|
+
# Read arguments based on flags
|
|
304
|
+
if (flags & 0x0001).zero?
|
|
305
|
+
arg1 = io.read(1)&.unpack1("c")
|
|
306
|
+
arg2 = io.read(1)&.unpack1("c")
|
|
307
|
+
else # ARG_1_AND_2_ARE_WORDS
|
|
308
|
+
arg1 = read_int16(io)
|
|
309
|
+
arg2 = read_int16(io)
|
|
310
|
+
end
|
|
311
|
+
component[:arg1] = arg1
|
|
312
|
+
component[:arg2] = arg2
|
|
313
|
+
|
|
314
|
+
# Read transformation based on flags
|
|
315
|
+
if (flags & 0x0008) != 0 # WE_HAVE_A_SCALE
|
|
316
|
+
component[:scale] = io.read(2)&.unpack1("n")
|
|
317
|
+
elsif (flags & 0x0040) != 0 # WE_HAVE_AN_X_AND_Y_SCALE
|
|
318
|
+
component[:x_scale] = io.read(2)&.unpack1("n")
|
|
319
|
+
component[:y_scale] = io.read(2)&.unpack1("n")
|
|
320
|
+
elsif (flags & 0x0080) != 0 # WE_HAVE_A_TWO_BY_TWO
|
|
321
|
+
component[:x_scale] = io.read(2)&.unpack1("n")
|
|
322
|
+
component[:scale01] = io.read(2)&.unpack1("n")
|
|
323
|
+
component[:scale10] = io.read(2)&.unpack1("n")
|
|
324
|
+
component[:y_scale] = io.read(2)&.unpack1("n")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Store raw component data
|
|
328
|
+
end_pos = io.pos
|
|
329
|
+
component[:raw_data] = data[start_pos...end_pos]
|
|
330
|
+
|
|
331
|
+
components << component
|
|
332
|
+
|
|
333
|
+
(flags & 0x0100) != 0
|
|
334
|
+
|
|
335
|
+
break if (flags & 0x0020).zero? # MORE_COMPONENTS
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Read instructions if present
|
|
339
|
+
if components.last && (components.last[:flags] & 0x0100) != 0
|
|
340
|
+
inst_length = io.read(2)&.unpack1("n") || 0
|
|
341
|
+
instructions = inst_length.positive? ? io.read(inst_length) : ""
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
{
|
|
345
|
+
type: :composite,
|
|
346
|
+
bbox: [x_min, y_min, x_max, y_max],
|
|
347
|
+
components: components,
|
|
348
|
+
instructions: instructions,
|
|
349
|
+
}
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Parse hmtx table
|
|
353
|
+
#
|
|
354
|
+
# @param hmtx_data [String] hmtx table data
|
|
355
|
+
# @param num_h_metrics [Integer] Number of hMetric entries
|
|
356
|
+
# @param num_glyphs [Integer] Total number of glyphs
|
|
357
|
+
# @return [Array<Array<Integer>, Array<Integer>>] [advance_widths, lsbs]
|
|
358
|
+
def parse_hmtx_table(hmtx_data, num_h_metrics, num_glyphs)
|
|
359
|
+
io = StringIO.new(hmtx_data)
|
|
360
|
+
advance_widths = []
|
|
361
|
+
lsbs = []
|
|
362
|
+
|
|
363
|
+
# Read longHorMetric array (advance width + LSB pairs)
|
|
364
|
+
num_h_metrics.times do
|
|
365
|
+
advance_width = io.read(2)&.unpack1("n") || 0
|
|
366
|
+
lsb = read_int16(io)
|
|
367
|
+
|
|
368
|
+
advance_widths << advance_width
|
|
369
|
+
lsbs << lsb
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Read remaining LSB values (these glyphs share last advance width)
|
|
373
|
+
(num_glyphs - num_h_metrics).times do
|
|
374
|
+
lsb = read_int16(io)
|
|
375
|
+
lsbs << lsb
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
[advance_widths, lsbs]
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Build transformed hmtx table
|
|
382
|
+
#
|
|
383
|
+
# Uses proportional encoding with deltas for advance widths
|
|
384
|
+
# and explicit LSB values.
|
|
385
|
+
#
|
|
386
|
+
# @param advance_widths [Array<Integer>] Advance widths
|
|
387
|
+
# @param lsbs [Array<Integer>] Left side bearings
|
|
388
|
+
# @param num_h_metrics [Integer] Number of hMetric entries
|
|
389
|
+
# @param num_glyphs [Integer] Total number of glyphs
|
|
390
|
+
# @return [String] Transformed hmtx data
|
|
391
|
+
def build_transformed_hmtx(advance_widths, lsbs, num_h_metrics, num_glyphs)
|
|
392
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
393
|
+
|
|
394
|
+
# Flags: Use proportional encoding (not explicit) and explicit LSBs
|
|
395
|
+
# 0x00 = proportional advance widths
|
|
396
|
+
# 0x02 = explicit LSB values
|
|
397
|
+
flags = 0x02
|
|
398
|
+
data << [flags].pack("C")
|
|
399
|
+
|
|
400
|
+
# Write advance widths using proportional encoding
|
|
401
|
+
# First advance width is explicit
|
|
402
|
+
data << encode_255_uint16(advance_widths[0])
|
|
403
|
+
|
|
404
|
+
# Remaining advance widths as deltas
|
|
405
|
+
(1...num_h_metrics).each do |i|
|
|
406
|
+
delta = advance_widths[i] - advance_widths[i - 1]
|
|
407
|
+
data << [delta].pack("n") # int16 delta
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Write all LSB values explicitly
|
|
411
|
+
num_glyphs.times do |i|
|
|
412
|
+
lsb = lsbs[i] || 0
|
|
413
|
+
data << [lsb].pack("n") # int16 LSB
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
data
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Read coordinates from glyph data
|
|
420
|
+
#
|
|
421
|
+
# @param io [StringIO] Data stream
|
|
422
|
+
# @param flags [Array<Integer>] Point flags
|
|
423
|
+
# @param short_flag [Integer] Flag for short vector
|
|
424
|
+
# @param same_or_pos_flag [Integer] Flag for same/positive
|
|
425
|
+
# @return [Array<Integer>] Coordinates
|
|
426
|
+
def read_coordinates(io, flags, short_flag, same_or_pos_flag)
|
|
427
|
+
coords = []
|
|
428
|
+
value = 0
|
|
429
|
+
|
|
430
|
+
flags.each do |flag|
|
|
431
|
+
if (flag & short_flag) != 0
|
|
432
|
+
delta = io.read(1)&.unpack1("C")
|
|
433
|
+
delta = -delta if (flag & same_or_pos_flag).zero?
|
|
434
|
+
elsif (flag & same_or_pos_flag) != 0
|
|
435
|
+
delta = 0
|
|
436
|
+
else
|
|
437
|
+
delta = read_int16(io)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
value += delta
|
|
441
|
+
coords << value
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
coords
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Build transformed glyf data from parsed glyphs
|
|
448
|
+
#
|
|
449
|
+
# @param glyphs [Array<Hash>] Parsed glyphs
|
|
450
|
+
# @param num_glyphs [Integer] Number of glyphs
|
|
451
|
+
# @param index_format [Integer] Loca format
|
|
452
|
+
# @return [String] Transformed glyf data
|
|
453
|
+
def build_transformed_glyf(glyphs, num_glyphs, index_format)
|
|
454
|
+
# Build 8 streams
|
|
455
|
+
n_contour_stream = String.new(encoding: Encoding::BINARY)
|
|
456
|
+
n_points_stream = String.new(encoding: Encoding::BINARY)
|
|
457
|
+
flag_stream = String.new(encoding: Encoding::BINARY)
|
|
458
|
+
glyph_stream = String.new(encoding: Encoding::BINARY)
|
|
459
|
+
composite_stream = String.new(encoding: Encoding::BINARY)
|
|
460
|
+
bbox_stream = String.new(encoding: Encoding::BINARY)
|
|
461
|
+
instruction_stream = String.new(encoding: Encoding::BINARY)
|
|
462
|
+
|
|
463
|
+
glyphs.each do |glyph|
|
|
464
|
+
case glyph[:type]
|
|
465
|
+
when :empty
|
|
466
|
+
n_contour_stream << [0].pack("n")
|
|
467
|
+
when :simple
|
|
468
|
+
n_contour_stream << [glyph[:num_contours]].pack("n")
|
|
469
|
+
|
|
470
|
+
# Write end points as deltas (255UInt16)
|
|
471
|
+
prev_pt = -1
|
|
472
|
+
glyph[:end_pts].each do |pt|
|
|
473
|
+
delta = pt - prev_pt - 1
|
|
474
|
+
n_points_stream << encode_255_uint16(delta)
|
|
475
|
+
prev_pt = pt
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Write flags with run-length encoding
|
|
479
|
+
write_flags_rle(flag_stream, glyph[:flags])
|
|
480
|
+
|
|
481
|
+
# Write coordinates as deltas
|
|
482
|
+
write_coordinates(glyph_stream, glyph[:x_coords])
|
|
483
|
+
write_coordinates(glyph_stream, glyph[:y_coords])
|
|
484
|
+
|
|
485
|
+
# Write bounding box
|
|
486
|
+
glyph[:bbox].each { |v| bbox_stream << [v].pack("n") }
|
|
487
|
+
|
|
488
|
+
# Write instructions
|
|
489
|
+
instruction_stream << encode_255_uint16(glyph[:instructions].bytesize)
|
|
490
|
+
instruction_stream << glyph[:instructions] if glyph[:instructions].bytesize.positive?
|
|
491
|
+
|
|
492
|
+
when :composite
|
|
493
|
+
n_contour_stream << [-1].pack("n")
|
|
494
|
+
|
|
495
|
+
# Write all component data
|
|
496
|
+
glyph[:components].each { |c| composite_stream << c[:raw_data] }
|
|
497
|
+
|
|
498
|
+
# Write bounding box
|
|
499
|
+
glyph[:bbox].each { |v| bbox_stream << [v].pack("n") }
|
|
500
|
+
|
|
501
|
+
# Write instructions if present
|
|
502
|
+
if glyph[:instructions].bytesize.positive?
|
|
503
|
+
instruction_stream << [glyph[:instructions].bytesize].pack("n")
|
|
504
|
+
instruction_stream << glyph[:instructions]
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Build header and combine streams
|
|
510
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
511
|
+
data << [0].pack("N") # version
|
|
512
|
+
data << [num_glyphs].pack("n")
|
|
513
|
+
data << [index_format].pack("n")
|
|
514
|
+
|
|
515
|
+
# Write stream sizes and data
|
|
516
|
+
[n_contour_stream, n_points_stream, flag_stream, glyph_stream,
|
|
517
|
+
composite_stream, bbox_stream, instruction_stream, ""].each do |stream|
|
|
518
|
+
data << [stream.bytesize].pack("N")
|
|
519
|
+
data << stream
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
data
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Write flags with run-length encoding
|
|
526
|
+
#
|
|
527
|
+
# @param stream [String] Output stream
|
|
528
|
+
# @param flags [Array<Integer>] Flags to encode
|
|
529
|
+
def write_flags_rle(stream, flags)
|
|
530
|
+
i = 0
|
|
531
|
+
while i < flags.size
|
|
532
|
+
flag = flags[i]
|
|
533
|
+
count = 1
|
|
534
|
+
|
|
535
|
+
# Count repeats
|
|
536
|
+
while i + count < flags.size && flags[i + count] == flag && count < 255
|
|
537
|
+
count += 1
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
if count > 1
|
|
541
|
+
stream << [flag | 0x08].pack("C") # Set REPEAT_FLAG
|
|
542
|
+
stream << [count - 1].pack("C") # Repeat count (not including first)
|
|
543
|
+
i += count
|
|
544
|
+
else
|
|
545
|
+
stream << [flag].pack("C")
|
|
546
|
+
i += 1
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Write coordinates as deltas
|
|
552
|
+
#
|
|
553
|
+
# @param stream [String] Output stream
|
|
554
|
+
# @param coords [Array<Integer>] Coordinates
|
|
555
|
+
def write_coordinates(stream, coords)
|
|
556
|
+
prev = 0
|
|
557
|
+
coords.each do |coord|
|
|
558
|
+
delta = coord - prev
|
|
559
|
+
|
|
560
|
+
stream << if delta.abs <= 255
|
|
561
|
+
[delta.abs].pack("C")
|
|
562
|
+
else
|
|
563
|
+
[delta].pack("n")
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
prev = coord
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Encode 255UInt16 value
|
|
571
|
+
#
|
|
572
|
+
# @param value [Integer] Value to encode
|
|
573
|
+
# @return [String] Encoded bytes
|
|
574
|
+
def encode_255_uint16(value)
|
|
575
|
+
if value < 253
|
|
576
|
+
[value].pack("C")
|
|
577
|
+
elsif value < 506
|
|
578
|
+
[253, value - 253].pack("CC")
|
|
579
|
+
elsif value < 65536
|
|
580
|
+
[254].pack("C") + [value].pack("n")
|
|
581
|
+
else
|
|
582
|
+
[255].pack("C") + [value - 506].pack("n")
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Read signed 16-bit integer
|
|
587
|
+
#
|
|
588
|
+
# @param io [StringIO] Input stream
|
|
589
|
+
# @return [Integer] Signed value
|
|
590
|
+
def read_int16(io)
|
|
591
|
+
value = io.read(2)&.unpack1("n") || 0
|
|
592
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
160
593
|
end
|
|
161
594
|
end
|
|
162
595
|
end
|
data/lib/fontisan/woff2_font.rb
CHANGED
|
@@ -156,7 +156,22 @@ module Fontisan
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
# Get decompressed table data
|
|
159
|
-
|
|
159
|
+
#
|
|
160
|
+
# @param tag [String, nil] The table tag (optional)
|
|
161
|
+
# @return [String, Hash, nil] Table data if tag provided, or hash of all tables if no tag
|
|
162
|
+
def table_data(tag = nil)
|
|
163
|
+
# If no tag provided, return all tables
|
|
164
|
+
if tag.nil?
|
|
165
|
+
# First try underlying font's table data if available
|
|
166
|
+
if @underlying_font.respond_to?(:table_data)
|
|
167
|
+
return @underlying_font.table_data
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Fallback to decompressed_tables
|
|
171
|
+
return @decompressed_tables.dup
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Tag provided - return specific table
|
|
160
175
|
# First try underlying font's table data if available
|
|
161
176
|
if @underlying_font.respond_to?(:table_data)
|
|
162
177
|
underlying_data = @underlying_font.table_data[tag]
|
|
@@ -410,15 +425,20 @@ module Fontisan
|
|
|
410
425
|
|
|
411
426
|
# Determine if transformLength should be read
|
|
412
427
|
# According to WOFF2 spec section 4.2:
|
|
413
|
-
# -
|
|
414
|
-
# -
|
|
415
|
-
# -
|
|
428
|
+
# - transformLength is ONLY present when table is actually transformed
|
|
429
|
+
# - For glyf/loca: transformation is indicated by transform_version = 0
|
|
430
|
+
# - For hmtx: transformation is indicated by transform_version = 1
|
|
431
|
+
# - For all other tables: no transformation, no transformLength
|
|
416
432
|
transform_version = (flags >> 6) & 0x03
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
433
|
+
|
|
434
|
+
# transformLength is present when table is actually transformed
|
|
435
|
+
# glyf/loca use version 0 for transformation, hmtx uses version 1
|
|
436
|
+
has_transform_length = if ["glyf", "loca"].include?(entry.tag)
|
|
437
|
+
# For glyf/loca, version 0 means transformed
|
|
438
|
+
transform_version.zero?
|
|
439
|
+
elsif entry.tag == "hmtx"
|
|
440
|
+
# For hmtx, version 1 means transformed
|
|
441
|
+
transform_version == 1
|
|
422
442
|
else
|
|
423
443
|
false
|
|
424
444
|
end
|