fontisan 0.4.47 → 0.4.48
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/BENCHMARKS.md +56 -0
- data/CHANGELOG.md +12 -0
- data/Cargo.lock +550 -0
- data/Cargo.toml +12 -0
- data/Gemfile +10 -0
- data/README.adoc +52 -0
- data/Rakefile +5 -1
- data/benchmark/baseline.json +5 -0
- data/benchmark/ci_gate.rb +217 -0
- data/benchmark/corpus/harness.rb +175 -0
- data/benchmark/corpus/ops.rb +150 -0
- data/benchmark/corpus/run.rb +182 -0
- data/benchmark/corpus/scan_child.rb +8 -0
- data/benchmark/corpus/tiers.rb +176 -0
- data/benchmark/tier_baseline.json +12 -0
- data/docs/.vitepress/config.ts +2 -1
- data/docs/guide/engines.md +117 -0
- data/docs/reference-facade.md +130 -0
- data/ext/fontisan_cdylib/Cargo.lock +313 -0
- data/ext/fontisan_cdylib/Cargo.toml +18 -0
- data/ext/fontisan_cdylib/build.rs +22 -0
- data/ext/fontisan_cdylib/extconf.rb +36 -0
- data/ext/fontisan_cdylib/src/lib.rs +13 -0
- data/ext/fontisan_native/Cargo.toml +21 -0
- data/ext/fontisan_native/build.rs +22 -0
- data/ext/fontisan_native/clippy.toml +10 -0
- data/ext/fontisan_native/extconf.rb +59 -0
- data/ext/fontisan_native/src/lib.rs +463 -0
- data/lib/fontisan/audit/checks/cmap_check.rb +7 -0
- data/lib/fontisan/audit/checks/hinting_check.rb +1 -1
- data/lib/fontisan/audit/checks/maxp_check.rb +1 -1
- data/lib/fontisan/audit/checks/opentype_conformance_check.rb +5 -2
- data/lib/fontisan/base_collection.rb +30 -0
- data/lib/fontisan/byte_ops.rb +219 -0
- data/lib/fontisan/cli.rb +5 -5
- data/lib/fontisan/commands/audit_command.rb +38 -43
- data/lib/fontisan/commands/convert_command.rb +53 -0
- data/lib/fontisan/commands/instance_command.rb +75 -0
- data/lib/fontisan/commands/subset_command.rb +43 -0
- data/lib/fontisan/commands/unpack_command.rb +56 -0
- data/lib/fontisan/converters/cff_table_builder.rb +1 -1
- data/lib/fontisan/converters/collection_converter.rb +13 -5
- data/lib/fontisan/converters/format_converter.rb +1 -1
- data/lib/fontisan/converters/woff2_encoder.rb +8 -6
- data/lib/fontisan/converters/woff_writer.rb +6 -5
- data/lib/fontisan/engine.rb +350 -0
- data/lib/fontisan/ffi/client.rb +121 -0
- data/lib/fontisan/ffi.rb +343 -0
- data/lib/fontisan/ffi_platform.rb +24 -0
- data/lib/fontisan/font_loader.rb +5 -1
- data/lib/fontisan/font_writer.rb +13 -1
- data/lib/fontisan/native.rb +231 -0
- data/lib/fontisan/reference/table_digests.rb +251 -0
- data/lib/fontisan/reference.rb +619 -0
- data/lib/fontisan/scan.rb +372 -0
- data/lib/fontisan/sfnt_directory.rb +41 -0
- data/lib/fontisan/sfnt_font.rb +16 -6
- data/lib/fontisan/subset/builder.rb +2 -1
- data/lib/fontisan/tables/cff/charstring.rb +16 -0
- data/lib/fontisan/tables/cff.rb +15 -5
- data/lib/fontisan/tables/cmap.rb +11 -1
- data/lib/fontisan/tables/gpos.rb +7 -0
- data/lib/fontisan/tables/gsub.rb +7 -0
- data/lib/fontisan/tables/layout_common.rb +172 -0
- data/lib/fontisan/tables/name.rb +10 -4
- data/lib/fontisan/utilities/brotli_wrapper.rb +39 -4
- data/lib/fontisan/validators/opentype_validator.rb +2 -0
- data/lib/fontisan/variable/instancer.rb +1 -0
- data/lib/fontisan/variable/static_font_builder.rb +4 -1
- data/lib/fontisan/variable/table_updater.rb +4 -2
- data/lib/fontisan/variation/instancing/true_type_instancer.rb +14 -6
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +15 -1
- data/lib/fontisan/woff2/collection_encoder.rb +29 -6
- data/lib/fontisan/woff2/encoder_rules.rb +12 -4
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +1 -1
- data/lib/fontisan/woff2_collection.rb +7 -5
- data/lib/fontisan/woff2_font.rb +12 -3
- data/lib/fontisan/woff_container.rb +83 -0
- data/lib/fontisan/woff_font.rb +24 -20
- data/lib/fontisan.rb +24 -1
- data/rakelib/bench.rake +9 -0
- data/rakelib/cdylib.rake +39 -0
- data/rakelib/corpus.rake +272 -0
- data/rakelib/corpus_convert.rake +43 -0
- data/rakelib/corpus_goldens.rake +22 -0
- data/rakelib/corpus_malformed.rake +20 -0
- data/rakelib/corpus_scan.rake +24 -0
- data/rakelib/java.rake +61 -0
- data/rakelib/native.rake +43 -0
- data/scripts/build-woff2.sh +57 -0
- data/scripts/corpus/generate_convert_goldens.rb +478 -0
- data/scripts/corpus/generate_goldens.rb +282 -0
- data/scripts/corpus/generate_malformed_goldens.rb +254 -0
- data/scripts/corpus/generate_scan_goldens.rb +98 -0
- data/scripts/tier_fallback.rb +219 -0
- data/scripts/tier_smoke.rb +64 -0
- metadata +68 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Reference
|
|
7
|
+
# Canonical digests of parsed table content, computed from the raw table
|
|
8
|
+
# bytes with the OpenType layouts written out here (not through the
|
|
9
|
+
# table classes), so another implementation can reproduce them from the
|
|
10
|
+
# same bytes. Every function returns JSON-ready data with String keys;
|
|
11
|
+
# Fixed 16.16 values are raw integers. A structure that runs past the
|
|
12
|
+
# end of its table raises {Truncated}.
|
|
13
|
+
#
|
|
14
|
+
# See docs/reference-facade.md ("Parsed table digests").
|
|
15
|
+
module TableDigests
|
|
16
|
+
class Truncated < StandardError; end
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# SHA-256 over the concatenation of the 32-byte digests of each part.
|
|
21
|
+
def digest_of_parts(parts)
|
|
22
|
+
Digest::SHA256.hexdigest(parts.map { |p| Digest::SHA256.digest(p) }.join)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sha(bytes)
|
|
26
|
+
Digest::SHA256.hexdigest(bytes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def slice(data, offset, length)
|
|
30
|
+
raise Truncated, "#{length} bytes at #{offset} past #{data.bytesize}" if
|
|
31
|
+
offset.negative? || length.negative? || offset + length > data.bytesize
|
|
32
|
+
|
|
33
|
+
data.byteslice(offset, length)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def u8(data, off) = slice(data, off, 1).unpack1("C")
|
|
37
|
+
def u16(data, off) = slice(data, off, 2).unpack1("n")
|
|
38
|
+
def u32(data, off) = slice(data, off, 4).unpack1("N")
|
|
39
|
+
def i32(data, off) = slice(data, off, 4).unpack1("l>")
|
|
40
|
+
|
|
41
|
+
# gvar: header fields, the shared tuples, and one digest over every
|
|
42
|
+
# glyph's GlyphVariationData byte range (located by the offset array).
|
|
43
|
+
def gvar(data)
|
|
44
|
+
axis_count = u16(data, 4)
|
|
45
|
+
shared_count = u16(data, 6)
|
|
46
|
+
shared_offset = u32(data, 8)
|
|
47
|
+
glyph_count = u16(data, 12)
|
|
48
|
+
flags = u16(data, 14)
|
|
49
|
+
array_offset = u32(data, 16)
|
|
50
|
+
long = flags.anybits?(1)
|
|
51
|
+
step = long ? 4 : 2
|
|
52
|
+
offsets = Array.new(glyph_count + 1) do |i|
|
|
53
|
+
value = long ? u32(data, 20 + (i * step)) : u16(data, 20 + (i * step)) * 2
|
|
54
|
+
array_offset + value
|
|
55
|
+
end
|
|
56
|
+
parts = offsets.each_cons(2).map { |a, b| slice(data, a, b - a) }
|
|
57
|
+
{
|
|
58
|
+
"axis_count" => axis_count,
|
|
59
|
+
"flags" => flags,
|
|
60
|
+
"glyph_count" => glyph_count,
|
|
61
|
+
"glyphs_with_data" => parts.count { |p| !p.empty? },
|
|
62
|
+
"glyphs_digest" => digest_of_parts(parts),
|
|
63
|
+
"shared_tuple_count" => shared_count,
|
|
64
|
+
"shared_tuples_sha256" => sha(slice(data, shared_offset, shared_count * axis_count * 2)),
|
|
65
|
+
"version" => [u16(data, 0), u16(data, 2)],
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# HVAR / VVAR share a layout: header, ItemVariationStore, and up to
|
|
70
|
+
# three DeltaSetIndexMaps.
|
|
71
|
+
def hvar(data)
|
|
72
|
+
maps = { "advance_mapping" => 8, "lsb_mapping" => 12, "rsb_mapping" => 16 }
|
|
73
|
+
result = { "version" => [u16(data, 0), u16(data, 2)] }
|
|
74
|
+
result["item_variation_store"] = item_variation_store(data, u32(data, 4))
|
|
75
|
+
maps.each do |name, at|
|
|
76
|
+
offset = u32(data, at)
|
|
77
|
+
result[name] = offset.zero? ? nil : delta_set_index_map(data, offset)
|
|
78
|
+
end
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# MVAR: value records (tag, outer, inner) and the ItemVariationStore
|
|
83
|
+
# (Offset16 from the table start).
|
|
84
|
+
def mvar(data)
|
|
85
|
+
record_size = u16(data, 6)
|
|
86
|
+
count = u16(data, 8)
|
|
87
|
+
store_offset = u16(data, 10)
|
|
88
|
+
{
|
|
89
|
+
"version" => [u16(data, 0), u16(data, 2)],
|
|
90
|
+
"value_records" => Array.new(count) do |i|
|
|
91
|
+
at = 12 + (i * record_size)
|
|
92
|
+
{ "tag" => slice(data, at, 4), "outer" => u16(data, at + 4), "inner" => u16(data, at + 6) }
|
|
93
|
+
end,
|
|
94
|
+
"item_variation_store" => store_offset.zero? ? nil : item_variation_store(data, store_offset),
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Exact byte ranges: header+offsets, region list, each
|
|
99
|
+
# ItemVariationData. Digests are per range, then one over all.
|
|
100
|
+
def item_variation_store(data, base)
|
|
101
|
+
region_offset = u32(data, base + 2)
|
|
102
|
+
data_count = u16(data, base + 6)
|
|
103
|
+
region_axes = u16(data, base + region_offset)
|
|
104
|
+
region_count = u16(data, base + region_offset + 2)
|
|
105
|
+
region_len = 4 + (region_count * region_axes * 6)
|
|
106
|
+
subtables = Array.new(data_count) do |i|
|
|
107
|
+
start = base + u32(data, base + 8 + (i * 4))
|
|
108
|
+
items = u16(data, start)
|
|
109
|
+
words = u16(data, start + 2)
|
|
110
|
+
indexes = u16(data, start + 4)
|
|
111
|
+
long = words.anybits?(0x8000)
|
|
112
|
+
word_count = words & 0x7FFF
|
|
113
|
+
row = long ? (word_count * 4) + ((indexes - word_count) * 2) : word_count + indexes
|
|
114
|
+
slice(data, start, 6 + (indexes * 2) + (items * row))
|
|
115
|
+
end
|
|
116
|
+
{
|
|
117
|
+
"region_count" => region_count,
|
|
118
|
+
"item_data_count" => data_count,
|
|
119
|
+
"regions_sha256" => sha(slice(data, base + region_offset, region_len)),
|
|
120
|
+
"data_digest" => digest_of_parts(subtables),
|
|
121
|
+
"data_sha256" => subtables.map { |s| sha(s) },
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# DeltaSetIndexMap: format 0 (u16 count) or 1 (u32 count).
|
|
126
|
+
def delta_set_index_map(data, offset)
|
|
127
|
+
format = u8(data, offset)
|
|
128
|
+
entry_format = u8(data, offset + 1)
|
|
129
|
+
count = format.zero? ? u16(data, offset + 2) : u32(data, offset + 2)
|
|
130
|
+
header = format.zero? ? 4 : 6
|
|
131
|
+
size = ((entry_format >> 4) & 3) + 1
|
|
132
|
+
{
|
|
133
|
+
"format" => format,
|
|
134
|
+
"entry_format" => entry_format,
|
|
135
|
+
"map_count" => count,
|
|
136
|
+
"sha256" => sha(slice(data, offset, header + (count * size))),
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# STAT: design axes and every axis value record, Fixed as raw ints.
|
|
141
|
+
def stat(data)
|
|
142
|
+
axis_size = u16(data, 4)
|
|
143
|
+
axis_count = u16(data, 6)
|
|
144
|
+
axes_offset = u32(data, 8)
|
|
145
|
+
value_count = u16(data, 12)
|
|
146
|
+
values_offset = u32(data, 14)
|
|
147
|
+
minor = u16(data, 2)
|
|
148
|
+
{
|
|
149
|
+
"version" => [u16(data, 0), minor],
|
|
150
|
+
"elided_fallback_name_id" => minor >= 1 ? u16(data, 18) : nil,
|
|
151
|
+
"axes" => Array.new(axis_count) do |i|
|
|
152
|
+
at = axes_offset + (i * axis_size)
|
|
153
|
+
{ "tag" => slice(data, at, 4), "name_id" => u16(data, at + 4), "ordering" => u16(data, at + 6) }
|
|
154
|
+
end,
|
|
155
|
+
"axis_values" => Array.new(value_count) do |i|
|
|
156
|
+
axis_value(data, values_offset + u16(data, values_offset + (i * 2)))
|
|
157
|
+
end,
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def axis_value(data, pos)
|
|
162
|
+
format = u16(data, pos)
|
|
163
|
+
case format
|
|
164
|
+
when 1
|
|
165
|
+
fixed_value(data, pos, format, 8, %w[value])
|
|
166
|
+
when 2
|
|
167
|
+
fixed_value(data, pos, format, 8, %w[nominal range_min range_max])
|
|
168
|
+
when 3
|
|
169
|
+
fixed_value(data, pos, format, 8, %w[value linked_value])
|
|
170
|
+
when 4
|
|
171
|
+
n = u16(data, pos + 2)
|
|
172
|
+
{ "format" => 4, "flags" => u16(data, pos + 4), "value_name_id" => u16(data, pos + 6),
|
|
173
|
+
"axis_values" => Array.new(n) do |j|
|
|
174
|
+
{ "axis_index" => u16(data, pos + 8 + (j * 6)), "value" => i32(data, pos + 10 + (j * 6)) }
|
|
175
|
+
end }
|
|
176
|
+
else
|
|
177
|
+
{ "format" => format, "unsupported" => true }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def fixed_value(data, pos, format, first, names)
|
|
182
|
+
out = { "format" => format, "axis_index" => u16(data, pos + 2), "flags" => u16(data, pos + 4),
|
|
183
|
+
"value_name_id" => u16(data, pos + 6) }
|
|
184
|
+
names.each_with_index { |n, i| out["#{n}_raw"] = i32(data, pos + first + (i * 4)) }
|
|
185
|
+
out
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# CFF2: CharStrings INDEX located through Top DICT operator 17.
|
|
189
|
+
# Returns the per-glyph charstring byte slices.
|
|
190
|
+
def cff2_charstrings(data)
|
|
191
|
+
header_size = u8(data, 2)
|
|
192
|
+
top_length = u16(data, 3)
|
|
193
|
+
offset = dict_operand(slice(data, header_size, top_length), 17)
|
|
194
|
+
return [] unless offset
|
|
195
|
+
|
|
196
|
+
count = u32(data, offset)
|
|
197
|
+
return [] if count.zero?
|
|
198
|
+
|
|
199
|
+
off_size = u8(data, offset + 4)
|
|
200
|
+
# The offset array must fit in the table; a fuzzed count of 2^32
|
|
201
|
+
# would otherwise build billions of slices (same bound as the Rust
|
|
202
|
+
# engine's INDEX reader).
|
|
203
|
+
raise Truncated, "invalid CFF2 offSize #{off_size}" unless (1..4).cover?(off_size)
|
|
204
|
+
|
|
205
|
+
slice(data, offset + 5, (count + 1) * off_size)
|
|
206
|
+
read = ->(i) { slice(data, offset + 5 + (i * off_size), off_size).bytes.inject(0) { |a, b| (a << 8) | b } }
|
|
207
|
+
base = offset + 5 + ((count + 1) * off_size) - 1
|
|
208
|
+
Array.new(count) { |i| slice(data, base + read.call(i), read.call(i + 1) - read.call(i)) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The last operand before `operator` in a DICT, or nil. Only the
|
|
212
|
+
# operand encodings a CharStrings offset can use are decoded; real
|
|
213
|
+
# numbers are skipped (pushed as 0).
|
|
214
|
+
def dict_operand(dict, operator)
|
|
215
|
+
stack = []
|
|
216
|
+
i = 0
|
|
217
|
+
while i < dict.bytesize
|
|
218
|
+
lead = dict.getbyte(i)
|
|
219
|
+
if lead <= 21
|
|
220
|
+
op = lead == 12 ? (i += 1) && (1200 + dict.getbyte(i)) : lead
|
|
221
|
+
return stack.last if op == operator
|
|
222
|
+
|
|
223
|
+
stack.clear
|
|
224
|
+
else
|
|
225
|
+
value, i = dict_number(dict, i, lead)
|
|
226
|
+
stack << value
|
|
227
|
+
end
|
|
228
|
+
i += 1
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Decode the operand starting at `i`; returns [value, index of its
|
|
234
|
+
# last byte].
|
|
235
|
+
def dict_number(dict, idx, lead)
|
|
236
|
+
case lead
|
|
237
|
+
when 28 then [dict.byteslice(idx + 1, 2).unpack1("s>"), idx + 2]
|
|
238
|
+
when 29 then [dict.byteslice(idx + 1, 4).unpack1("l>"), idx + 4]
|
|
239
|
+
when 30
|
|
240
|
+
idx += 1
|
|
241
|
+
idx += 1 until idx >= dict.bytesize || dict.getbyte(idx) >> 4 == 0xF || dict.getbyte(idx) & 0xF == 0xF
|
|
242
|
+
[0, idx]
|
|
243
|
+
when 32..246 then [lead - 139, idx]
|
|
244
|
+
when 247..250 then [((lead - 247) * 256) + dict.getbyte(idx + 1) + 108, idx + 1]
|
|
245
|
+
when 251..254 then [-((lead - 251) * 256) - dict.getbyte(idx + 1) - 108, idx + 1]
|
|
246
|
+
else [0, idx]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|