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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Validation
|
|
5
|
+
# Woff2HeaderValidator validates the WOFF2 header structure
|
|
6
|
+
#
|
|
7
|
+
# This validator checks the WOFF2 header for:
|
|
8
|
+
# - Valid signature (0x774F4632 'wOF2')
|
|
9
|
+
# - Valid flavor (TrueType or CFF)
|
|
10
|
+
# - Reserved field is zero
|
|
11
|
+
# - File length consistency
|
|
12
|
+
# - Valid table count
|
|
13
|
+
# - Valid compressed size
|
|
14
|
+
# - Metadata offset/length consistency
|
|
15
|
+
# - Private data offset/length consistency
|
|
16
|
+
#
|
|
17
|
+
# Single Responsibility: WOFF2 header validation
|
|
18
|
+
#
|
|
19
|
+
# @example Validating a WOFF2 header
|
|
20
|
+
# validator = Woff2HeaderValidator.new(rules)
|
|
21
|
+
# issues = validator.validate(woff2_font)
|
|
22
|
+
class Woff2HeaderValidator
|
|
23
|
+
# Valid WOFF2 flavors
|
|
24
|
+
VALID_FLAVORS = [
|
|
25
|
+
0x00010000, # TrueType
|
|
26
|
+
0x74727565, # 'true' (TrueType)
|
|
27
|
+
0x4F54544F, # 'OTTO' (CFF/OpenType)
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# Initialize WOFF2 header validator
|
|
31
|
+
#
|
|
32
|
+
# @param rules [Hash] Validation rules configuration
|
|
33
|
+
def initialize(rules)
|
|
34
|
+
@rules = rules
|
|
35
|
+
@woff2_config = rules["woff2_validation"] || {}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Validate WOFF2 header
|
|
39
|
+
#
|
|
40
|
+
# @param woff2_font [Woff2Font] The WOFF2 font to validate
|
|
41
|
+
# @return [Array<Hash>] Array of validation issues
|
|
42
|
+
def validate(woff2_font)
|
|
43
|
+
issues = []
|
|
44
|
+
|
|
45
|
+
header = woff2_font.header
|
|
46
|
+
return issues unless header
|
|
47
|
+
|
|
48
|
+
# Check signature
|
|
49
|
+
issues.concat(check_signature(header))
|
|
50
|
+
|
|
51
|
+
# Check flavor
|
|
52
|
+
issues.concat(check_flavor(header))
|
|
53
|
+
|
|
54
|
+
# Check reserved field
|
|
55
|
+
issues.concat(check_reserved_field(header))
|
|
56
|
+
|
|
57
|
+
# Check table count
|
|
58
|
+
issues.concat(check_table_count(header, woff2_font))
|
|
59
|
+
|
|
60
|
+
# Check compressed size
|
|
61
|
+
issues.concat(check_compressed_size(header))
|
|
62
|
+
|
|
63
|
+
# Check metadata consistency
|
|
64
|
+
issues.concat(check_metadata(header))
|
|
65
|
+
|
|
66
|
+
# Check private data consistency
|
|
67
|
+
issues.concat(check_private_data(header))
|
|
68
|
+
|
|
69
|
+
issues
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Check signature validity
|
|
75
|
+
#
|
|
76
|
+
# @param header [Woff2::Woff2Header] The header
|
|
77
|
+
# @return [Array<Hash>] Array of signature issues
|
|
78
|
+
def check_signature(header)
|
|
79
|
+
issues = []
|
|
80
|
+
|
|
81
|
+
unless header.signature == Woff2::Woff2Header::SIGNATURE
|
|
82
|
+
issues << {
|
|
83
|
+
severity: "error",
|
|
84
|
+
category: "woff2_header",
|
|
85
|
+
message: "Invalid WOFF2 signature: expected 0x#{Woff2::Woff2Header::SIGNATURE.to_s(16)}, " \
|
|
86
|
+
"got 0x#{header.signature.to_s(16)}",
|
|
87
|
+
location: "header",
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
issues
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check flavor validity
|
|
95
|
+
#
|
|
96
|
+
# @param header [Woff2::Woff2Header] The header
|
|
97
|
+
# @return [Array<Hash>] Array of flavor issues
|
|
98
|
+
def check_flavor(header)
|
|
99
|
+
issues = []
|
|
100
|
+
|
|
101
|
+
unless VALID_FLAVORS.include?(header.flavor)
|
|
102
|
+
issues << {
|
|
103
|
+
severity: "error",
|
|
104
|
+
category: "woff2_header",
|
|
105
|
+
message: "Invalid WOFF2 flavor: 0x#{header.flavor.to_s(16)} " \
|
|
106
|
+
"(expected TrueType 0x00010000 or CFF 0x4F54544F)",
|
|
107
|
+
location: "header",
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
issues
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check reserved field
|
|
115
|
+
#
|
|
116
|
+
# @param header [Woff2::Woff2Header] The header
|
|
117
|
+
# @return [Array<Hash>] Array of reserved field issues
|
|
118
|
+
def check_reserved_field(header)
|
|
119
|
+
issues = []
|
|
120
|
+
|
|
121
|
+
if header.reserved != 0
|
|
122
|
+
issues << {
|
|
123
|
+
severity: "warning",
|
|
124
|
+
category: "woff2_header",
|
|
125
|
+
message: "Reserved field should be 0, got #{header.reserved}",
|
|
126
|
+
location: "header",
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
issues
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check table count validity
|
|
134
|
+
#
|
|
135
|
+
# @param header [Woff2::Woff2Header] The header
|
|
136
|
+
# @param woff2_font [Woff2Font] The WOFF2 font
|
|
137
|
+
# @return [Array<Hash>] Array of table count issues
|
|
138
|
+
def check_table_count(header, woff2_font)
|
|
139
|
+
issues = []
|
|
140
|
+
|
|
141
|
+
if header.num_tables.zero?
|
|
142
|
+
issues << {
|
|
143
|
+
severity: "error",
|
|
144
|
+
category: "woff2_header",
|
|
145
|
+
message: "Number of tables cannot be zero",
|
|
146
|
+
location: "header",
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check consistency with actual table entries
|
|
151
|
+
actual_count = woff2_font.table_entries.length
|
|
152
|
+
if header.num_tables != actual_count
|
|
153
|
+
issues << {
|
|
154
|
+
severity: "error",
|
|
155
|
+
category: "woff2_header",
|
|
156
|
+
message: "Table count mismatch: header=#{header.num_tables}, actual=#{actual_count}",
|
|
157
|
+
location: "header",
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
issues
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check compressed size validity
|
|
165
|
+
#
|
|
166
|
+
# @param header [Woff2::Woff2Header] The header
|
|
167
|
+
# @return [Array<Hash>] Array of compressed size issues
|
|
168
|
+
def check_compressed_size(header)
|
|
169
|
+
issues = []
|
|
170
|
+
|
|
171
|
+
if header.total_compressed_size.zero?
|
|
172
|
+
issues << {
|
|
173
|
+
severity: "error",
|
|
174
|
+
category: "woff2_header",
|
|
175
|
+
message: "Total compressed size cannot be zero",
|
|
176
|
+
location: "header",
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check compression ratio
|
|
181
|
+
if header.total_sfnt_size.positive? && header.total_compressed_size.positive?
|
|
182
|
+
ratio = header.total_compressed_size.to_f / header.total_sfnt_size
|
|
183
|
+
min_ratio = @woff2_config["min_compression_ratio"] || 0.2
|
|
184
|
+
max_ratio = @woff2_config["max_compression_ratio"] || 0.95
|
|
185
|
+
|
|
186
|
+
if ratio < min_ratio
|
|
187
|
+
issues << {
|
|
188
|
+
severity: "warning",
|
|
189
|
+
category: "woff2_header",
|
|
190
|
+
message: "Compression ratio (#{(ratio * 100).round(2)}%) is unusually low",
|
|
191
|
+
location: "header",
|
|
192
|
+
}
|
|
193
|
+
elsif ratio > max_ratio
|
|
194
|
+
issues << {
|
|
195
|
+
severity: "warning",
|
|
196
|
+
category: "woff2_header",
|
|
197
|
+
message: "Compression ratio (#{(ratio * 100).round(2)}%) is unusually high",
|
|
198
|
+
location: "header",
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
issues
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Check metadata consistency
|
|
207
|
+
#
|
|
208
|
+
# @param header [Woff2::Woff2Header] The header
|
|
209
|
+
# @return [Array<Hash>] Array of metadata issues
|
|
210
|
+
def check_metadata(header)
|
|
211
|
+
issues = []
|
|
212
|
+
|
|
213
|
+
# If metadata offset is set, length must be positive
|
|
214
|
+
if header.meta_offset.positive? && header.meta_length.zero?
|
|
215
|
+
issues << {
|
|
216
|
+
severity: "warning",
|
|
217
|
+
category: "woff2_header",
|
|
218
|
+
message: "Metadata offset is set but length is zero",
|
|
219
|
+
location: "header",
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# If metadata length is set, offset must be positive
|
|
224
|
+
if header.meta_length.positive? && header.meta_offset.zero?
|
|
225
|
+
issues << {
|
|
226
|
+
severity: "warning",
|
|
227
|
+
category: "woff2_header",
|
|
228
|
+
message: "Metadata length is set but offset is zero",
|
|
229
|
+
location: "header",
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Original length should be >= compressed length
|
|
234
|
+
if header.meta_orig_length.positive? && header.meta_length.positive? && (header.meta_orig_length < header.meta_length)
|
|
235
|
+
issues << {
|
|
236
|
+
severity: "error",
|
|
237
|
+
category: "woff2_header",
|
|
238
|
+
message: "Metadata original length (#{header.meta_orig_length}) " \
|
|
239
|
+
"is less than compressed length (#{header.meta_length})",
|
|
240
|
+
location: "header",
|
|
241
|
+
}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
issues
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Check private data consistency
|
|
248
|
+
#
|
|
249
|
+
# @param header [Woff2::Woff2Header] The header
|
|
250
|
+
# @return [Array<Hash>] Array of private data issues
|
|
251
|
+
def check_private_data(header)
|
|
252
|
+
issues = []
|
|
253
|
+
|
|
254
|
+
# If private offset is set, length must be positive
|
|
255
|
+
if header.priv_offset.positive? && header.priv_length.zero?
|
|
256
|
+
issues << {
|
|
257
|
+
severity: "warning",
|
|
258
|
+
category: "woff2_header",
|
|
259
|
+
message: "Private data offset is set but length is zero",
|
|
260
|
+
location: "header",
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# If private length is set, offset must be positive
|
|
265
|
+
if header.priv_length.positive? && header.priv_offset.zero?
|
|
266
|
+
issues << {
|
|
267
|
+
severity: "warning",
|
|
268
|
+
category: "woff2_header",
|
|
269
|
+
message: "Private data length is set but offset is zero",
|
|
270
|
+
location: "header",
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
issues
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../woff2/directory"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Validation
|
|
7
|
+
# Woff2TableValidator validates WOFF2 table directory entries
|
|
8
|
+
#
|
|
9
|
+
# This validator checks each table entry for:
|
|
10
|
+
# - Valid tag (known or custom)
|
|
11
|
+
# - Valid transformation version
|
|
12
|
+
# - Transform length consistency
|
|
13
|
+
# - Table size validity
|
|
14
|
+
# - Flags byte correctness
|
|
15
|
+
#
|
|
16
|
+
# Single Responsibility: WOFF2 table directory validation
|
|
17
|
+
#
|
|
18
|
+
# @example Validating WOFF2 tables
|
|
19
|
+
# validator = Woff2TableValidator.new(rules)
|
|
20
|
+
# issues = validator.validate(woff2_font)
|
|
21
|
+
class Woff2TableValidator
|
|
22
|
+
# Initialize WOFF2 table validator
|
|
23
|
+
#
|
|
24
|
+
# @param rules [Hash] Validation rules configuration
|
|
25
|
+
def initialize(rules)
|
|
26
|
+
@rules = rules
|
|
27
|
+
@woff2_config = rules["woff2_validation"] || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Validate WOFF2 table directory
|
|
31
|
+
#
|
|
32
|
+
# @param woff2_font [Woff2Font] The WOFF2 font to validate
|
|
33
|
+
# @return [Array<Hash>] Array of validation issues
|
|
34
|
+
def validate(woff2_font)
|
|
35
|
+
issues = []
|
|
36
|
+
|
|
37
|
+
woff2_font.table_entries.each do |entry|
|
|
38
|
+
# Check tag validity
|
|
39
|
+
issues.concat(check_tag(entry))
|
|
40
|
+
|
|
41
|
+
# Check flags byte
|
|
42
|
+
issues.concat(check_flags(entry))
|
|
43
|
+
|
|
44
|
+
# Check transformation version
|
|
45
|
+
issues.concat(check_transformation(entry))
|
|
46
|
+
|
|
47
|
+
# Check table sizes
|
|
48
|
+
issues.concat(check_sizes(entry))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check for duplicate tables
|
|
52
|
+
issues.concat(check_duplicates(woff2_font.table_entries))
|
|
53
|
+
|
|
54
|
+
issues
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Check tag validity
|
|
60
|
+
#
|
|
61
|
+
# @param entry [Woff2TableDirectoryEntry] The table entry
|
|
62
|
+
# @return [Array<Hash>] Array of tag issues
|
|
63
|
+
def check_tag(entry)
|
|
64
|
+
issues = []
|
|
65
|
+
|
|
66
|
+
if entry.tag.nil? || entry.tag.empty?
|
|
67
|
+
issues << {
|
|
68
|
+
severity: "error",
|
|
69
|
+
category: "woff2_tables",
|
|
70
|
+
message: "Table entry has nil or empty tag",
|
|
71
|
+
location: "table directory",
|
|
72
|
+
}
|
|
73
|
+
elsif entry.tag.bytesize != 4
|
|
74
|
+
issues << {
|
|
75
|
+
severity: "error",
|
|
76
|
+
category: "woff2_tables",
|
|
77
|
+
message: "Table tag '#{entry.tag}' must be exactly 4 bytes, got #{entry.tag.bytesize}",
|
|
78
|
+
location: entry.tag,
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
issues
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check flags byte validity
|
|
86
|
+
#
|
|
87
|
+
# @param entry [Woff2TableDirectoryEntry] The table entry
|
|
88
|
+
# @return [Array<Hash>] Array of flags issues
|
|
89
|
+
def check_flags(entry)
|
|
90
|
+
issues = []
|
|
91
|
+
|
|
92
|
+
tag_index = entry.flags & 0x3F
|
|
93
|
+
(entry.flags >> 6) & 0x03
|
|
94
|
+
|
|
95
|
+
# Check tag index consistency
|
|
96
|
+
if tag_index == Woff2::Directory::CUSTOM_TAG_INDEX
|
|
97
|
+
# Custom tag - should not be in known tags
|
|
98
|
+
if Woff2::Directory::KNOWN_TAGS.include?(entry.tag)
|
|
99
|
+
issues << {
|
|
100
|
+
severity: "warning",
|
|
101
|
+
category: "woff2_tables",
|
|
102
|
+
message: "Table '#{entry.tag}' uses custom tag index but is a known tag",
|
|
103
|
+
location: entry.tag,
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
# Known tag - should match index
|
|
108
|
+
expected_tag = Woff2::Directory::KNOWN_TAGS[tag_index]
|
|
109
|
+
if expected_tag && expected_tag != entry.tag
|
|
110
|
+
issues << {
|
|
111
|
+
severity: "error",
|
|
112
|
+
category: "woff2_tables",
|
|
113
|
+
message: "Table tag mismatch: index #{tag_index} should be '#{expected_tag}', got '#{entry.tag}'",
|
|
114
|
+
location: entry.tag,
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
issues
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check transformation version
|
|
123
|
+
#
|
|
124
|
+
# @param entry [Woff2TableDirectoryEntry] The table entry
|
|
125
|
+
# @return [Array<Hash>] Array of transformation issues
|
|
126
|
+
def check_transformation(entry)
|
|
127
|
+
issues = []
|
|
128
|
+
|
|
129
|
+
transform_version = (entry.flags >> 6) & 0x03
|
|
130
|
+
|
|
131
|
+
# Check transformation consistency for transformable tables
|
|
132
|
+
case entry.tag
|
|
133
|
+
when "glyf", "loca"
|
|
134
|
+
# glyf/loca: version 0 = transformed (needs transform_length)
|
|
135
|
+
# version 1-3 = not transformed (no transform_length)
|
|
136
|
+
if transform_version.zero?
|
|
137
|
+
# Should be transformed
|
|
138
|
+
unless entry.transform_length&.positive?
|
|
139
|
+
issues << {
|
|
140
|
+
severity: "error",
|
|
141
|
+
category: "woff2_tables",
|
|
142
|
+
message: "Table '#{entry.tag}' has transform version 0 but no transform_length",
|
|
143
|
+
location: entry.tag,
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
elsif entry.transform_length&.positive?
|
|
147
|
+
# Should not be transformed
|
|
148
|
+
issues << {
|
|
149
|
+
severity: "warning",
|
|
150
|
+
category: "woff2_tables",
|
|
151
|
+
message: "Table '#{entry.tag}' has transform version #{transform_version} but has transform_length",
|
|
152
|
+
location: entry.tag,
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
when "hmtx"
|
|
157
|
+
# hmtx: version 1 = transformed (needs transform_length)
|
|
158
|
+
# version 0, 2, 3 = not transformed (no transform_length)
|
|
159
|
+
if transform_version == 1
|
|
160
|
+
# Should be transformed
|
|
161
|
+
unless entry.transform_length&.positive?
|
|
162
|
+
issues << {
|
|
163
|
+
severity: "error",
|
|
164
|
+
category: "woff2_tables",
|
|
165
|
+
message: "Table '#{entry.tag}' has transform version 1 but no transform_length",
|
|
166
|
+
location: entry.tag,
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
elsif entry.transform_length&.positive?
|
|
170
|
+
# Should not be transformed
|
|
171
|
+
issues << {
|
|
172
|
+
severity: "warning",
|
|
173
|
+
category: "woff2_tables",
|
|
174
|
+
message: "Table '#{entry.tag}' has transform version #{transform_version} but has transform_length",
|
|
175
|
+
location: entry.tag,
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
else
|
|
180
|
+
# Other tables should not be transformed
|
|
181
|
+
if entry.transform_length&.positive?
|
|
182
|
+
issues << {
|
|
183
|
+
severity: "warning",
|
|
184
|
+
category: "woff2_tables",
|
|
185
|
+
message: "Table '#{entry.tag}' is not transformable but has transform_length",
|
|
186
|
+
location: entry.tag,
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
issues
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check table sizes
|
|
195
|
+
#
|
|
196
|
+
# @param entry [Woff2TableDirectoryEntry] The table entry
|
|
197
|
+
# @return [Array<Hash>] Array of size issues
|
|
198
|
+
def check_sizes(entry)
|
|
199
|
+
issues = []
|
|
200
|
+
|
|
201
|
+
# Check orig_length
|
|
202
|
+
if entry.orig_length.nil? || entry.orig_length.zero?
|
|
203
|
+
issues << {
|
|
204
|
+
severity: "error",
|
|
205
|
+
category: "woff2_tables",
|
|
206
|
+
message: "Table '#{entry.tag}' has invalid orig_length: #{entry.orig_length}",
|
|
207
|
+
location: entry.tag,
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Check transform_length if present
|
|
212
|
+
if entry.transform_length
|
|
213
|
+
if entry.transform_length.zero?
|
|
214
|
+
issues << {
|
|
215
|
+
severity: "warning",
|
|
216
|
+
category: "woff2_tables",
|
|
217
|
+
message: "Table '#{entry.tag}' has transform_length of zero",
|
|
218
|
+
location: entry.tag,
|
|
219
|
+
}
|
|
220
|
+
elsif entry.transform_length > entry.orig_length
|
|
221
|
+
issues << {
|
|
222
|
+
severity: "warning",
|
|
223
|
+
category: "woff2_tables",
|
|
224
|
+
message: "Table '#{entry.tag}' has transform_length (#{entry.transform_length}) " \
|
|
225
|
+
"greater than orig_length (#{entry.orig_length})",
|
|
226
|
+
location: entry.tag,
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Check for extremely large tables
|
|
232
|
+
max_table_size = @woff2_config["max_table_size"] || 104_857_600 # 100MB
|
|
233
|
+
if entry.orig_length > max_table_size
|
|
234
|
+
issues << {
|
|
235
|
+
severity: "warning",
|
|
236
|
+
category: "woff2_tables",
|
|
237
|
+
message: "Table '#{entry.tag}' has unusually large size: #{entry.orig_length} bytes",
|
|
238
|
+
location: entry.tag,
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
issues
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Check for duplicate table tags
|
|
246
|
+
#
|
|
247
|
+
# @param entries [Array<Woff2TableDirectoryEntry>] All table entries
|
|
248
|
+
# @return [Array<Hash>] Array of duplicate issues
|
|
249
|
+
def check_duplicates(entries)
|
|
250
|
+
issues = []
|
|
251
|
+
|
|
252
|
+
tag_counts = Hash.new(0)
|
|
253
|
+
entries.each { |entry| tag_counts[entry.tag] += 1 }
|
|
254
|
+
|
|
255
|
+
tag_counts.each do |tag, count|
|
|
256
|
+
if count > 1
|
|
257
|
+
issues << {
|
|
258
|
+
severity: "error",
|
|
259
|
+
category: "woff2_tables",
|
|
260
|
+
message: "Duplicate table tag '#{tag}' appears #{count} times",
|
|
261
|
+
location: tag,
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
issues
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|