fontisan 0.4.15 → 0.4.16
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/converters/woff2_encoder.rb +30 -9
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +3 -23
- data/lib/fontisan/woff2/collection_encoder.rb +3 -15
- data/lib/fontisan/woff2/directory.rb +4 -32
- data/lib/fontisan/woff2/encoder_rules.rb +0 -7
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +3 -23
- data/lib/fontisan/woff2/glyf_loca_transform.rb +8 -19
- data/lib/fontisan/woff2/hmtx_transformer.rb +2 -27
- data/lib/fontisan/woff2/table_transformer.rb +3 -19
- data/lib/fontisan/woff2/uint255.rb +101 -0
- data/lib/fontisan/woff2.rb +1 -0
- data/lib/fontisan/woff2_font.rb +0 -16
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d27d25052549a912eab7392f8f0b34e7917fad9e9344d178b853bfe5a9fb476
|
|
4
|
+
data.tar.gz: 7fe03e99020f0a628efa4bac17236c455143345dbe4aef1dd1df5a8890cf00ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ddb0c714d55d9f0e3b6b2ae02322f210d2392391d604368628cf6a789e2c1633645b08d4ee1edca6e05c0dc87e7c6e6ec550bc412bcc79fe751cc21f1e55f62
|
|
7
|
+
data.tar.gz: f8a646128fbadfd95df57bffc933a17095b3fb228603c941471198186df112bc517d836d09c567c1456b5aa8caed3aec4c14f8c561ba4cc19a4561bf75124940
|
|
@@ -227,6 +227,12 @@ module Fontisan
|
|
|
227
227
|
# are kept in `table_data` so its `origLength` is preserved; the
|
|
228
228
|
# transformed bytes are passed to the entries builder separately.
|
|
229
229
|
#
|
|
230
|
+
# glyf.origLength is computed by round-tripping the transformed glyf
|
|
231
|
+
# through the decoder — this guarantees the directory matches what
|
|
232
|
+
# any compliant decoder will produce, including padding and minor
|
|
233
|
+
# re-encoding differences (OFF section 5.3.3 allows multiple valid
|
|
234
|
+
# reconstructions of the same glyph data).
|
|
235
|
+
#
|
|
230
236
|
# @return [Hash{Symbol => Object}, nil]
|
|
231
237
|
def apply_glyf_loca_transform!(table_data, font)
|
|
232
238
|
return nil unless font.respond_to?(:has_table?)
|
|
@@ -240,17 +246,32 @@ module Fontisan
|
|
|
240
246
|
head = font.table("head")
|
|
241
247
|
return nil unless maxp && head
|
|
242
248
|
|
|
249
|
+
index_format = head.index_to_loc_format
|
|
250
|
+
|
|
243
251
|
transformed = Woff2::GlyfLocaTransform.new(
|
|
244
252
|
glyf_data:,
|
|
245
253
|
loca_data:,
|
|
246
254
|
num_glyphs: maxp.num_glyphs,
|
|
247
|
-
index_format
|
|
255
|
+
index_format:,
|
|
248
256
|
).transform
|
|
249
257
|
|
|
250
|
-
|
|
258
|
+
# Round-trip through the decoder to get the exact reconstructed
|
|
259
|
+
# sizes the directory must advertise. fontTools re-compiles glyf
|
|
260
|
+
# during encoding, producing sizes that differ from the source by
|
|
261
|
+
# small amounts (encoding variations per OFF section 5.3.3). Using
|
|
262
|
+
# the source bytesize causes fontTools' decoder to reject the
|
|
263
|
+
# WOFF2 with "not enough 'glyf' table data".
|
|
264
|
+
reconstructed = Woff2::GlyfLocaReconstruct.new(
|
|
265
|
+
transformed_glyf: transformed,
|
|
266
|
+
num_glyphs: maxp.num_glyphs,
|
|
267
|
+
index_format:,
|
|
268
|
+
).reconstruct
|
|
269
|
+
|
|
270
|
+
glyf_orig_length = reconstructed[:glyf].bytesize
|
|
271
|
+
loca_orig_length = reconstructed[:loca].bytesize
|
|
251
272
|
table_data.delete("loca")
|
|
252
273
|
|
|
253
|
-
{ transformed_glyf: transformed, loca_orig_length: }
|
|
274
|
+
{ transformed_glyf: transformed, glyf_orig_length:, loca_orig_length: }
|
|
254
275
|
end
|
|
255
276
|
|
|
256
277
|
def get_table_data(font, tag)
|
|
@@ -277,7 +298,7 @@ module Fontisan
|
|
|
277
298
|
|
|
278
299
|
table_data.keys.sort.each do |tag|
|
|
279
300
|
if tag == "glyf" && glyf_transform
|
|
280
|
-
entries << build_glyf_entry(
|
|
301
|
+
entries << build_glyf_entry(glyf_transform[:glyf_orig_length],
|
|
281
302
|
glyf_transform[:transformed_glyf])
|
|
282
303
|
entries << build_loca_entry(glyf_transform[:loca_orig_length])
|
|
283
304
|
transformed_data["glyf"] = glyf_transform[:transformed_glyf]
|
|
@@ -314,13 +335,13 @@ module Fontisan
|
|
|
314
335
|
entry
|
|
315
336
|
end
|
|
316
337
|
|
|
317
|
-
# Build the glyf directory entry. origLength is the
|
|
318
|
-
# glyf size; transformLength is the
|
|
319
|
-
# glyf stream per spec section 5.1.
|
|
320
|
-
def build_glyf_entry(
|
|
338
|
+
# Build the glyf directory entry. origLength is the reconstructed
|
|
339
|
+
# glyf size (what a decoder will produce); transformLength is the
|
|
340
|
+
# size of the WOFF2-transformed glyf stream per spec section 5.1.
|
|
341
|
+
def build_glyf_entry(glyf_orig_length, transformed_data)
|
|
321
342
|
entry = Woff2::Directory::Entry.new
|
|
322
343
|
entry.tag = "glyf"
|
|
323
|
-
entry.orig_length =
|
|
344
|
+
entry.orig_length = glyf_orig_length
|
|
324
345
|
entry.transform_length = transformed_data.bytesize
|
|
325
346
|
entry.flags = entry.calculate_flags
|
|
326
347
|
entry
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -133,14 +133,14 @@ module Fontisan
|
|
|
133
133
|
pos += 4
|
|
134
134
|
raise InvalidFontError, "Unsupported CollectionHeader version 0x#{version.to_s(16)}" unless version == 0x00010000
|
|
135
135
|
|
|
136
|
-
num_fonts, pos =
|
|
136
|
+
num_fonts, pos = UInt255.decode_at(@data, pos)
|
|
137
137
|
font_entries = Array.new(num_fonts) do
|
|
138
|
-
num_tables_f, pos2 =
|
|
138
|
+
num_tables_f, pos2 = UInt255.decode_at(@data, pos)
|
|
139
139
|
pos = pos2
|
|
140
140
|
flavor = @data[pos, 4].unpack1("N")
|
|
141
141
|
pos += 4
|
|
142
142
|
indices = Array.new(num_tables_f) do
|
|
143
|
-
idx, pos2 =
|
|
143
|
+
idx, pos2 = UInt255.decode_at(@data, pos)
|
|
144
144
|
pos = pos2
|
|
145
145
|
idx
|
|
146
146
|
end
|
|
@@ -201,26 +201,6 @@ module Fontisan
|
|
|
201
201
|
end
|
|
202
202
|
raise InvalidFontError, "UIntBase128 sequence exceeds 5 bytes"
|
|
203
203
|
end
|
|
204
|
-
|
|
205
|
-
def read_255_uint16(pos)
|
|
206
|
-
code = @data.getbyte(pos)
|
|
207
|
-
pos += 1
|
|
208
|
-
case code
|
|
209
|
-
when 0..252 then [code, pos]
|
|
210
|
-
when 253
|
|
211
|
-
v = @data.getbyte(pos)
|
|
212
|
-
pos += 1
|
|
213
|
-
[253 + v, pos]
|
|
214
|
-
when 254
|
|
215
|
-
v = @data[pos, 2].unpack1("n")
|
|
216
|
-
pos += 2
|
|
217
|
-
[v, pos]
|
|
218
|
-
when 255
|
|
219
|
-
v = @data[pos, 2].unpack1("n")
|
|
220
|
-
pos += 2
|
|
221
|
-
[v + 506, pos]
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
204
|
end
|
|
225
205
|
end
|
|
226
206
|
end
|
|
@@ -238,7 +238,7 @@ module Fontisan
|
|
|
238
238
|
def write_collection_directory(io, fonts, font_to_ckeys, ckey_to_index)
|
|
239
239
|
# CollectionHeader
|
|
240
240
|
io.write([0x00010000].pack("N")) # version 1.0
|
|
241
|
-
io.write(
|
|
241
|
+
io.write(UInt255.encode(fonts.size))
|
|
242
242
|
|
|
243
243
|
# One CollectionFontEntry per font
|
|
244
244
|
fonts.each_with_index do |font, fi|
|
|
@@ -254,12 +254,12 @@ module Fontisan
|
|
|
254
254
|
ordered << "loca" if has_placeholder
|
|
255
255
|
ordered = ordered.sort
|
|
256
256
|
|
|
257
|
-
io.write(
|
|
257
|
+
io.write(UInt255.encode(ordered.size))
|
|
258
258
|
io.write([font_flavor(font)].pack("N"))
|
|
259
259
|
ordered.each do |tag|
|
|
260
260
|
ckey = font_keys[tag] || placeholder_key
|
|
261
261
|
index = ckey_to_index.fetch(ckey)
|
|
262
|
-
io.write(
|
|
262
|
+
io.write(UInt255.encode(index))
|
|
263
263
|
end
|
|
264
264
|
end
|
|
265
265
|
end
|
|
@@ -271,18 +271,6 @@ module Fontisan
|
|
|
271
271
|
Constants::SFNT_VERSION_TRUETYPE
|
|
272
272
|
end
|
|
273
273
|
end
|
|
274
|
-
|
|
275
|
-
def encode_255_uint16(value)
|
|
276
|
-
if value < 253
|
|
277
|
-
[value].pack("C")
|
|
278
|
-
elsif value < 506
|
|
279
|
-
[253, value - 253].pack("CC")
|
|
280
|
-
elsif value < 65_536
|
|
281
|
-
[254].pack("C") + [value].pack("n")
|
|
282
|
-
else
|
|
283
|
-
[255].pack("C") + [value - 506].pack("n")
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
274
|
end
|
|
287
275
|
end
|
|
288
276
|
end
|
|
@@ -235,48 +235,20 @@ module Fontisan
|
|
|
235
235
|
raise Fontisan::Error, "Invalid UIntBase128 encoding"
|
|
236
236
|
end
|
|
237
237
|
|
|
238
|
-
# Encode 255UInt16
|
|
239
|
-
#
|
|
240
|
-
# Used in transformed glyf table:
|
|
241
|
-
# - 0-252: value itself (1 byte)
|
|
242
|
-
# - 253: next byte + 253 (2 bytes)
|
|
243
|
-
# - 254: next 2 bytes as big-endian (3 bytes)
|
|
244
|
-
# - 255: next 2 bytes + 506 (3 bytes)
|
|
238
|
+
# Encode 255UInt16. Delegates to {UInt255} (single source of truth).
|
|
245
239
|
#
|
|
246
240
|
# @param value [Integer] Value to encode (0-65535)
|
|
247
241
|
# @return [String] Binary encoded data
|
|
248
242
|
def self.encode_255_uint16(value)
|
|
249
|
-
|
|
250
|
-
[value].pack("C")
|
|
251
|
-
elsif value < 506
|
|
252
|
-
[253, value - 253].pack("C*")
|
|
253
|
-
elsif value < 65536
|
|
254
|
-
[254].pack("C") + [value].pack("n")
|
|
255
|
-
else
|
|
256
|
-
[255].pack("C") + [value - 506].pack("n")
|
|
257
|
-
end
|
|
243
|
+
UInt255.encode(value)
|
|
258
244
|
end
|
|
259
245
|
|
|
260
|
-
# Decode 255UInt16 from IO
|
|
246
|
+
# Decode 255UInt16 from IO. Delegates to {UInt255}.
|
|
261
247
|
#
|
|
262
248
|
# @param io [IO] Input stream
|
|
263
249
|
# @return [Integer] Decoded value
|
|
264
250
|
def self.decode_255_uint16(io)
|
|
265
|
-
|
|
266
|
-
return nil unless first
|
|
267
|
-
|
|
268
|
-
case first
|
|
269
|
-
when 0..252
|
|
270
|
-
first
|
|
271
|
-
when 253
|
|
272
|
-
second = io.read(1)&.unpack1("C")
|
|
273
|
-
253 + second
|
|
274
|
-
when 254
|
|
275
|
-
io.read(2)&.unpack1("n")
|
|
276
|
-
when 255
|
|
277
|
-
value = io.read(2)&.unpack1("n")
|
|
278
|
-
value + 506
|
|
279
|
-
end
|
|
251
|
+
UInt255.decode(io)
|
|
280
252
|
end
|
|
281
253
|
end
|
|
282
254
|
end
|
|
@@ -52,12 +52,6 @@ module Fontisan
|
|
|
52
52
|
# the 'flags' field of the head table ... to indicate that a recreated
|
|
53
53
|
# font file was subjected to lossless modifying transform."
|
|
54
54
|
#
|
|
55
|
-
# Also forces head.indexToLocFormat=0 when the glyf/loca tables are
|
|
56
|
-
# transformed, because Chrome's OTS only accepts WOFF2 glyf
|
|
57
|
-
# indexFormat=0 (see GlyfLocaTransform). For the reconstructed font
|
|
58
|
-
# to be self-consistent, head.indexToLocFormat must match the loca
|
|
59
|
-
# format that the decoder will produce (always short, format=0).
|
|
60
|
-
#
|
|
61
55
|
# Uses the model-driven Head BinData record so the bits are set via a
|
|
62
56
|
# named attribute on a typed object, not via raw byte slicing.
|
|
63
57
|
def self.mark_lossless_modifying!(table_data)
|
|
@@ -65,7 +59,6 @@ module Fontisan
|
|
|
65
59
|
|
|
66
60
|
head = Tables::Head.read(table_data["head"])
|
|
67
61
|
head.flags |= Tables::Head::FLAG_LOSSLESS_MODIFYING
|
|
68
|
-
head.index_to_loc_format = 0
|
|
69
62
|
table_data["head"] = head.to_binary_s
|
|
70
63
|
end
|
|
71
64
|
end
|
|
@@ -143,7 +143,7 @@ module Fontisan
|
|
|
143
143
|
end_pts = []
|
|
144
144
|
cumulative = -1
|
|
145
145
|
num_contours.times do
|
|
146
|
-
pts_of_contour =
|
|
146
|
+
pts_of_contour = UInt255.decode(streams[:n_points])
|
|
147
147
|
cumulative += pts_of_contour
|
|
148
148
|
end_pts << cumulative
|
|
149
149
|
end
|
|
@@ -169,7 +169,7 @@ module Fontisan
|
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
# Step 4-5: read instructionLength from glyphStream, bytes from instructionStream.
|
|
172
|
-
inst_len =
|
|
172
|
+
inst_len = UInt255.decode(streams[:glyph])
|
|
173
173
|
instructions = streams[:instructions].read(inst_len)
|
|
174
174
|
|
|
175
175
|
# Bbox: emit explicit bbox if bboxBitmap bit is set for this glyph,
|
|
@@ -209,7 +209,7 @@ module Fontisan
|
|
|
209
209
|
|
|
210
210
|
instructions = String.new(encoding: Encoding::BINARY)
|
|
211
211
|
if have_instructions
|
|
212
|
-
inst_len =
|
|
212
|
+
inst_len = UInt255.decode(streams[:glyph])
|
|
213
213
|
instructions = streams[:instructions].read(inst_len)
|
|
214
214
|
end
|
|
215
215
|
|
|
@@ -252,26 +252,6 @@ module Fontisan
|
|
|
252
252
|
bytes ? bytes.bytes : Array.new(n_bytes, 0)
|
|
253
253
|
end
|
|
254
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
255
|
def read_uint16_io(io)
|
|
276
256
|
io.read(2)&.unpack1("n") || 0
|
|
277
257
|
end
|
|
@@ -127,7 +127,7 @@ module Fontisan
|
|
|
127
127
|
# nPoints stream: per-contour point counts (NOT endPtsOfContours).
|
|
128
128
|
prev_end = -1
|
|
129
129
|
end_pts.each do |ep|
|
|
130
|
-
s.n_points <<
|
|
130
|
+
s.n_points << UInt255.encode(ep - prev_end)
|
|
131
131
|
prev_end = ep
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -157,7 +157,7 @@ module Fontisan
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
# glyphStream carries per-glyph instructionLength; bytes go to instructionStream
|
|
160
|
-
s.glyph <<
|
|
160
|
+
s.glyph << UInt255.encode(instructions.bytesize)
|
|
161
161
|
s.instructions << instructions
|
|
162
162
|
|
|
163
163
|
# Spec: simple glyphs omit bbox when it matches calculated bounds.
|
|
@@ -191,7 +191,7 @@ module Fontisan
|
|
|
191
191
|
if have_instructions
|
|
192
192
|
inst_len = io.read(2).unpack1("n")
|
|
193
193
|
instructions = io.read(inst_len)
|
|
194
|
-
s.glyph <<
|
|
194
|
+
s.glyph << UInt255.encode(instructions.bytesize)
|
|
195
195
|
s.instructions << instructions
|
|
196
196
|
end
|
|
197
197
|
|
|
@@ -255,10 +255,11 @@ module Fontisan
|
|
|
255
255
|
out << [0].pack("S>") # version
|
|
256
256
|
out << [(s.has_overlap? ? 1 : 0)].pack("S>") # optionFlags
|
|
257
257
|
out << [@num_glyphs].pack("S>")
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
|
|
258
|
+
# Emit the source's indexFormat. The earlier "always 0" override
|
|
259
|
+
# (commit 5f7e32d) was based on a misdiagnosis — Chrome's OTS
|
|
260
|
+
# accepts both 0 and 1, but short loca (0) can't address glyf
|
|
261
|
+
# offsets > 0x20000 bytes, so large fonts MUST use 1.
|
|
262
|
+
out << [@source_index_format].pack("S>")
|
|
262
263
|
out << [s.n_contour.bytesize].pack("L>")
|
|
263
264
|
out << [s.n_points.bytesize].pack("L>")
|
|
264
265
|
out << [s.flags.bytesize].pack("L>")
|
|
@@ -313,18 +314,6 @@ module Fontisan
|
|
|
313
314
|
|
|
314
315
|
[xs.min, ys.min, xs.max, ys.max]
|
|
315
316
|
end
|
|
316
|
-
|
|
317
|
-
def encode_255_uint16(value)
|
|
318
|
-
if value < 253
|
|
319
|
-
[value].pack("C")
|
|
320
|
-
elsif value < 506
|
|
321
|
-
[253, value - 253].pack("CC")
|
|
322
|
-
elsif value < 65_536
|
|
323
|
-
[254].pack("C") + [value].pack("n")
|
|
324
|
-
else
|
|
325
|
-
[255].pack("C") + [value - 506].pack("n")
|
|
326
|
-
end
|
|
327
|
-
end
|
|
328
317
|
end
|
|
329
318
|
end
|
|
330
319
|
end
|
|
@@ -46,7 +46,7 @@ glyf_lsbs = nil)
|
|
|
46
46
|
if (flags & HMTX_FLAG_EXPLICIT_ADVANCE_WIDTHS).zero?
|
|
47
47
|
# Proportional encoding - read deltas
|
|
48
48
|
# First advance width is explicit
|
|
49
|
-
first_advance =
|
|
49
|
+
first_advance = UInt255.decode(io)
|
|
50
50
|
advance_widths << first_advance
|
|
51
51
|
|
|
52
52
|
# Remaining are deltas from previous
|
|
@@ -57,7 +57,7 @@ glyf_lsbs = nil)
|
|
|
57
57
|
else
|
|
58
58
|
# Explicit advance widths in transformed format
|
|
59
59
|
num_h_metrics.times do
|
|
60
|
-
advance_widths <<
|
|
60
|
+
advance_widths << UInt255.decode(io)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -88,31 +88,6 @@ glyf_lsbs = nil)
|
|
|
88
88
|
build_hmtx_table(advance_widths, lsbs, num_h_metrics, num_glyphs)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# Read variable-length 255UInt16 integer
|
|
92
|
-
#
|
|
93
|
-
# Format from WOFF2 spec:
|
|
94
|
-
# - value < 253: one byte
|
|
95
|
-
# - value == 253: 253 + next uint16
|
|
96
|
-
# - value == 254: 253 * 2 + next uint16
|
|
97
|
-
# - value == 255: 253 * 3 + next uint16
|
|
98
|
-
#
|
|
99
|
-
# @param io [StringIO] Input stream
|
|
100
|
-
# @return [Integer] Decoded value
|
|
101
|
-
def self.read_255_uint16(io)
|
|
102
|
-
code = read_uint8(io)
|
|
103
|
-
|
|
104
|
-
case code
|
|
105
|
-
when 255
|
|
106
|
-
759 + read_uint16(io) # 253 * 3 + value
|
|
107
|
-
when 254
|
|
108
|
-
506 + read_uint16(io) # 253 * 2 + value
|
|
109
|
-
when 253
|
|
110
|
-
253 + read_uint16(io)
|
|
111
|
-
else
|
|
112
|
-
code
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
91
|
# Build standard hmtx table format
|
|
117
92
|
#
|
|
118
93
|
# Standard hmtx format:
|
|
@@ -401,7 +401,7 @@ num_glyphs)
|
|
|
401
401
|
|
|
402
402
|
# Write advance widths using proportional encoding
|
|
403
403
|
# First advance width is explicit
|
|
404
|
-
data <<
|
|
404
|
+
data << UInt255.encode(advance_widths[0])
|
|
405
405
|
|
|
406
406
|
# Remaining advance widths as deltas
|
|
407
407
|
(1...num_h_metrics).each do |i|
|
|
@@ -473,7 +473,7 @@ num_glyphs)
|
|
|
473
473
|
prev_pt = -1
|
|
474
474
|
glyph[:end_pts].each do |pt|
|
|
475
475
|
delta = pt - prev_pt - 1
|
|
476
|
-
n_points_stream <<
|
|
476
|
+
n_points_stream << UInt255.encode(delta)
|
|
477
477
|
prev_pt = pt
|
|
478
478
|
end
|
|
479
479
|
|
|
@@ -488,7 +488,7 @@ num_glyphs)
|
|
|
488
488
|
glyph[:bbox].each { |v| bbox_stream << [v].pack("n") }
|
|
489
489
|
|
|
490
490
|
# Write instructions
|
|
491
|
-
instruction_stream <<
|
|
491
|
+
instruction_stream << UInt255.encode(glyph[:instructions].bytesize)
|
|
492
492
|
instruction_stream << glyph[:instructions] if glyph[:instructions].bytesize.positive?
|
|
493
493
|
|
|
494
494
|
when :composite
|
|
@@ -569,22 +569,6 @@ num_glyphs)
|
|
|
569
569
|
end
|
|
570
570
|
end
|
|
571
571
|
|
|
572
|
-
# Encode 255UInt16 value
|
|
573
|
-
#
|
|
574
|
-
# @param value [Integer] Value to encode
|
|
575
|
-
# @return [String] Encoded bytes
|
|
576
|
-
def encode_255_uint16(value)
|
|
577
|
-
if value < 253
|
|
578
|
-
[value].pack("C")
|
|
579
|
-
elsif value < 506
|
|
580
|
-
[253, value - 253].pack("CC")
|
|
581
|
-
elsif value < 65536
|
|
582
|
-
[254].pack("C") + [value].pack("n")
|
|
583
|
-
else
|
|
584
|
-
[255].pack("C") + [value - 506].pack("n")
|
|
585
|
-
end
|
|
586
|
-
end
|
|
587
|
-
|
|
588
572
|
# Read signed 16-bit integer
|
|
589
573
|
#
|
|
590
574
|
# @param io [StringIO] Input stream
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# 255UInt16 variable-length encoding per WOFF2 spec section 3.1.
|
|
8
|
+
#
|
|
9
|
+
# Single source of truth for encoding and decoding this data type. All
|
|
10
|
+
# other WOFF2 code MUST delegate to this module — the previous inline
|
|
11
|
+
# copies had the code-byte mapping inverted for values ≥ 253.
|
|
12
|
+
#
|
|
13
|
+
# Encoding table (per W3C spec pseudocode):
|
|
14
|
+
#
|
|
15
|
+
# value range | encoding | bytes
|
|
16
|
+
# ----------------|-----------------------|------
|
|
17
|
+
# 0 .. 252 | literal | 1
|
|
18
|
+
# 253 .. 505 | code 255 + (v - 253) | 2
|
|
19
|
+
# 506 .. 761 | code 254 + (v - 506) | 2
|
|
20
|
+
# 762 .. 65535 | code 253 + uint16 | 3
|
|
21
|
+
#
|
|
22
|
+
# Decoding (inverse):
|
|
23
|
+
#
|
|
24
|
+
# code byte | meaning
|
|
25
|
+
# ----------|--------------------------------
|
|
26
|
+
# 0 .. 252 | literal value
|
|
27
|
+
# 253 | read uint16 → value
|
|
28
|
+
# 254 | read 1 byte → value + 506
|
|
29
|
+
# 255 | read 1 byte → value + 253
|
|
30
|
+
module UInt255
|
|
31
|
+
WORD_CODE = 253
|
|
32
|
+
ONE_MORE_BYTE_CODE1 = 255 # adds 253
|
|
33
|
+
ONE_MORE_BYTE_CODE2 = 254 # adds 506
|
|
34
|
+
|
|
35
|
+
MAX_VALUE = 65_535
|
|
36
|
+
|
|
37
|
+
# Encode a value (0..65535) into a 1-3 byte binary string.
|
|
38
|
+
#
|
|
39
|
+
# @param value [Integer]
|
|
40
|
+
# @return [String] binary-encoded bytes
|
|
41
|
+
# @raise [ArgumentError] if value is out of range
|
|
42
|
+
def self.encode(value)
|
|
43
|
+
case value
|
|
44
|
+
when 0..252
|
|
45
|
+
[value].pack("C")
|
|
46
|
+
when 253..505
|
|
47
|
+
[ONE_MORE_BYTE_CODE1, value - 253].pack("C*")
|
|
48
|
+
when 506..761
|
|
49
|
+
[ONE_MORE_BYTE_CODE2, value - 506].pack("C*")
|
|
50
|
+
when 762..MAX_VALUE
|
|
51
|
+
[WORD_CODE].pack("C") + [value].pack("n")
|
|
52
|
+
else
|
|
53
|
+
raise ArgumentError,
|
|
54
|
+
"255UInt16 requires 0 <= value <= #{MAX_VALUE}, got #{value}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Decode a 255UInt16 value from an IO stream. Reads 1-3 bytes.
|
|
59
|
+
#
|
|
60
|
+
# @param io [IO, StringIO] input stream positioned at the code byte
|
|
61
|
+
# @return [Integer, nil] decoded value, or nil if the stream is empty
|
|
62
|
+
def self.decode(io)
|
|
63
|
+
code = io.read(1)&.unpack1("C")
|
|
64
|
+
return nil unless code
|
|
65
|
+
|
|
66
|
+
case code
|
|
67
|
+
when 0..252
|
|
68
|
+
code
|
|
69
|
+
when WORD_CODE
|
|
70
|
+
io.read(2)&.unpack1("n")
|
|
71
|
+
when ONE_MORE_BYTE_CODE2
|
|
72
|
+
506 + (io.read(1)&.unpack1("C") || 0)
|
|
73
|
+
when ONE_MORE_BYTE_CODE1
|
|
74
|
+
253 + (io.read(1)&.unpack1("C") || 0)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Decode from a binary String at a given position. Returns the value
|
|
79
|
+
# and the new cursor position. Used by callers that index into a
|
|
80
|
+
# flat String rather than wrapping it in a StringIO.
|
|
81
|
+
#
|
|
82
|
+
# @param data [String] binary data
|
|
83
|
+
# @param pos [Integer] byte offset of the code byte
|
|
84
|
+
# @return [Array(Integer, Integer)] [value, new_pos]
|
|
85
|
+
def self.decode_at(data, pos)
|
|
86
|
+
code = data.getbyte(pos)
|
|
87
|
+
pos += 1
|
|
88
|
+
case code
|
|
89
|
+
when 0..252
|
|
90
|
+
[code, pos]
|
|
91
|
+
when WORD_CODE
|
|
92
|
+
[data[pos, 2].unpack1("n"), pos + 2]
|
|
93
|
+
when ONE_MORE_BYTE_CODE2
|
|
94
|
+
[506 + data.getbyte(pos), pos + 1]
|
|
95
|
+
when ONE_MORE_BYTE_CODE1
|
|
96
|
+
[253 + data.getbyte(pos), pos + 1]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/fontisan/woff2.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Fontisan
|
|
|
13
13
|
autoload :HmtxTransformer, "fontisan/woff2/hmtx_transformer"
|
|
14
14
|
autoload :TableTransformer, "fontisan/woff2/table_transformer"
|
|
15
15
|
autoload :TripletCodec, "fontisan/woff2/triplet_codec"
|
|
16
|
+
autoload :UInt255, "fontisan/woff2/uint255"
|
|
16
17
|
autoload :Woff2Header, "fontisan/woff2/header"
|
|
17
18
|
end
|
|
18
19
|
end
|
data/lib/fontisan/woff2_font.rb
CHANGED
|
@@ -732,22 +732,6 @@ module Fontisan
|
|
|
732
732
|
self.class.read_uint_base128_from_io(io)
|
|
733
733
|
end
|
|
734
734
|
|
|
735
|
-
# Read 255UInt16 variable-length integer
|
|
736
|
-
def read_255_uint16(io)
|
|
737
|
-
code = io.read(1).unpack1("C")
|
|
738
|
-
|
|
739
|
-
case code
|
|
740
|
-
when 0..252
|
|
741
|
-
code
|
|
742
|
-
when 253
|
|
743
|
-
253 + io.read(1).unpack1("C")
|
|
744
|
-
when 254
|
|
745
|
-
io.read(2).unpack1("n")
|
|
746
|
-
when 255
|
|
747
|
-
io.read(2).unpack1("n") + 506
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
|
|
751
735
|
# Calculate offset table fields
|
|
752
736
|
def calculate_offset_table_fields(num_tables)
|
|
753
737
|
entry_selector = (Math.log(num_tables) / Math.log(2)).floor
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -729,6 +729,7 @@ files:
|
|
|
729
729
|
- lib/fontisan/woff2/hmtx_transformer.rb
|
|
730
730
|
- lib/fontisan/woff2/table_transformer.rb
|
|
731
731
|
- lib/fontisan/woff2/triplet_codec.rb
|
|
732
|
+
- lib/fontisan/woff2/uint255.rb
|
|
732
733
|
- lib/fontisan/woff2_collection.rb
|
|
733
734
|
- lib/fontisan/woff2_font.rb
|
|
734
735
|
- lib/fontisan/woff_font.rb
|