fontisan 0.4.13 → 0.4.14
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 +1 -1
- data/docs/WOFF_WOFF2_FORMATS.adoc +73 -0
- data/lib/fontisan/converters/woff_writer.rb +94 -59
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_encoder.rb +288 -0
- data/lib/fontisan/woff2/encoder_rules.rb +8 -1
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +400 -0
- data/lib/fontisan/woff2/glyf_loca_transform.rb +12 -5
- data/lib/fontisan/woff2.rb +2 -1
- data/lib/fontisan/woff2_font.rb +23 -40
- metadata +4 -3
- data/lib/fontisan/woff2/glyf_transformer.rb +0 -716
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# Reconstructs glyf and loca tables from the WOFF2 transformed glyf
|
|
8
|
+
# stream per spec section 5.1. This is the decoder counterpart of
|
|
9
|
+
# {Woff2::GlyfLocaTransform}.
|
|
10
|
+
#
|
|
11
|
+
# The transformed glyf table is split into 7 streams preceded by a
|
|
12
|
+
# 36-byte header (uint16 version + uint16 optionFlags + uint16 numGlyphs
|
|
13
|
+
# + uint16 indexFormat + 7 × uint32 stream sizes). An optional
|
|
14
|
+
# overlapSimpleBitmap follows when optionFlags bit 0 is set.
|
|
15
|
+
#
|
|
16
|
+
# Reference: W3C WOFF2 spec, section 5.1.
|
|
17
|
+
class GlyfLocaReconstruct
|
|
18
|
+
# TrueType simple-glyph flag bits per OpenType spec.
|
|
19
|
+
FLAG_ON_CURVE = 0x01
|
|
20
|
+
FLAG_X_SHORT = 0x02
|
|
21
|
+
FLAG_Y_SHORT = 0x04
|
|
22
|
+
FLAG_REPEAT = 0x08
|
|
23
|
+
FLAG_X_SAME_OR_POS = 0x10
|
|
24
|
+
FLAG_Y_SAME_OR_POS = 0x20
|
|
25
|
+
FLAG_OVERLAP_SIMPLE = 0x40
|
|
26
|
+
|
|
27
|
+
# TrueType composite-glyph flag bits.
|
|
28
|
+
ARG_1_AND_2_ARE_WORDS = 0x0001
|
|
29
|
+
WE_HAVE_A_SCALE = 0x0008
|
|
30
|
+
MORE_COMPONENTS = 0x0020
|
|
31
|
+
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
|
|
32
|
+
WE_HAVE_A_TWO_BY_TWO = 0x0080
|
|
33
|
+
WE_HAVE_INSTRUCTIONS = 0x0100
|
|
34
|
+
|
|
35
|
+
# 8-byte header + 7 × 4-byte stream sizes.
|
|
36
|
+
HEADER_SIZE = 36
|
|
37
|
+
|
|
38
|
+
attr_reader :num_glyphs, :index_format
|
|
39
|
+
|
|
40
|
+
# @param transformed_glyf [String] bytes of the transformed glyf table
|
|
41
|
+
# @param num_glyphs [Integer] from maxp
|
|
42
|
+
# @param index_format [Integer] 0 (short) or 1 (long), from head
|
|
43
|
+
def initialize(transformed_glyf:, num_glyphs:, index_format:)
|
|
44
|
+
@data = transformed_glyf
|
|
45
|
+
@num_glyphs = num_glyphs
|
|
46
|
+
@index_format = index_format
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Reconstruct glyf and loca tables.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash{Symbol => String}] `{ glyf:, loca: }`
|
|
52
|
+
def reconstruct
|
|
53
|
+
header = parse_header
|
|
54
|
+
streams = read_streams(header)
|
|
55
|
+
|
|
56
|
+
glyf = String.new(encoding: Encoding::BINARY)
|
|
57
|
+
offsets = [0]
|
|
58
|
+
|
|
59
|
+
num_glyphs.times do |glyph_id|
|
|
60
|
+
glyph = decode_glyph(glyph_id, streams)
|
|
61
|
+
glyf << glyph
|
|
62
|
+
offsets << glyf.bytesize
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
{ glyf:, loca: build_loca(offsets) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Parsed header. Holds the 4 uint16 fields plus the 7 uint32 stream
|
|
71
|
+
# sizes. Stored as a Hash so the keys document themselves.
|
|
72
|
+
def parse_header
|
|
73
|
+
version, option_flags, ng, idx_fmt = @data[0, 8].unpack("S>S>S>S>")
|
|
74
|
+
sizes = @data[8, 28].unpack("L>7")
|
|
75
|
+
|
|
76
|
+
if ng != @num_glyphs
|
|
77
|
+
raise InvalidFontError,
|
|
78
|
+
"WOFF2 glyf numGlyphs mismatch: header says #{ng}, expected #{@num_glyphs}"
|
|
79
|
+
end
|
|
80
|
+
if idx_fmt != @index_format
|
|
81
|
+
raise InvalidFontError,
|
|
82
|
+
"WOFF2 glyf indexFormat mismatch: header says #{idx_fmt}, expected #{@index_format}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
version:,
|
|
87
|
+
option_flags:,
|
|
88
|
+
has_overlap_bitmap: (option_flags & 0x01).nonzero?,
|
|
89
|
+
stream_sizes: sizes,
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Slice the 7 streams out of the data block and set up per-stream
|
|
94
|
+
# StringIO cursors. The bboxStream is split into bboxBitmap (fixed
|
|
95
|
+
# size per numGlyphs) and bboxStream (variable, conditionally read
|
|
96
|
+
# per glyph). The optional overlapSimpleBitmap is read at the end.
|
|
97
|
+
def read_streams(header)
|
|
98
|
+
pos = HEADER_SIZE
|
|
99
|
+
bitmap_size = ((@num_glyphs + 31) >> 5) << 2
|
|
100
|
+
overlap_size = header[:has_overlap_bitmap] ? ((@num_glyphs + 7) >> 3) : 0
|
|
101
|
+
|
|
102
|
+
names = %i[n_contour n_points flags glyph composite bbox instructions]
|
|
103
|
+
streams = {}
|
|
104
|
+
names.each_with_index do |name, i|
|
|
105
|
+
size = header[:stream_sizes][i]
|
|
106
|
+
streams[name] = StringIO.new(@data[pos, size])
|
|
107
|
+
pos += size
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# bboxStream begins with bboxBitmap (4 × floor((numGlyphs+31)/32)).
|
|
111
|
+
bbox_blob = streams[:bbox].string
|
|
112
|
+
streams[:bbox_bitmap] = bbox_blob[0, bitmap_size] || ""
|
|
113
|
+
streams[:bbox_entries] = StringIO.new(bbox_blob[bitmap_size..] || "")
|
|
114
|
+
|
|
115
|
+
# overlapSimpleBitmap follows all streams when optionFlags bit 0 set.
|
|
116
|
+
streams[:overlap_bitmap] = header[:has_overlap_bitmap] ? @data[pos, overlap_size] : ""
|
|
117
|
+
|
|
118
|
+
# nContour is read positionally (int16 per glyph).
|
|
119
|
+
nc_data = streams[:n_contour].string
|
|
120
|
+
streams[:n_contour_values] = nc_data.unpack("s>*")
|
|
121
|
+
|
|
122
|
+
streams
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def decode_glyph(glyph_id, streams)
|
|
126
|
+
num_contours = streams[:n_contour_values][glyph_id]
|
|
127
|
+
|
|
128
|
+
case num_contours
|
|
129
|
+
when 0
|
|
130
|
+
# Empty glyph: just an entry in loca (loca[n] == loca[n-1]).
|
|
131
|
+
""
|
|
132
|
+
when -1
|
|
133
|
+
decode_composite(glyph_id, streams)
|
|
134
|
+
else
|
|
135
|
+
decode_simple(glyph_id, num_contours, streams)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Decode a simple glyph per spec section 5.1 "Decoding of Simple Glyphs".
|
|
140
|
+
def decode_simple(glyph_id, num_contours, streams)
|
|
141
|
+
# Step 1: read num_contours 255UInt16 values from nPointsStream to
|
|
142
|
+
# build endPtsOfContours[].
|
|
143
|
+
end_pts = []
|
|
144
|
+
cumulative = -1
|
|
145
|
+
num_contours.times do
|
|
146
|
+
pts_of_contour = read_255_uint16(streams[:n_points])
|
|
147
|
+
cumulative += pts_of_contour
|
|
148
|
+
end_pts << cumulative
|
|
149
|
+
end
|
|
150
|
+
total_points = end_pts.last + 1
|
|
151
|
+
|
|
152
|
+
# Step 2: read total_points flag bytes from flagStream (triplet flags).
|
|
153
|
+
triplet_flags = streams[:flags].read(total_points).bytes
|
|
154
|
+
|
|
155
|
+
# Step 3: read coordinate triplets from glyphStream (1-4 bytes per
|
|
156
|
+
# point depending on flag). Decode to (dx, dy) deltas.
|
|
157
|
+
xs = []
|
|
158
|
+
ys = []
|
|
159
|
+
on_curve_flags = []
|
|
160
|
+
x = 0
|
|
161
|
+
y = 0
|
|
162
|
+
triplet_flags.each do |flag|
|
|
163
|
+
dx, dy, oc = TripletCodec.decode(flag, read_triplet_payload(flag, streams[:glyph]))
|
|
164
|
+
x += dx
|
|
165
|
+
y += dy
|
|
166
|
+
xs << x
|
|
167
|
+
ys << y
|
|
168
|
+
on_curve_flags << (oc ? FLAG_ON_CURVE : 0)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Step 4-5: read instructionLength from glyphStream, bytes from instructionStream.
|
|
172
|
+
inst_len = read_255_uint16(streams[:glyph])
|
|
173
|
+
instructions = streams[:instructions].read(inst_len)
|
|
174
|
+
|
|
175
|
+
# Bbox: emit explicit bbox if bboxBitmap bit is set for this glyph,
|
|
176
|
+
# otherwise recalculate from coordinates.
|
|
177
|
+
bbox = if bbox_bit_set?(streams[:bbox_bitmap], glyph_id)
|
|
178
|
+
read_bbox(streams[:bbox_entries])
|
|
179
|
+
else
|
|
180
|
+
calc_bounds(xs, ys) || [0, 0, 0, 0]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Re-encode in standard TrueType format.
|
|
184
|
+
build_simple_glyph_bytes(num_contours:, bbox:, end_pts:,
|
|
185
|
+
on_curve_flags:, xs:, ys:, instructions:,
|
|
186
|
+
overlap: overlap_bit_set?(streams[:overlap_bitmap], glyph_id))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def decode_composite(_glyph_id, streams)
|
|
190
|
+
# Composite components are stored verbatim in compositeStream. Walk
|
|
191
|
+
# the component records to find their total byte length, then copy.
|
|
192
|
+
# Per spec section 5.1 "Decoding of Composite Glyphs": each component
|
|
193
|
+
# has a uint16 flags word, then 4-14 argument bytes.
|
|
194
|
+
composite_io = streams[:composite]
|
|
195
|
+
component_start = composite_io.pos
|
|
196
|
+
have_instructions = false
|
|
197
|
+
loop do
|
|
198
|
+
break if composite_io.eof?
|
|
199
|
+
|
|
200
|
+
flags = read_uint16_io(composite_io)
|
|
201
|
+
composite_io.read(2) # glyphIndex
|
|
202
|
+
composite_io.read((flags & ARG_1_AND_2_ARE_WORDS).nonzero? ? 4 : 2)
|
|
203
|
+
composite_io.read(composite_transform_size(flags))
|
|
204
|
+
have_instructions = (flags & WE_HAVE_INSTRUCTIONS).nonzero?
|
|
205
|
+
break if (flags & MORE_COMPONENTS).zero?
|
|
206
|
+
end
|
|
207
|
+
component_end = composite_io.pos
|
|
208
|
+
component_bytes = composite_io.string[component_start...component_end]
|
|
209
|
+
|
|
210
|
+
instructions = String.new(encoding: Encoding::BINARY)
|
|
211
|
+
if have_instructions
|
|
212
|
+
inst_len = read_255_uint16(streams[:glyph])
|
|
213
|
+
instructions = streams[:instructions].read(inst_len)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Composite glyphs always have explicit bbox in bboxStream.
|
|
217
|
+
bbox = read_bbox(streams[:bbox_entries])
|
|
218
|
+
|
|
219
|
+
# num_contours = -1 (int16), bbox 4 × int16, components, optional instructions
|
|
220
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
221
|
+
out << [-1].pack("s>")
|
|
222
|
+
out << bbox.pack("s>*")
|
|
223
|
+
out << component_bytes
|
|
224
|
+
# If the last component had WE_HAVE_INSTRUCTIONS, the standard
|
|
225
|
+
# TrueType composite format expects a uint16 instructionLength
|
|
226
|
+
# followed by the instruction bytes.
|
|
227
|
+
if have_instructions
|
|
228
|
+
out << [instructions.bytesize].pack("n")
|
|
229
|
+
out << instructions
|
|
230
|
+
end
|
|
231
|
+
out
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def composite_transform_size(flags)
|
|
235
|
+
if (flags & WE_HAVE_A_SCALE).nonzero? then 2
|
|
236
|
+
elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE).nonzero? then 4
|
|
237
|
+
elsif (flags & WE_HAVE_A_TWO_BY_TWO).nonzero? then 8
|
|
238
|
+
else
|
|
239
|
+
0
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Read 1-4 payload bytes for a triplet based on the flag's index bits.
|
|
244
|
+
def read_triplet_payload(flag, io)
|
|
245
|
+
idx = flag & 0x7F
|
|
246
|
+
n_bytes = if idx < 84 then 1
|
|
247
|
+
elsif idx < 120 then 2
|
|
248
|
+
elsif idx < 124 then 3
|
|
249
|
+
else 4
|
|
250
|
+
end
|
|
251
|
+
bytes = io.read(n_bytes)
|
|
252
|
+
bytes ? bytes.bytes : Array.new(n_bytes, 0)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def read_255_uint16(io)
|
|
256
|
+
return 0 if io.eof?
|
|
257
|
+
|
|
258
|
+
code = io.read(1)&.unpack1("C")
|
|
259
|
+
return 0 unless code
|
|
260
|
+
|
|
261
|
+
case code
|
|
262
|
+
when 0..252 then code
|
|
263
|
+
when 253
|
|
264
|
+
b = io.read(1)&.unpack1("C") || 0
|
|
265
|
+
253 + b
|
|
266
|
+
when 254
|
|
267
|
+
io.read(2)&.unpack1("n") || 0
|
|
268
|
+
|
|
269
|
+
when 255
|
|
270
|
+
v = io.read(2)&.unpack1("n") || 0
|
|
271
|
+
v + 506
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def read_uint16_io(io)
|
|
276
|
+
io.read(2)&.unpack1("n") || 0
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def bbox_bit_set?(bbox_bitmap, glyph_id)
|
|
280
|
+
return false if bbox_bitmap.nil? || bbox_bitmap.empty?
|
|
281
|
+
|
|
282
|
+
byte_idx = glyph_id >> 3
|
|
283
|
+
bit_idx = glyph_id & 7
|
|
284
|
+
(bbox_bitmap.getbyte(byte_idx) & (0x80 >> bit_idx)).nonzero?
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def overlap_bit_set?(overlap_bitmap, glyph_id)
|
|
288
|
+
return false unless overlap_bitmap && !overlap_bitmap.empty?
|
|
289
|
+
|
|
290
|
+
byte_idx = glyph_id >> 3
|
|
291
|
+
bit_idx = glyph_id & 7
|
|
292
|
+
(overlap_bitmap.getbyte(byte_idx) & (0x80 >> bit_idx)).nonzero?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def read_bbox(io)
|
|
296
|
+
bytes = io.read(8)
|
|
297
|
+
return [0, 0, 0, 0] unless bytes&.bytesize == 8
|
|
298
|
+
|
|
299
|
+
bytes.unpack("s>*")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def calc_bounds(xs, ys)
|
|
303
|
+
return nil if xs.empty?
|
|
304
|
+
|
|
305
|
+
[xs.min, ys.min, xs.max, ys.max]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Build a standard TrueType simple-glyph record from per-point arrays.
|
|
309
|
+
# Re-encodes coordinates using the standard TrueType flag scheme based
|
|
310
|
+
# on per-point delta magnitudes (per OFF section 5.3.3 spec rules).
|
|
311
|
+
def build_simple_glyph_bytes(num_contours:, bbox:, end_pts:, on_curve_flags:,
|
|
312
|
+
xs:, ys:, instructions:, overlap:)
|
|
313
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
314
|
+
out << [num_contours].pack("s>")
|
|
315
|
+
out << bbox.pack("s>*")
|
|
316
|
+
out << end_pts.pack("n*")
|
|
317
|
+
out << [instructions.bytesize].pack("n")
|
|
318
|
+
out << instructions
|
|
319
|
+
|
|
320
|
+
prev_x = prev_y = 0
|
|
321
|
+
tt_flags = []
|
|
322
|
+
x_bytes = String.new(encoding: Encoding::BINARY)
|
|
323
|
+
y_bytes = String.new(encoding: Encoding::BINARY)
|
|
324
|
+
|
|
325
|
+
xs.each_with_index do |x, i|
|
|
326
|
+
y = ys[i]
|
|
327
|
+
dx = x - prev_x
|
|
328
|
+
dy = y - prev_y
|
|
329
|
+
prev_x = x
|
|
330
|
+
prev_y = y
|
|
331
|
+
|
|
332
|
+
base = on_curve_flags[i]
|
|
333
|
+
base |= FLAG_OVERLAP_SIMPLE if overlap && i.zero?
|
|
334
|
+
|
|
335
|
+
flag = encode_axis_bits(base, x_bytes, short_bit: FLAG_X_SHORT,
|
|
336
|
+
same_bit: FLAG_X_SAME_OR_POS,
|
|
337
|
+
value: dx)
|
|
338
|
+
flag = encode_axis_bits(flag, y_bytes, short_bit: FLAG_Y_SHORT,
|
|
339
|
+
same_bit: FLAG_Y_SAME_OR_POS,
|
|
340
|
+
value: dy)
|
|
341
|
+
tt_flags << flag
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
out << encode_flags_rle(tt_flags)
|
|
345
|
+
out << x_bytes
|
|
346
|
+
out << y_bytes
|
|
347
|
+
out
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Encode one axis delta into standard TrueType byte layout. Returns
|
|
351
|
+
# the updated flag value (X bits if short_bit is FLAG_X_SHORT, Y bits
|
|
352
|
+
# if short_bit is FLAG_Y_SHORT).
|
|
353
|
+
def encode_axis_bits(base, out, short_bit:, same_bit:, value:)
|
|
354
|
+
if value.zero?
|
|
355
|
+
base | same_bit
|
|
356
|
+
elsif value.abs <= 255
|
|
357
|
+
flag = base | short_bit
|
|
358
|
+
flag |= same_bit if value.positive?
|
|
359
|
+
out << (value.abs & 0xFF)
|
|
360
|
+
flag
|
|
361
|
+
else
|
|
362
|
+
out << [value & 0xFFFF].pack("n")
|
|
363
|
+
base
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Run-length encode the flags array per OFF spec: when two or more
|
|
368
|
+
# consecutive flags are identical, emit REPEAT_FLAG followed by count.
|
|
369
|
+
def encode_flags_rle(flags)
|
|
370
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
371
|
+
i = 0
|
|
372
|
+
while i < flags.size
|
|
373
|
+
flag = flags[i]
|
|
374
|
+
count = 1
|
|
375
|
+
while i + count < flags.size && flags[i + count] == flag && count < 255
|
|
376
|
+
count += 1
|
|
377
|
+
end
|
|
378
|
+
if count > 1
|
|
379
|
+
out << (flag | FLAG_REPEAT).chr(Encoding::BINARY)
|
|
380
|
+
out << count.chr(Encoding::BINARY)
|
|
381
|
+
i += count
|
|
382
|
+
else
|
|
383
|
+
out << flag.chr(Encoding::BINARY)
|
|
384
|
+
i += 1
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
out
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Build loca table from accumulated glyph offsets per spec section 5.3.
|
|
391
|
+
def build_loca(offsets)
|
|
392
|
+
if @index_format.zero?
|
|
393
|
+
offsets.map { |o| o / 2 }.pack("n*")
|
|
394
|
+
else
|
|
395
|
+
offsets.pack("N*")
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
@@ -41,17 +41,21 @@ module Fontisan
|
|
|
41
41
|
# Simple glyph records start with int16 numberOfContours + 4 int16 bbox.
|
|
42
42
|
SIMPLE_HEADER_SIZE = 10
|
|
43
43
|
|
|
44
|
-
attr_reader :num_glyphs, :
|
|
44
|
+
attr_reader :num_glyphs, :source_index_format
|
|
45
45
|
|
|
46
46
|
# @param glyf_data [String] raw glyf table bytes
|
|
47
47
|
# @param loca_data [String] raw loca table bytes
|
|
48
48
|
# @param num_glyphs [Integer] from maxp
|
|
49
|
-
# @param index_format [Integer] 0 (short) or 1 (long), from head
|
|
49
|
+
# @param index_format [Integer] 0 (short) or 1 (long), from head.
|
|
50
|
+
# Used to parse the source loca; the WOFF2 transformed glyf
|
|
51
|
+
# always emits indexFormat=0 (Chrome OTS silently rejects any
|
|
52
|
+
# other value, even though WOFF2 spec section 5.1 allows the
|
|
53
|
+
# source's indexToLocFormat).
|
|
50
54
|
def initialize(glyf_data:, loca_data:, num_glyphs:, index_format:)
|
|
51
55
|
@glyf_data = glyf_data
|
|
52
56
|
@loca_data = loca_data
|
|
53
57
|
@num_glyphs = num_glyphs
|
|
54
|
-
@
|
|
58
|
+
@source_index_format = index_format
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
# Encode the glyf/loca transform, returning the transformed glyf bytes.
|
|
@@ -251,7 +255,10 @@ module Fontisan
|
|
|
251
255
|
out << [0].pack("S>") # version
|
|
252
256
|
out << [(s.has_overlap? ? 1 : 0)].pack("S>") # optionFlags
|
|
253
257
|
out << [@num_glyphs].pack("S>")
|
|
254
|
-
|
|
258
|
+
# Always emit indexFormat=0 per Chrome OTS requirement. WOFF2 spec
|
|
259
|
+
# section 5.1 allows the source's indexToLocFormat, but Chrome's
|
|
260
|
+
# OTS silently rejects any value other than 0.
|
|
261
|
+
out << [0].pack("S>")
|
|
255
262
|
out << [s.n_contour.bytesize].pack("L>")
|
|
256
263
|
out << [s.n_points.bytesize].pack("L>")
|
|
257
264
|
out << [s.flags.bytesize].pack("L>")
|
|
@@ -271,7 +278,7 @@ module Fontisan
|
|
|
271
278
|
end
|
|
272
279
|
|
|
273
280
|
def parse_loca_offsets
|
|
274
|
-
if @
|
|
281
|
+
if @source_index_format.zero?
|
|
275
282
|
@loca_data.unpack("n*").map { |v| v * 2 }
|
|
276
283
|
else
|
|
277
284
|
@loca_data.unpack("N*")
|
data/lib/fontisan/woff2.rb
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
module Fontisan
|
|
6
6
|
module Woff2
|
|
7
|
+
autoload :CollectionEncoder, "fontisan/woff2/collection_encoder"
|
|
7
8
|
autoload :Directory, "fontisan/woff2/directory"
|
|
8
9
|
autoload :EncoderRules, "fontisan/woff2/encoder_rules"
|
|
10
|
+
autoload :GlyfLocaReconstruct, "fontisan/woff2/glyf_loca_reconstruct"
|
|
9
11
|
autoload :GlyfLocaTransform, "fontisan/woff2/glyf_loca_transform"
|
|
10
|
-
autoload :GlyfTransformer, "fontisan/woff2/glyf_transformer"
|
|
11
12
|
autoload :HmtxTransformer, "fontisan/woff2/hmtx_transformer"
|
|
12
13
|
autoload :TableTransformer, "fontisan/woff2/table_transformer"
|
|
13
14
|
autoload :TripletCodec, "fontisan/woff2/triplet_codec"
|
data/lib/fontisan/woff2_font.rb
CHANGED
|
@@ -538,59 +538,42 @@ module Fontisan
|
|
|
538
538
|
# @param decompressed_tables [Hash<String, String>] Decompressed tables
|
|
539
539
|
# @return [void] Modifies decompressed_tables in place
|
|
540
540
|
def self.apply_transformations!(table_entries, decompressed_tables)
|
|
541
|
-
# Find entries that need transformation
|
|
542
541
|
glyf_entry = table_entries.find { |e| e.tag == "glyf" }
|
|
543
542
|
hmtx_entry = table_entries.find { |e| e.tag == "hmtx" }
|
|
544
543
|
|
|
545
|
-
# Get required metadata for transformations
|
|
546
544
|
maxp_data = decompressed_tables["maxp"]
|
|
547
545
|
hhea_data = decompressed_tables["hhea"]
|
|
546
|
+
head_data = decompressed_tables["head"]
|
|
548
547
|
|
|
549
|
-
return unless maxp_data && hhea_data
|
|
548
|
+
return unless maxp_data && hhea_data && head_data
|
|
550
549
|
|
|
551
|
-
# Parse num_glyphs from maxp table
|
|
552
550
|
# maxp format: version(4) + numGlyphs(2) + ...
|
|
553
551
|
num_glyphs = maxp_data[4, 2].unpack1("n")
|
|
554
|
-
|
|
555
|
-
# Parse numberOfHMetrics from hhea table
|
|
556
552
|
# hhea format: ... + numberOfHMetrics(2) at offset 34
|
|
557
553
|
number_of_h_metrics = hhea_data[34, 2].unpack1("n")
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
#
|
|
563
|
-
#
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
num_glyphs,
|
|
573
|
-
variable_font: variable_font,
|
|
574
|
-
)
|
|
575
|
-
decompressed_tables["glyf"] = result[:glyf]
|
|
576
|
-
decompressed_tables["loca"] = result[:loca]
|
|
577
|
-
end
|
|
554
|
+
# head format: ... + indexToLocFormat(2) at offset 50
|
|
555
|
+
index_format = head_data[50, 2].unpack1("n")
|
|
556
|
+
|
|
557
|
+
# Reconstruct glyf/loca per spec section 5.1/5.3 when glyf is
|
|
558
|
+
# transformed. Per spec section 4.1, transformLength is present IFF
|
|
559
|
+
# the table is transformed, so a non-nil value indicates transform.
|
|
560
|
+
if glyf_entry&.transform_length && decompressed_tables["glyf"]
|
|
561
|
+
result = Woff2::GlyfLocaReconstruct.new(
|
|
562
|
+
transformed_glyf: decompressed_tables["glyf"],
|
|
563
|
+
num_glyphs:,
|
|
564
|
+
index_format:,
|
|
565
|
+
).reconstruct
|
|
566
|
+
decompressed_tables["glyf"] = result[:glyf]
|
|
567
|
+
decompressed_tables["loca"] = result[:loca]
|
|
578
568
|
end
|
|
579
569
|
|
|
580
|
-
#
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if transformed_hmtx
|
|
588
|
-
decompressed_tables["hmtx"] = Woff2::HmtxTransformer.reconstruct(
|
|
589
|
-
transformed_hmtx,
|
|
590
|
-
num_glyphs,
|
|
591
|
-
number_of_h_metrics,
|
|
592
|
-
)
|
|
593
|
-
end
|
|
570
|
+
# Reconstruct hmtx per spec section 5.4 when hmtx is transformed.
|
|
571
|
+
if hmtx_entry&.transform_length && decompressed_tables["hmtx"]
|
|
572
|
+
decompressed_tables["hmtx"] = Woff2::HmtxTransformer.reconstruct(
|
|
573
|
+
decompressed_tables["hmtx"],
|
|
574
|
+
num_glyphs,
|
|
575
|
+
number_of_h_metrics,
|
|
576
|
+
)
|
|
594
577
|
end
|
|
595
578
|
end
|
|
596
579
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -718,10 +718,11 @@ files:
|
|
|
718
718
|
- lib/fontisan/version.rb
|
|
719
719
|
- lib/fontisan/version.rb.orig
|
|
720
720
|
- lib/fontisan/woff2.rb
|
|
721
|
+
- lib/fontisan/woff2/collection_encoder.rb
|
|
721
722
|
- lib/fontisan/woff2/directory.rb
|
|
722
723
|
- lib/fontisan/woff2/encoder_rules.rb
|
|
724
|
+
- lib/fontisan/woff2/glyf_loca_reconstruct.rb
|
|
723
725
|
- lib/fontisan/woff2/glyf_loca_transform.rb
|
|
724
|
-
- lib/fontisan/woff2/glyf_transformer.rb
|
|
725
726
|
- lib/fontisan/woff2/header.rb
|
|
726
727
|
- lib/fontisan/woff2/hmtx_transformer.rb
|
|
727
728
|
- lib/fontisan/woff2/table_transformer.rb
|