fontisan 0.2.4 → 0.2.6
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 +168 -32
- data/README.adoc +673 -1091
- data/lib/fontisan/cli.rb +94 -13
- data/lib/fontisan/collection/dfont_builder.rb +315 -0
- data/lib/fontisan/commands/convert_command.rb +118 -7
- data/lib/fontisan/commands/pack_command.rb +129 -22
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +175 -1
- data/lib/fontisan/constants.rb +8 -0
- data/lib/fontisan/converters/collection_converter.rb +438 -0
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/dfont_collection.rb +185 -0
- data/lib/fontisan/font_loader.rb +91 -6
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/parsers/dfont_parser.rb +192 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/true_type_font.rb +8 -46
- data/lib/fontisan/validation/collection_validator.rb +265 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +78 -6
- metadata +13 -12
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- data/lib/fontisan/validation/woff2_validator.rb +0 -248
|
@@ -1,270 +0,0 @@
|
|
|
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
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
require_relative "../models/validation_report"
|
|
5
|
-
require_relative "woff2_header_validator"
|
|
6
|
-
require_relative "woff2_table_validator"
|
|
7
|
-
|
|
8
|
-
module Fontisan
|
|
9
|
-
module Validation
|
|
10
|
-
# Woff2Validator is the main orchestrator for WOFF2 font validation
|
|
11
|
-
#
|
|
12
|
-
# This class coordinates WOFF2-specific validation checks (header, tables)
|
|
13
|
-
# and produces a comprehensive ValidationReport. It is designed to validate
|
|
14
|
-
# WOFF2 encoding quality and spec compliance.
|
|
15
|
-
#
|
|
16
|
-
# Single Responsibility: Orchestration of WOFF2 validation workflow
|
|
17
|
-
#
|
|
18
|
-
# @example Validating a WOFF2 font
|
|
19
|
-
# validator = Woff2Validator.new(level: :standard)
|
|
20
|
-
# report = validator.validate(woff2_font, font_path)
|
|
21
|
-
# puts report.text_summary
|
|
22
|
-
#
|
|
23
|
-
# @example Validating WOFF2 encoding result
|
|
24
|
-
# woff2_font = Woff2Font.from_file("output.woff2")
|
|
25
|
-
# validator = Woff2Validator.new
|
|
26
|
-
# report = validator.validate(woff2_font, "output.woff2")
|
|
27
|
-
# puts "Valid: #{report.valid}"
|
|
28
|
-
class Woff2Validator
|
|
29
|
-
# Validation levels
|
|
30
|
-
LEVELS = %i[strict standard lenient].freeze
|
|
31
|
-
|
|
32
|
-
# Initialize WOFF2 validator
|
|
33
|
-
#
|
|
34
|
-
# @param level [Symbol] Validation level (:strict, :standard, :lenient)
|
|
35
|
-
# @param rules_path [String, nil] Path to custom rules file
|
|
36
|
-
def initialize(level: :standard, rules_path: nil)
|
|
37
|
-
@level = level
|
|
38
|
-
validate_level!
|
|
39
|
-
|
|
40
|
-
@rules = load_rules(rules_path)
|
|
41
|
-
@header_validator = Woff2HeaderValidator.new(@rules)
|
|
42
|
-
@table_validator = Woff2TableValidator.new(@rules)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Validate a WOFF2 font
|
|
46
|
-
#
|
|
47
|
-
# @param woff2_font [Woff2Font] The WOFF2 font to validate
|
|
48
|
-
# @param font_path [String] Path to the font file
|
|
49
|
-
# @return [Models::ValidationReport] Validation report
|
|
50
|
-
def validate(woff2_font, font_path)
|
|
51
|
-
report = Models::ValidationReport.new(
|
|
52
|
-
font_path: font_path,
|
|
53
|
-
valid: true,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
begin
|
|
57
|
-
# Run all validation checks
|
|
58
|
-
all_issues = []
|
|
59
|
-
|
|
60
|
-
# 1. Header validation
|
|
61
|
-
all_issues.concat(@header_validator.validate(woff2_font))
|
|
62
|
-
|
|
63
|
-
# 2. Table validation
|
|
64
|
-
all_issues.concat(@table_validator.validate(woff2_font))
|
|
65
|
-
|
|
66
|
-
# 3. WOFF2-specific checks
|
|
67
|
-
all_issues.concat(check_woff2_specific(woff2_font))
|
|
68
|
-
|
|
69
|
-
# Add issues to report
|
|
70
|
-
all_issues.each do |issue|
|
|
71
|
-
case issue[:severity]
|
|
72
|
-
when "error"
|
|
73
|
-
report.add_error(issue[:category], issue[:message],
|
|
74
|
-
issue[:location])
|
|
75
|
-
when "warning"
|
|
76
|
-
report.add_warning(issue[:category], issue[:message],
|
|
77
|
-
issue[:location])
|
|
78
|
-
when "info"
|
|
79
|
-
report.add_info(issue[:category], issue[:message],
|
|
80
|
-
issue[:location])
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Determine overall validity based on level
|
|
85
|
-
report.valid = determine_validity(report)
|
|
86
|
-
rescue StandardError => e
|
|
87
|
-
report.add_error("woff2_validation", "WOFF2 validation failed: #{e.message}", nil)
|
|
88
|
-
report.valid = false
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
report
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Get the current validation level
|
|
95
|
-
#
|
|
96
|
-
# @return [Symbol] The validation level
|
|
97
|
-
attr_reader :level
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
# Validate that the level is supported
|
|
102
|
-
#
|
|
103
|
-
# @raise [ArgumentError] if level is invalid
|
|
104
|
-
# @return [void]
|
|
105
|
-
def validate_level!
|
|
106
|
-
unless LEVELS.include?(@level)
|
|
107
|
-
raise ArgumentError,
|
|
108
|
-
"Invalid validation level: #{@level}. Must be one of: #{LEVELS.join(', ')}"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Load validation rules
|
|
113
|
-
#
|
|
114
|
-
# @param rules_path [String, nil] Path to custom rules file
|
|
115
|
-
# @return [Hash] The rules configuration
|
|
116
|
-
def load_rules(rules_path)
|
|
117
|
-
path = rules_path || default_rules_path
|
|
118
|
-
YAML.load_file(path)
|
|
119
|
-
rescue Errno::ENOENT
|
|
120
|
-
# If rules file doesn't exist, use minimal defaults
|
|
121
|
-
{
|
|
122
|
-
"woff2_validation" => {
|
|
123
|
-
"min_compression_ratio" => 0.2,
|
|
124
|
-
"max_compression_ratio" => 0.95,
|
|
125
|
-
"max_table_size" => 104_857_600,
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
rescue Psych::SyntaxError => e
|
|
129
|
-
raise "Invalid validation rules YAML: #{e.message}"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Get the default rules path
|
|
133
|
-
#
|
|
134
|
-
# @return [String] Path to default rules file
|
|
135
|
-
def default_rules_path
|
|
136
|
-
File.join(__dir__, "..", "config", "validation_rules.yml")
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# WOFF2-specific validation checks
|
|
140
|
-
#
|
|
141
|
-
# @param woff2_font [Woff2Font] The WOFF2 font
|
|
142
|
-
# @return [Array<Hash>] Array of WOFF2-specific issues
|
|
143
|
-
def check_woff2_specific(woff2_font)
|
|
144
|
-
issues = []
|
|
145
|
-
|
|
146
|
-
# Check required tables for font type
|
|
147
|
-
issues.concat(check_required_woff2_tables(woff2_font))
|
|
148
|
-
|
|
149
|
-
# Check compression quality
|
|
150
|
-
issues.concat(check_compression_quality(woff2_font))
|
|
151
|
-
|
|
152
|
-
issues
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Check required tables based on font flavor
|
|
156
|
-
#
|
|
157
|
-
# @param woff2_font [Woff2Font] The WOFF2 font
|
|
158
|
-
# @return [Array<Hash>] Array of required table issues
|
|
159
|
-
def check_required_woff2_tables(woff2_font)
|
|
160
|
-
issues = []
|
|
161
|
-
|
|
162
|
-
# Basic required tables for all fonts
|
|
163
|
-
required_tables = %w[head hhea maxp name cmap post]
|
|
164
|
-
|
|
165
|
-
# Add flavor-specific tables
|
|
166
|
-
if woff2_font.truetype?
|
|
167
|
-
# For TrueType, we need glyf and hmtx
|
|
168
|
-
# Note: loca is NOT required in WOFF2 table directory because it can be
|
|
169
|
-
# reconstructed from transformed glyf. This is standard WOFF2 behavior.
|
|
170
|
-
required_tables << "glyf"
|
|
171
|
-
required_tables << "hmtx"
|
|
172
|
-
elsif woff2_font.cff?
|
|
173
|
-
required_tables << "CFF "
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Check each required table
|
|
177
|
-
required_tables.each do |table_tag|
|
|
178
|
-
unless woff2_font.has_table?(table_tag)
|
|
179
|
-
issues << {
|
|
180
|
-
severity: "error",
|
|
181
|
-
category: "woff2_structure",
|
|
182
|
-
message: "Missing required table: #{table_tag}",
|
|
183
|
-
location: nil,
|
|
184
|
-
}
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
issues
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Check compression quality
|
|
192
|
-
#
|
|
193
|
-
# @param woff2_font [Woff2Font] The WOFF2 font
|
|
194
|
-
# @return [Array<Hash>] Array of compression quality issues
|
|
195
|
-
def check_compression_quality(woff2_font)
|
|
196
|
-
issues = []
|
|
197
|
-
|
|
198
|
-
header = woff2_font.header
|
|
199
|
-
return issues unless header
|
|
200
|
-
|
|
201
|
-
# Calculate actual compression ratio
|
|
202
|
-
if header.total_sfnt_size.positive? && header.total_compressed_size.positive?
|
|
203
|
-
ratio = header.total_compressed_size.to_f / header.total_sfnt_size
|
|
204
|
-
percentage = (ratio * 100).round(2)
|
|
205
|
-
|
|
206
|
-
# Info about compression achieved
|
|
207
|
-
issues << {
|
|
208
|
-
severity: "info",
|
|
209
|
-
category: "woff2_compression",
|
|
210
|
-
message: "Compression ratio: #{percentage}% (#{header.total_compressed_size} / #{header.total_sfnt_size} bytes)",
|
|
211
|
-
location: nil,
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
# Warn if compression is poor (> 80%)
|
|
215
|
-
if ratio > 0.80
|
|
216
|
-
issues << {
|
|
217
|
-
severity: "warning",
|
|
218
|
-
category: "woff2_compression",
|
|
219
|
-
message: "Poor compression ratio: #{percentage}% (expected < 80%)",
|
|
220
|
-
location: nil,
|
|
221
|
-
}
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
issues
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Determine if WOFF2 font is valid based on validation level
|
|
229
|
-
#
|
|
230
|
-
# @param report [Models::ValidationReport] The validation report
|
|
231
|
-
# @return [Boolean] true if font is valid for the given level
|
|
232
|
-
def determine_validity(report)
|
|
233
|
-
case @level
|
|
234
|
-
when :strict
|
|
235
|
-
# Strict: no errors, no warnings
|
|
236
|
-
!report.has_errors? && !report.has_warnings?
|
|
237
|
-
when :standard
|
|
238
|
-
# Standard: no errors (warnings allowed)
|
|
239
|
-
!report.has_errors?
|
|
240
|
-
when :lenient
|
|
241
|
-
# Lenient: no critical errors (some errors may be acceptable)
|
|
242
|
-
# For WOFF2, treat lenient same as standard
|
|
243
|
-
!report.has_errors?
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|