fontisan 0.4.17 → 0.4.18
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 +102 -28
- data/lib/fontisan/utilities/brotli_wrapper.rb +1 -16
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/encoder_rules.rb +39 -0
- data/lib/fontisan/woff2/glyf_canonicalizer.rb +89 -0
- data/lib/fontisan/woff2/sfnt_checksum.rb +108 -0
- data/lib/fontisan/woff2.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2991840074aedcf587e7bbc673cd6b26b3c887f5e6b481b83c47d6a43151d287
|
|
4
|
+
data.tar.gz: 8cd0901c7e2fe8953e7ea24349c11c63b651ca989abbdfc4243e0ed3dba280f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37053820640d91782777be8982ac22cf0bc09cdcaff60b484161ce711785ebb9462d3c333fe4564699e381db4557656d049f77394b64ecb2935d54e1f80d93be
|
|
7
|
+
data.tar.gz: 92d7207a5babbbfb155b4aec2cd9f4adfff4efcbe84886f58480a14596fcfa9941f6e406e4c7e4b3f9a822e7169277767cfd69ed8841ab4de3acfb8bdd64cb9e
|
|
@@ -28,6 +28,11 @@ module Fontisan
|
|
|
28
28
|
class Woff2Encoder
|
|
29
29
|
include ConversionStrategy
|
|
30
30
|
|
|
31
|
+
# Highest glyf offset addressable by short loca (uint16 max × 2).
|
|
32
|
+
# Above this, loca MUST use the long format. Public so specs can
|
|
33
|
+
# reference the same threshold the encoder uses.
|
|
34
|
+
SHORT_LOCA_GLYF_MAX = 0x1FFFE
|
|
35
|
+
|
|
31
36
|
# @return [Hash] Configuration settings
|
|
32
37
|
attr_reader :config
|
|
33
38
|
|
|
@@ -83,6 +88,13 @@ module Fontisan
|
|
|
83
88
|
transform_enabled,
|
|
84
89
|
glyf_transform:)
|
|
85
90
|
|
|
91
|
+
# Recompute head.checksumAdjustment for the SFNT the decoder
|
|
92
|
+
# reconstructs. Chrome's OTS validates this strictly; a stale
|
|
93
|
+
# source value fails once glyf/loca/head have been modified.
|
|
94
|
+
# Must run AFTER touch_head_modified! so the modified timestamp
|
|
95
|
+
# change is included in the checksum.
|
|
96
|
+
update_checksum_adjustment!(table_data, flavor:, glyf_transform:)
|
|
97
|
+
|
|
86
98
|
compressed_data = compress_tables(entries, table_data,
|
|
87
99
|
transformed_data, quality)
|
|
88
100
|
|
|
@@ -221,17 +233,17 @@ module Fontisan
|
|
|
221
233
|
|
|
222
234
|
# Apply the WOFF2 glyf/loca paired transform per spec section 5.1/5.3.
|
|
223
235
|
#
|
|
224
|
-
# Returns a hash with the transformed glyf bytes and
|
|
225
|
-
# loca
|
|
226
|
-
#
|
|
227
|
-
# are kept in `table_data` so its `origLength` is preserved; the
|
|
228
|
-
# transformed bytes are passed to the entries builder separately.
|
|
236
|
+
# Returns a hash with the transformed glyf bytes and reconstructed
|
|
237
|
+
# glyf/loca bytes the directory/checksum must reference, or nil for
|
|
238
|
+
# CFF fonts or fonts missing glyf/loca.
|
|
229
239
|
#
|
|
230
|
-
# glyf
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
240
|
+
# The source glyf is first canonicalized to 4-byte-per-glyph
|
|
241
|
+
# alignment via `Woff2::GlyfCanonicalizer`, matching fontTools'
|
|
242
|
+
# `_normaliseGlyfAndLoca(padding=4)`. Chrome's OTS fails to
|
|
243
|
+
# reconstruct "unpadded" glyf tables — see fontTools'
|
|
244
|
+
# WOFF2Writer.close() comment. The canonical glyf is the input to
|
|
245
|
+
# the WOFF2 transform, the value of `glyf.origLength` in the
|
|
246
|
+
# directory, and the bytes the SFNT checksum is computed over.
|
|
235
247
|
#
|
|
236
248
|
# @return [Hash{Symbol => Object}, nil]
|
|
237
249
|
def apply_glyf_loca_transform!(table_data, font)
|
|
@@ -246,32 +258,55 @@ module Fontisan
|
|
|
246
258
|
head = font.table("head")
|
|
247
259
|
return nil unless maxp && head
|
|
248
260
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
source_format = head.index_to_loc_format
|
|
262
|
+
|
|
263
|
+
# Pick the most compact loca format the canonical glyf fits in.
|
|
264
|
+
# The canonical glyf adds ≤ 3 padding bytes per glyph; estimate
|
|
265
|
+
# the upper bound so the format choice is safe. fontTools'
|
|
266
|
+
# encoder does the same — short loca halves the loca table size
|
|
267
|
+
# and Chrome's OTS accepts both formats.
|
|
268
|
+
max_canonical_glyf_size = glyf_data.bytesize + 3 * maxp.num_glyphs
|
|
269
|
+
target_format = if max_canonical_glyf_size <= SHORT_LOCA_GLYF_MAX
|
|
270
|
+
0
|
|
271
|
+
else
|
|
272
|
+
source_format
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
canonical = Woff2::GlyfCanonicalizer.new(
|
|
252
276
|
glyf_data:,
|
|
253
277
|
loca_data:,
|
|
254
278
|
num_glyphs: maxp.num_glyphs,
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
source_format:,
|
|
280
|
+
target_format:,
|
|
281
|
+
).canonical
|
|
282
|
+
canonical_glyf = canonical[:glyf]
|
|
283
|
+
canonical_loca = canonical[:loca]
|
|
257
284
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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,
|
|
285
|
+
transformed = Woff2::GlyfLocaTransform.new(
|
|
286
|
+
glyf_data: canonical_glyf,
|
|
287
|
+
loca_data: canonical_loca,
|
|
266
288
|
num_glyphs: maxp.num_glyphs,
|
|
267
|
-
index_format
|
|
268
|
-
).
|
|
289
|
+
index_format: target_format,
|
|
290
|
+
).transform
|
|
269
291
|
|
|
270
|
-
|
|
271
|
-
|
|
292
|
+
if target_format != source_format
|
|
293
|
+
Woff2::EncoderRules.set_head_index_to_loc_format!(
|
|
294
|
+
table_data, target_format
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
glyf_orig_length = canonical_glyf.bytesize
|
|
299
|
+
loca_orig_length = canonical_loca.bytesize
|
|
300
|
+
table_data["glyf"] = canonical_glyf
|
|
272
301
|
table_data.delete("loca")
|
|
273
302
|
|
|
274
|
-
{
|
|
303
|
+
{
|
|
304
|
+
transformed_glyf: transformed,
|
|
305
|
+
reconstructed_glyf: canonical_glyf,
|
|
306
|
+
reconstructed_loca: canonical_loca,
|
|
307
|
+
glyf_orig_length:,
|
|
308
|
+
loca_orig_length:,
|
|
309
|
+
}
|
|
275
310
|
end
|
|
276
311
|
|
|
277
312
|
def get_table_data(font, tag)
|
|
@@ -396,6 +431,45 @@ module Fontisan
|
|
|
396
431
|
size
|
|
397
432
|
end
|
|
398
433
|
|
|
434
|
+
# Recompute head.checksumAdjustment for the SFNT the WOFF2 decoder
|
|
435
|
+
# will reconstruct. The decoder assembles glyf/loca from the
|
|
436
|
+
# transformed stream and uses the source bytes for every other
|
|
437
|
+
# table; the resulting SFNT's whole-file uint32 sum must equal
|
|
438
|
+
# 0xB1B0AFBA once checksumAdjustment is applied. Source's value is
|
|
439
|
+
# stale after head.flags/glyf/indexToLocFormat edits and Chrome's
|
|
440
|
+
# OTS rejects files that fail the checksum check.
|
|
441
|
+
#
|
|
442
|
+
# Mutates `table_data["head"]` in place via the Head BinData model.
|
|
443
|
+
def update_checksum_adjustment!(table_data, flavor:, glyf_transform:)
|
|
444
|
+
return unless table_data.key?("head")
|
|
445
|
+
|
|
446
|
+
# The synthetic loca entry was removed from table_data by
|
|
447
|
+
# apply_glyf_loca_transform!, but it's still part of the
|
|
448
|
+
# reconstructed SFNT, so it must be included in the checksum.
|
|
449
|
+
tags = table_data.keys.sort
|
|
450
|
+
if glyf_transform && !tags.include?("loca")
|
|
451
|
+
loca_idx = tags.index { |t| t > "loca" } || tags.length
|
|
452
|
+
tags.insert(loca_idx, "loca")
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
tables = tags.map do |tag|
|
|
456
|
+
bytes = if tag == "glyf" && glyf_transform
|
|
457
|
+
glyf_transform[:reconstructed_glyf]
|
|
458
|
+
elsif tag == "loca" && glyf_transform
|
|
459
|
+
glyf_transform[:reconstructed_loca]
|
|
460
|
+
else
|
|
461
|
+
table_data[tag]
|
|
462
|
+
end
|
|
463
|
+
Woff2::SfntChecksum::Table.new(tag:, bytes:)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
adjustment = Woff2::SfntChecksum.new(flavor:, tables:).adjustment
|
|
467
|
+
|
|
468
|
+
head = Tables::Head.read(table_data["head"])
|
|
469
|
+
head.checksum_adjustment = adjustment
|
|
470
|
+
table_data["head"] = head.to_binary_s
|
|
471
|
+
end
|
|
472
|
+
|
|
399
473
|
def build_header(flavor:, num_tables:, total_sfnt_size:,
|
|
400
474
|
total_compressed_size:)
|
|
401
475
|
header = Woff2::Woff2Header.new
|
|
@@ -48,9 +48,7 @@ module Fontisan
|
|
|
48
48
|
validate_data!(data)
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
|
-
|
|
52
|
-
# The brotli gem doesn't expose mode constants, only quality
|
|
53
|
-
Brotli.deflate(data, quality: quality)
|
|
51
|
+
Brotli.deflate(data, quality: quality, mode: mode)
|
|
54
52
|
rescue StandardError => e
|
|
55
53
|
raise Fontisan::Error,
|
|
56
54
|
"Brotli compression failed: #{e.message}"
|
|
@@ -140,19 +138,6 @@ module Fontisan
|
|
|
140
138
|
"Data must be a String-like object, got #{data.class}"
|
|
141
139
|
end
|
|
142
140
|
end
|
|
143
|
-
|
|
144
|
-
# Convert mode symbol to Brotli constant
|
|
145
|
-
#
|
|
146
|
-
# NOTE: The brotli gem doesn't expose mode constants
|
|
147
|
-
# This method is kept for API compatibility but unused
|
|
148
|
-
#
|
|
149
|
-
# @param mode [Symbol] Mode symbol
|
|
150
|
-
# @return [Integer] Mode value (unused)
|
|
151
|
-
def brotli_mode(_mode)
|
|
152
|
-
# The brotli gem only accepts quality parameter
|
|
153
|
-
# Mode is not configurable in current version
|
|
154
|
-
0
|
|
155
|
-
end
|
|
156
141
|
end
|
|
157
142
|
end
|
|
158
143
|
end
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Fontisan
|
|
|
33
33
|
def self.apply!(table_data)
|
|
34
34
|
exclude_tables!(table_data)
|
|
35
35
|
mark_lossless_modifying!(table_data)
|
|
36
|
+
touch_head_modified!(table_data)
|
|
36
37
|
table_data
|
|
37
38
|
end
|
|
38
39
|
|
|
@@ -61,6 +62,44 @@ module Fontisan
|
|
|
61
62
|
head.flags |= Tables::Head::FLAG_LOSSLESS_MODIFYING
|
|
62
63
|
table_data["head"] = head.to_binary_s
|
|
63
64
|
end
|
|
65
|
+
|
|
66
|
+
# Set head.indexToLocFormat to match the chosen output loca format.
|
|
67
|
+
#
|
|
68
|
+
# fontTools' WOFF2 encoder re-picks the loca format (preferring short
|
|
69
|
+
# when glyf fits). The reconstructed SFNT must carry the matching
|
|
70
|
+
# indexToLocFormat in head, or Chrome's OTS rejects the file with
|
|
71
|
+
# "Failed to convert WOFF 2.0 font to SFNT".
|
|
72
|
+
#
|
|
73
|
+
# @param table_data [Hash{String => String}]
|
|
74
|
+
# @param format_code [Integer] 0 (short) or 1 (long)
|
|
75
|
+
def self.set_head_index_to_loc_format!(table_data, format_code)
|
|
76
|
+
return unless table_data.key?("head")
|
|
77
|
+
|
|
78
|
+
head = Tables::Head.read(table_data["head"])
|
|
79
|
+
head.index_to_loc_format = format_code
|
|
80
|
+
table_data["head"] = head.to_binary_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Bump head.modified past head.created.
|
|
84
|
+
#
|
|
85
|
+
# Chrome's OTS rejects fonts whose modified timestamp is not strictly
|
|
86
|
+
# greater than created (source fonts often have modified == created,
|
|
87
|
+
# which fails this check). Setting modified = created + 1 is the
|
|
88
|
+
# minimal change that satisfies the constraint.
|
|
89
|
+
#
|
|
90
|
+
# Must be called BEFORE checksum recompute so the modified bytes are
|
|
91
|
+
# included in the checksum.
|
|
92
|
+
#
|
|
93
|
+
# @param table_data [Hash{String => String}]
|
|
94
|
+
def self.touch_head_modified!(table_data)
|
|
95
|
+
return unless table_data.key?("head")
|
|
96
|
+
|
|
97
|
+
head = Tables::Head.read(table_data["head"])
|
|
98
|
+
return if head.modified_raw.to_i > head.created_raw.to_i
|
|
99
|
+
|
|
100
|
+
head.modified_raw = head.created_raw + 1
|
|
101
|
+
table_data["head"] = head.to_binary_s
|
|
102
|
+
end
|
|
64
103
|
end
|
|
65
104
|
end
|
|
66
105
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Canonicalizes a source glyf table by padding each glyph to a 4-byte
|
|
6
|
+
# boundary, matching fontTools' WOFF2 encoder behavior.
|
|
7
|
+
#
|
|
8
|
+
# fontTools' WOFF2 writer calls `_normaliseGlyfAndLoca(padding=4)` before
|
|
9
|
+
# transforming the glyf table (see Lib/fontTools/ttLib/woff2.py). Each
|
|
10
|
+
# glyph is recompiled with `recalcBBoxes=False`, which makes
|
|
11
|
+
# `Glyph#compile` return the source glyph bytes verbatim, then the
|
|
12
|
+
# glyf-table-level compile pads each glyph to the configured alignment.
|
|
13
|
+
# The result is a glyf whose bytes match the source byte-for-byte
|
|
14
|
+
# except for added padding (≤ 3 bytes per glyph).
|
|
15
|
+
#
|
|
16
|
+
# Chrome's OpenType Sanitizer (OTS) fails to reconstruct "unpadded"
|
|
17
|
+
# glyf tables — see the long comment in WOFF2Writer.close():
|
|
18
|
+
# https://github.com/google/woff2/issues/15
|
|
19
|
+
# https://github.com/khaledhosny/ots/issues/60
|
|
20
|
+
#
|
|
21
|
+
# The canonical glyf is the bytes the WOFF2 transform encodes, the
|
|
22
|
+
# bytes the decoder reconstructs, the bytes `glyf.origLength` reports,
|
|
23
|
+
# and the bytes the SFNT checksum is computed over. All four must
|
|
24
|
+
# agree.
|
|
25
|
+
class GlyfCanonicalizer
|
|
26
|
+
# @param glyf_data [String] Source glyf table bytes
|
|
27
|
+
# @param loca_data [String] Source loca table bytes
|
|
28
|
+
# @param num_glyphs [Integer] Glyph count from maxp
|
|
29
|
+
# @param source_format [Integer] 0 (short loca) or 1 (long loca) —
|
|
30
|
+
# how to interpret the source loca
|
|
31
|
+
# @param target_format [Integer] 0 (short loca) or 1 (long loca) —
|
|
32
|
+
# how to emit the canonical loca. Defaults to `source_format`.
|
|
33
|
+
def initialize(glyf_data:, loca_data:, num_glyphs:, source_format:,
|
|
34
|
+
target_format: nil)
|
|
35
|
+
@glyf_data = glyf_data
|
|
36
|
+
@loca_data = loca_data
|
|
37
|
+
@num_glyphs = num_glyphs
|
|
38
|
+
@source_format = source_format
|
|
39
|
+
@target_format = target_format || source_format
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Hash{Symbol => String}] `{ glyf:, loca: }` where glyf is
|
|
43
|
+
# padded to 4-byte alignment per glyph and loca is emitted in
|
|
44
|
+
# `target_format`.
|
|
45
|
+
def canonical
|
|
46
|
+
glyf = String.new(encoding: Encoding::BINARY)
|
|
47
|
+
new_offsets = [0]
|
|
48
|
+
|
|
49
|
+
@num_glyphs.times do |i|
|
|
50
|
+
start_offset = source_offsets[i]
|
|
51
|
+
end_offset = source_offsets[i + 1]
|
|
52
|
+
if start_offset < end_offset
|
|
53
|
+
glyf << @glyf_data.byteslice(start_offset, end_offset - start_offset)
|
|
54
|
+
end
|
|
55
|
+
remainder = glyf.bytesize % 4
|
|
56
|
+
glyf << ("\x00" * (4 - remainder)) if remainder.positive?
|
|
57
|
+
new_offsets << glyf.bytesize
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ glyf:, loca: build_loca(new_offsets) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def source_offsets
|
|
66
|
+
@source_offsets ||= begin
|
|
67
|
+
offsets = if @source_format.zero?
|
|
68
|
+
@loca_data.unpack("n*").map { |v| v * 2 }
|
|
69
|
+
else
|
|
70
|
+
@loca_data.unpack("N*")
|
|
71
|
+
end
|
|
72
|
+
if offsets.length != @num_glyphs + 1
|
|
73
|
+
raise InvalidFontError,
|
|
74
|
+
"loca has #{offsets.length} entries, expected #{@num_glyphs + 1}"
|
|
75
|
+
end
|
|
76
|
+
offsets
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_loca(offsets)
|
|
81
|
+
if @target_format.zero?
|
|
82
|
+
offsets.map { |o| o / 2 }.pack("n*")
|
|
83
|
+
else
|
|
84
|
+
offsets.pack("N*")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Computes head.checksumAdjustment for the SFNT that the WOFF2 decoder
|
|
6
|
+
# reconstructs at runtime.
|
|
7
|
+
#
|
|
8
|
+
# Per OpenType spec, head.checksumAdjustment must be set so that the
|
|
9
|
+
# uint32-wise sum of the entire SFNT file (offset table + table
|
|
10
|
+
# directory + all tables) equals the magic 0xB1B0AFBA. Chrome's OTS
|
|
11
|
+
# validates this; a stale value from the source font causes rejection
|
|
12
|
+
# with "Failed to convert WOFF 2.0 font to SFNT" once any table has
|
|
13
|
+
# been modified (head.flags bit 11, glyf/loca transformation, etc.).
|
|
14
|
+
#
|
|
15
|
+
# Reference: OpenType spec, "Calculating Checksums".
|
|
16
|
+
class SfntChecksum
|
|
17
|
+
# Per-table data the checksum is computed over. tag is the 4-byte
|
|
18
|
+
# table tag, bytes is the (already reconstructed, post-transform)
|
|
19
|
+
# table data the decoder will see.
|
|
20
|
+
Table = Struct.new(:tag, :bytes, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
# @param flavor [Integer] sfnt version (0x00010000 for TrueType,
|
|
23
|
+
# 0x4F54544F for CFF)
|
|
24
|
+
# @param tables [Array<Table>] reconstructed tables in alphabetical
|
|
25
|
+
# tag order
|
|
26
|
+
def initialize(flavor:, tables:)
|
|
27
|
+
@flavor = flavor
|
|
28
|
+
@tables = tables
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Compute the head.checksumAdjustment value (uint32).
|
|
32
|
+
#
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def adjustment
|
|
35
|
+
(Constants::CHECKSUM_ADJUSTMENT_MAGIC - total_checksum) & 0xFFFFFFFF
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Whole-SFNT checksum with head.checksumAdjustment treated as 0.
|
|
41
|
+
def total_checksum
|
|
42
|
+
sum = offset_table_checksum
|
|
43
|
+
sum = (sum + directory_checksum) & 0xFFFFFFFF
|
|
44
|
+
@tables.each do |t|
|
|
45
|
+
sum = (sum + per_table_checksum(t)) & 0xFFFFFFFF
|
|
46
|
+
end
|
|
47
|
+
sum
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Checksum of the 12-byte offset table (sfnt version + numTables +
|
|
51
|
+
# searchRange + entrySelector + rangeShift).
|
|
52
|
+
def offset_table_checksum
|
|
53
|
+
n = @tables.size
|
|
54
|
+
search_range = (2**Integer(Math.log2(n))) * 16
|
|
55
|
+
entry_selector = Integer(Math.log2(n))
|
|
56
|
+
range_shift = n * 16 - search_range
|
|
57
|
+
bytes = [@flavor, n, search_range, entry_selector, range_shift]
|
|
58
|
+
.pack("I>S>S>S>S>")
|
|
59
|
+
uint32_sum(bytes.ljust(12, "\x00"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Checksum of the table directory (one 16-byte entry per table).
|
|
63
|
+
# Each entry is tag(4) + checksum(4) + offset(4) + length(4).
|
|
64
|
+
def directory_checksum
|
|
65
|
+
offsets = {}
|
|
66
|
+
cursor = 12 + @tables.size * 16
|
|
67
|
+
@tables.each do |t|
|
|
68
|
+
offsets[t.tag] = cursor
|
|
69
|
+
cursor += padded_size(t.bytes.bytesize)
|
|
70
|
+
end
|
|
71
|
+
bytes = String.new(encoding: Encoding::BINARY)
|
|
72
|
+
@tables.each do |t|
|
|
73
|
+
tag_bytes = t.tag.encode(Encoding::BINARY).ljust(4, " ")
|
|
74
|
+
bytes << tag_bytes
|
|
75
|
+
bytes << [per_table_checksum(t)].pack("I>")
|
|
76
|
+
bytes << [offsets[t.tag]].pack("I>")
|
|
77
|
+
bytes << [t.bytes.bytesize].pack("I>")
|
|
78
|
+
end
|
|
79
|
+
uint32_sum(bytes)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# OpenType per-table checksum. The head table's checksumAdjustment
|
|
83
|
+
# field (offset 8-11) is treated as zero per spec. Tables are
|
|
84
|
+
# zero-padded to a 4-byte boundary for checksum, even when their
|
|
85
|
+
# actual length is shorter.
|
|
86
|
+
def per_table_checksum(table)
|
|
87
|
+
bytes = table.bytes.dup.force_encoding(Encoding::BINARY)
|
|
88
|
+
if table.tag == "head"
|
|
89
|
+
bytes = "#{bytes.byteslice(0, 8)}\u0000\u0000\u0000\u0000#{bytes.byteslice(12..)}"
|
|
90
|
+
end
|
|
91
|
+
uint32_sum(pad_to_4(bytes))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def uint32_sum(bytes)
|
|
95
|
+
bytes.unpack("I>*").sum & 0xFFFFFFFF
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def pad_to_4(bytes)
|
|
99
|
+
pad = (-bytes.bytesize) % 4
|
|
100
|
+
pad.zero? ? bytes : bytes + ("\x00" * pad)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def padded_size(size)
|
|
104
|
+
(size + 3) & ~3
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/fontisan/woff2.rb
CHANGED
|
@@ -8,9 +8,11 @@ module Fontisan
|
|
|
8
8
|
autoload :CollectionEncoder, "fontisan/woff2/collection_encoder"
|
|
9
9
|
autoload :Directory, "fontisan/woff2/directory"
|
|
10
10
|
autoload :EncoderRules, "fontisan/woff2/encoder_rules"
|
|
11
|
+
autoload :GlyfCanonicalizer, "fontisan/woff2/glyf_canonicalizer"
|
|
11
12
|
autoload :GlyfLocaReconstruct, "fontisan/woff2/glyf_loca_reconstruct"
|
|
12
13
|
autoload :GlyfLocaTransform, "fontisan/woff2/glyf_loca_transform"
|
|
13
14
|
autoload :HmtxTransformer, "fontisan/woff2/hmtx_transformer"
|
|
15
|
+
autoload :SfntChecksum, "fontisan/woff2/sfnt_checksum"
|
|
14
16
|
autoload :TableTransformer, "fontisan/woff2/table_transformer"
|
|
15
17
|
autoload :TripletCodec, "fontisan/woff2/triplet_codec"
|
|
16
18
|
autoload :UInt255, "fontisan/woff2/uint255"
|
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.18
|
|
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-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -723,10 +723,12 @@ files:
|
|
|
723
723
|
- lib/fontisan/woff2/collection_encoder.rb
|
|
724
724
|
- lib/fontisan/woff2/directory.rb
|
|
725
725
|
- lib/fontisan/woff2/encoder_rules.rb
|
|
726
|
+
- lib/fontisan/woff2/glyf_canonicalizer.rb
|
|
726
727
|
- lib/fontisan/woff2/glyf_loca_reconstruct.rb
|
|
727
728
|
- lib/fontisan/woff2/glyf_loca_transform.rb
|
|
728
729
|
- lib/fontisan/woff2/header.rb
|
|
729
730
|
- lib/fontisan/woff2/hmtx_transformer.rb
|
|
731
|
+
- lib/fontisan/woff2/sfnt_checksum.rb
|
|
730
732
|
- lib/fontisan/woff2/table_transformer.rb
|
|
731
733
|
- lib/fontisan/woff2/triplet_codec.rb
|
|
732
734
|
- lib/fontisan/woff2/uint255.rb
|