fontisan 0.2.4 → 0.2.5
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 +150 -30
- data/README.adoc +497 -242
- data/lib/fontisan/cli.rb +67 -6
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/models/validation_report.rb +227 -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/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 +7 -11
- 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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "basic_validator"
|
|
4
|
+
require_relative "font_book_validator"
|
|
5
|
+
require_relative "opentype_validator"
|
|
6
|
+
require_relative "web_font_validator"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Validators
|
|
10
|
+
# ProfileLoader manages validation profiles and loads appropriate validators
|
|
11
|
+
#
|
|
12
|
+
# This class provides a registry of validation profiles, each configured for
|
|
13
|
+
# specific use cases. Profiles define which validator to use, loading mode,
|
|
14
|
+
# and severity thresholds.
|
|
15
|
+
#
|
|
16
|
+
# Available profiles:
|
|
17
|
+
# - indexability: Fast validation for font discovery (BasicValidator)
|
|
18
|
+
# - usability: Basic usability for installation (FontBookValidator)
|
|
19
|
+
# - production: Comprehensive quality checks (OpenTypeValidator)
|
|
20
|
+
# - web: Web embedding and optimization (WebFontValidator)
|
|
21
|
+
# - spec_compliance: Full OpenType spec compliance (OpenTypeValidator)
|
|
22
|
+
# - default: Alias for production profile
|
|
23
|
+
#
|
|
24
|
+
# @example Loading a profile
|
|
25
|
+
# validator = ProfileLoader.load(:production)
|
|
26
|
+
# report = validator.validate(font)
|
|
27
|
+
#
|
|
28
|
+
# @example Getting profile info
|
|
29
|
+
# info = ProfileLoader.profile_info(:web)
|
|
30
|
+
# puts info[:description]
|
|
31
|
+
class ProfileLoader
|
|
32
|
+
# Profile definitions (hardcoded, no YAML)
|
|
33
|
+
PROFILES = {
|
|
34
|
+
indexability: {
|
|
35
|
+
name: "Font Indexability",
|
|
36
|
+
description: "Fast validation for font discovery and indexing",
|
|
37
|
+
validator: "BasicValidator",
|
|
38
|
+
loading_mode: "metadata",
|
|
39
|
+
severity_threshold: "error",
|
|
40
|
+
},
|
|
41
|
+
usability: {
|
|
42
|
+
name: "Font Usability",
|
|
43
|
+
description: "Basic usability for installation",
|
|
44
|
+
validator: "FontBookValidator",
|
|
45
|
+
loading_mode: "full",
|
|
46
|
+
severity_threshold: "warning",
|
|
47
|
+
},
|
|
48
|
+
production: {
|
|
49
|
+
name: "Production Quality",
|
|
50
|
+
description: "Comprehensive quality checks",
|
|
51
|
+
validator: "OpenTypeValidator",
|
|
52
|
+
loading_mode: "full",
|
|
53
|
+
severity_threshold: "warning",
|
|
54
|
+
},
|
|
55
|
+
web: {
|
|
56
|
+
name: "Web Font Readiness",
|
|
57
|
+
description: "Web embedding and optimization",
|
|
58
|
+
validator: "WebFontValidator",
|
|
59
|
+
loading_mode: "full",
|
|
60
|
+
severity_threshold: "warning",
|
|
61
|
+
},
|
|
62
|
+
spec_compliance: {
|
|
63
|
+
name: "OpenType Specification",
|
|
64
|
+
description: "Full OpenType spec compliance",
|
|
65
|
+
validator: "OpenTypeValidator",
|
|
66
|
+
loading_mode: "full",
|
|
67
|
+
severity_threshold: "info",
|
|
68
|
+
},
|
|
69
|
+
default: {
|
|
70
|
+
name: "Default Profile",
|
|
71
|
+
description: "Default validation profile (alias for production)",
|
|
72
|
+
validator: "OpenTypeValidator",
|
|
73
|
+
loading_mode: "full",
|
|
74
|
+
severity_threshold: "warning",
|
|
75
|
+
},
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
class << self
|
|
79
|
+
# Load a validator for the specified profile
|
|
80
|
+
#
|
|
81
|
+
# @param profile_name [Symbol, String] Profile name
|
|
82
|
+
# @return [Validator] Validator instance for the profile
|
|
83
|
+
# @raise [ArgumentError] if profile name is unknown
|
|
84
|
+
#
|
|
85
|
+
# @example Load production validator
|
|
86
|
+
# validator = ProfileLoader.load(:production)
|
|
87
|
+
def load(profile_name)
|
|
88
|
+
profile_name = profile_name.to_sym
|
|
89
|
+
profile_config = PROFILES[profile_name]
|
|
90
|
+
|
|
91
|
+
unless profile_config
|
|
92
|
+
raise ArgumentError,
|
|
93
|
+
"Unknown profile: #{profile_name}. " \
|
|
94
|
+
"Available profiles: #{available_profiles.join(', ')}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
validator_class_name = profile_config[:validator]
|
|
98
|
+
validator_class = Validators.const_get(validator_class_name)
|
|
99
|
+
validator_class.new
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get list of available profile names
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Symbol>] Array of profile names
|
|
105
|
+
#
|
|
106
|
+
# @example List available profiles
|
|
107
|
+
# ProfileLoader.available_profiles
|
|
108
|
+
# # => [:indexability, :usability, :production, :web, :spec_compliance, :default]
|
|
109
|
+
def available_profiles
|
|
110
|
+
PROFILES.keys
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get profile configuration
|
|
114
|
+
#
|
|
115
|
+
# @param profile_name [Symbol, String] Profile name
|
|
116
|
+
# @return [Hash, nil] Profile configuration or nil if not found
|
|
117
|
+
#
|
|
118
|
+
# @example Get profile info
|
|
119
|
+
# info = ProfileLoader.profile_info(:web)
|
|
120
|
+
# puts info[:description]
|
|
121
|
+
def profile_info(profile_name)
|
|
122
|
+
PROFILES[profile_name.to_sym]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get all profiles with their configurations
|
|
126
|
+
#
|
|
127
|
+
# @return [Hash] All profile configurations
|
|
128
|
+
#
|
|
129
|
+
# @example Get all profiles
|
|
130
|
+
# ProfileLoader.all_profiles.each do |name, config|
|
|
131
|
+
# puts "#{name}: #{config[:description]}"
|
|
132
|
+
# end
|
|
133
|
+
def all_profiles
|
|
134
|
+
PROFILES
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/validation_report"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Validators
|
|
7
|
+
# Base class for all validators using block-based DSL
|
|
8
|
+
#
|
|
9
|
+
# This class provides a declarative DSL for defining validation checks
|
|
10
|
+
# and an execution engine that runs those checks against font files.
|
|
11
|
+
# Subclasses define their validation logic by implementing define_checks.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a custom validator
|
|
14
|
+
# class MyValidator < Validator
|
|
15
|
+
# private
|
|
16
|
+
#
|
|
17
|
+
# def define_checks
|
|
18
|
+
# check_table :name_table, 'name' do
|
|
19
|
+
# check_field :family_name, :family_name do |table, value|
|
|
20
|
+
# !value.nil? && !value.empty?
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Running validation
|
|
27
|
+
# validator = MyValidator.new
|
|
28
|
+
# report = validator.validate(font)
|
|
29
|
+
# puts report.valid?
|
|
30
|
+
class Validator
|
|
31
|
+
# Initialize validator and define checks
|
|
32
|
+
def initialize
|
|
33
|
+
@checks = []
|
|
34
|
+
@current_table_context = nil
|
|
35
|
+
define_checks
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Validate a font and return a ValidationReport
|
|
39
|
+
#
|
|
40
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font object to validate
|
|
41
|
+
# @return [ValidationReport] Complete validation report
|
|
42
|
+
def validate(font)
|
|
43
|
+
start_time = Time.now
|
|
44
|
+
all_results = []
|
|
45
|
+
|
|
46
|
+
@checks.each do |check_def|
|
|
47
|
+
result = execute_check(font, check_def)
|
|
48
|
+
all_results << result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
elapsed = Time.now - start_time
|
|
52
|
+
build_report(font, all_results, elapsed)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
# DSL method: Define a table-level check
|
|
58
|
+
#
|
|
59
|
+
# @param check_id [Symbol] Unique identifier for this check
|
|
60
|
+
# @param table_tag [String] OpenType table tag (e.g., 'name', 'head')
|
|
61
|
+
# @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
|
|
62
|
+
# @param block [Proc] Check logic that receives table as parameter
|
|
63
|
+
def check_table(check_id, table_tag, severity: :error, &block)
|
|
64
|
+
@checks << {
|
|
65
|
+
type: :table,
|
|
66
|
+
id: check_id,
|
|
67
|
+
table_tag: table_tag,
|
|
68
|
+
severity: severity,
|
|
69
|
+
block: block,
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# DSL method: Define a field-level check
|
|
74
|
+
#
|
|
75
|
+
# Must be called within a check_table block to establish table context
|
|
76
|
+
#
|
|
77
|
+
# @param check_id [Symbol] Unique identifier for this check
|
|
78
|
+
# @param field_key [Symbol] Field name to check
|
|
79
|
+
# @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
|
|
80
|
+
# @param block [Proc] Check logic that receives (table, value) as parameters
|
|
81
|
+
def check_field(check_id, field_key, severity: :error, &block)
|
|
82
|
+
unless @current_table_context
|
|
83
|
+
raise ArgumentError, "check_field must be called within check_table block"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@checks << {
|
|
87
|
+
type: :field,
|
|
88
|
+
id: check_id,
|
|
89
|
+
table_tag: @current_table_context,
|
|
90
|
+
field: field_key,
|
|
91
|
+
severity: severity,
|
|
92
|
+
block: block,
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# DSL method: Define a structural validation check
|
|
97
|
+
#
|
|
98
|
+
# Used for checks that validate font structure and relationships
|
|
99
|
+
#
|
|
100
|
+
# @param check_id [Symbol] Unique identifier for this check
|
|
101
|
+
# @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
|
|
102
|
+
# @param block [Proc] Check logic that receives font as parameter
|
|
103
|
+
def check_structure(check_id, severity: :error, &block)
|
|
104
|
+
@checks << {
|
|
105
|
+
type: :structure,
|
|
106
|
+
id: check_id,
|
|
107
|
+
severity: severity,
|
|
108
|
+
block: block,
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# DSL method: Define a usability check
|
|
113
|
+
#
|
|
114
|
+
# Used for checks that validate font usability and best practices
|
|
115
|
+
#
|
|
116
|
+
# @param check_id [Symbol] Unique identifier for this check
|
|
117
|
+
# @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
|
|
118
|
+
# @param block [Proc] Check logic that receives font as parameter
|
|
119
|
+
def check_usability(check_id, severity: :warning, &block)
|
|
120
|
+
@checks << {
|
|
121
|
+
type: :usability,
|
|
122
|
+
id: check_id,
|
|
123
|
+
severity: severity,
|
|
124
|
+
block: block,
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# DSL method: Define an instruction validation check
|
|
129
|
+
#
|
|
130
|
+
# Used for checks that validate TrueType instructions/hinting
|
|
131
|
+
#
|
|
132
|
+
# @param check_id [Symbol] Unique identifier for this check
|
|
133
|
+
# @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
|
|
134
|
+
# @param block [Proc] Check logic that receives font as parameter
|
|
135
|
+
def check_instructions(check_id, severity: :warning, &block)
|
|
136
|
+
@checks << {
|
|
137
|
+
type: :instructions,
|
|
138
|
+
id: check_id,
|
|
139
|
+
severity: severity,
|
|
140
|
+
block: block,
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# DSL method: Define a glyph-level check
|
|
145
|
+
#
|
|
146
|
+
# Used for checks that validate individual glyphs
|
|
147
|
+
#
|
|
148
|
+
# @param check_id [Symbol] Unique identifier for this check
|
|
149
|
+
# @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
|
|
150
|
+
# @param block [Proc] Check logic that receives font as parameter
|
|
151
|
+
def check_glyphs(check_id, severity: :error, &block)
|
|
152
|
+
@checks << {
|
|
153
|
+
type: :glyphs,
|
|
154
|
+
id: check_id,
|
|
155
|
+
severity: severity,
|
|
156
|
+
block: block,
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Template method: Subclasses implement this to define their checks
|
|
163
|
+
#
|
|
164
|
+
# @return [void]
|
|
165
|
+
def define_checks
|
|
166
|
+
# Subclasses override this method
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Execute a single check using strategy pattern
|
|
170
|
+
#
|
|
171
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
172
|
+
# @param check_def [Hash] Check definition
|
|
173
|
+
# @return [Hash] Check result with :passed, :severity, :messages, :issues
|
|
174
|
+
def execute_check(font, check_def)
|
|
175
|
+
case check_def[:type]
|
|
176
|
+
when :table
|
|
177
|
+
execute_table_check(font, check_def)
|
|
178
|
+
when :field
|
|
179
|
+
execute_field_check(font, check_def)
|
|
180
|
+
when :structure
|
|
181
|
+
execute_structure_check(font, check_def)
|
|
182
|
+
when :usability
|
|
183
|
+
execute_usability_check(font, check_def)
|
|
184
|
+
when :instructions
|
|
185
|
+
execute_instruction_check(font, check_def)
|
|
186
|
+
when :glyphs
|
|
187
|
+
execute_glyph_check(font, check_def)
|
|
188
|
+
else
|
|
189
|
+
{
|
|
190
|
+
check_id: check_def[:id],
|
|
191
|
+
passed: false,
|
|
192
|
+
severity: :fatal,
|
|
193
|
+
messages: ["Unknown check type: #{check_def[:type]}"],
|
|
194
|
+
issues: [],
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
rescue => e
|
|
198
|
+
{
|
|
199
|
+
check_id: check_def[:id],
|
|
200
|
+
passed: false,
|
|
201
|
+
severity: :fatal,
|
|
202
|
+
messages: ["Check execution failed: #{e.message}"],
|
|
203
|
+
issues: [{
|
|
204
|
+
severity: "fatal",
|
|
205
|
+
category: "check_execution",
|
|
206
|
+
message: "Exception during check execution: #{e.class} - #{e.message}",
|
|
207
|
+
}],
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Execute a table-level check
|
|
212
|
+
#
|
|
213
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
214
|
+
# @param check_def [Hash] Check definition
|
|
215
|
+
# @return [Hash] Check result
|
|
216
|
+
def execute_table_check(font, check_def)
|
|
217
|
+
table_tag = check_def[:table_tag]
|
|
218
|
+
table = font.table(table_tag)
|
|
219
|
+
|
|
220
|
+
unless table
|
|
221
|
+
return {
|
|
222
|
+
check_id: check_def[:id],
|
|
223
|
+
passed: false,
|
|
224
|
+
severity: check_def[:severity].to_s,
|
|
225
|
+
messages: ["Table '#{table_tag}' not found in font"],
|
|
226
|
+
table: table_tag,
|
|
227
|
+
issues: [{
|
|
228
|
+
severity: check_def[:severity].to_s,
|
|
229
|
+
category: "table_presence",
|
|
230
|
+
table: table_tag,
|
|
231
|
+
message: "Required table '#{table_tag}' is missing",
|
|
232
|
+
}],
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Set context for nested field checks
|
|
237
|
+
old_context = @current_table_context
|
|
238
|
+
@current_table_context = table_tag
|
|
239
|
+
|
|
240
|
+
begin
|
|
241
|
+
result = check_def[:block].call(table)
|
|
242
|
+
passed = result != false && result != nil
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
check_id: check_def[:id],
|
|
246
|
+
passed: passed,
|
|
247
|
+
severity: check_def[:severity].to_s,
|
|
248
|
+
messages: passed ? [] : ["Table '#{table_tag}' validation failed"],
|
|
249
|
+
table: table_tag,
|
|
250
|
+
issues: passed ? [] : [{
|
|
251
|
+
severity: check_def[:severity].to_s,
|
|
252
|
+
category: "table_validation",
|
|
253
|
+
table: table_tag,
|
|
254
|
+
message: "Table '#{table_tag}' failed validation",
|
|
255
|
+
}],
|
|
256
|
+
}
|
|
257
|
+
ensure
|
|
258
|
+
@current_table_context = old_context
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Execute a field-level check
|
|
263
|
+
#
|
|
264
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
265
|
+
# @param check_def [Hash] Check definition
|
|
266
|
+
# @return [Hash] Check result
|
|
267
|
+
def execute_field_check(font, check_def)
|
|
268
|
+
table_tag = check_def[:table_tag]
|
|
269
|
+
field_key = check_def[:field]
|
|
270
|
+
table = font.table(table_tag)
|
|
271
|
+
|
|
272
|
+
unless table
|
|
273
|
+
return {
|
|
274
|
+
check_id: check_def[:id],
|
|
275
|
+
passed: false,
|
|
276
|
+
severity: check_def[:severity].to_s,
|
|
277
|
+
messages: ["Table '#{table_tag}' not found"],
|
|
278
|
+
table: table_tag,
|
|
279
|
+
field: field_key.to_s,
|
|
280
|
+
issues: [{
|
|
281
|
+
severity: check_def[:severity].to_s,
|
|
282
|
+
category: "table_presence",
|
|
283
|
+
table: table_tag,
|
|
284
|
+
field: field_key.to_s,
|
|
285
|
+
message: "Cannot validate field '#{field_key}': table '#{table_tag}' missing",
|
|
286
|
+
}],
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Get field value
|
|
291
|
+
value = if table.respond_to?(field_key)
|
|
292
|
+
table.public_send(field_key)
|
|
293
|
+
else
|
|
294
|
+
nil
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
result = check_def[:block].call(table, value)
|
|
298
|
+
passed = result != false && result != nil
|
|
299
|
+
|
|
300
|
+
{
|
|
301
|
+
check_id: check_def[:id],
|
|
302
|
+
passed: passed,
|
|
303
|
+
severity: check_def[:severity].to_s,
|
|
304
|
+
messages: passed ? [] : ["Field '#{field_key}' validation failed"],
|
|
305
|
+
table: table_tag,
|
|
306
|
+
field: field_key.to_s,
|
|
307
|
+
issues: passed ? [] : [{
|
|
308
|
+
severity: check_def[:severity].to_s,
|
|
309
|
+
category: "field_validation",
|
|
310
|
+
table: table_tag,
|
|
311
|
+
field: field_key.to_s,
|
|
312
|
+
message: "Field '#{field_key}' in table '#{table_tag}' failed validation",
|
|
313
|
+
}],
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Execute a structure check
|
|
318
|
+
#
|
|
319
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
320
|
+
# @param check_def [Hash] Check definition
|
|
321
|
+
# @return [Hash] Check result
|
|
322
|
+
def execute_structure_check(font, check_def)
|
|
323
|
+
result = check_def[:block].call(font)
|
|
324
|
+
passed = result != false && result != nil
|
|
325
|
+
|
|
326
|
+
{
|
|
327
|
+
check_id: check_def[:id],
|
|
328
|
+
passed: passed,
|
|
329
|
+
severity: check_def[:severity].to_s,
|
|
330
|
+
messages: passed ? [] : ["Structure validation failed"],
|
|
331
|
+
issues: passed ? [] : [{
|
|
332
|
+
severity: check_def[:severity].to_s,
|
|
333
|
+
category: "structure",
|
|
334
|
+
message: "Font structure validation failed for check '#{check_def[:id]}'",
|
|
335
|
+
}],
|
|
336
|
+
}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Execute a usability check
|
|
340
|
+
#
|
|
341
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
342
|
+
# @param check_def [Hash] Check definition
|
|
343
|
+
# @return [Hash] Check result
|
|
344
|
+
def execute_usability_check(font, check_def)
|
|
345
|
+
result = check_def[:block].call(font)
|
|
346
|
+
passed = result != false && result != nil
|
|
347
|
+
|
|
348
|
+
{
|
|
349
|
+
check_id: check_def[:id],
|
|
350
|
+
passed: passed,
|
|
351
|
+
severity: check_def[:severity].to_s,
|
|
352
|
+
messages: passed ? [] : ["Usability check failed"],
|
|
353
|
+
issues: passed ? [] : [{
|
|
354
|
+
severity: check_def[:severity].to_s,
|
|
355
|
+
category: "usability",
|
|
356
|
+
message: "Font usability check failed for '#{check_def[:id]}'",
|
|
357
|
+
}],
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Execute an instruction check
|
|
362
|
+
#
|
|
363
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
364
|
+
# @param check_def [Hash] Check definition
|
|
365
|
+
# @return [Hash] Check result
|
|
366
|
+
def execute_instruction_check(font, check_def)
|
|
367
|
+
result = check_def[:block].call(font)
|
|
368
|
+
passed = result != false && result != nil
|
|
369
|
+
|
|
370
|
+
{
|
|
371
|
+
check_id: check_def[:id],
|
|
372
|
+
passed: passed,
|
|
373
|
+
severity: check_def[:severity].to_s,
|
|
374
|
+
messages: passed ? [] : ["Instruction validation failed"],
|
|
375
|
+
issues: passed ? [] : [{
|
|
376
|
+
severity: check_def[:severity].to_s,
|
|
377
|
+
category: "instructions",
|
|
378
|
+
message: "TrueType instruction check failed for '#{check_def[:id]}'",
|
|
379
|
+
}],
|
|
380
|
+
}
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Execute a glyph check
|
|
384
|
+
#
|
|
385
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
386
|
+
# @param check_def [Hash] Check definition
|
|
387
|
+
# @return [Hash] Check result
|
|
388
|
+
def execute_glyph_check(font, check_def)
|
|
389
|
+
result = check_def[:block].call(font)
|
|
390
|
+
passed = result != false && result != nil
|
|
391
|
+
|
|
392
|
+
{
|
|
393
|
+
check_id: check_def[:id],
|
|
394
|
+
passed: passed,
|
|
395
|
+
severity: check_def[:severity].to_s,
|
|
396
|
+
messages: passed ? [] : ["Glyph validation failed"],
|
|
397
|
+
issues: passed ? [] : [{
|
|
398
|
+
severity: check_def[:severity].to_s,
|
|
399
|
+
category: "glyphs",
|
|
400
|
+
message: "Glyph validation failed for check '#{check_def[:id]}'",
|
|
401
|
+
}],
|
|
402
|
+
}
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Build ValidationReport from check results
|
|
406
|
+
#
|
|
407
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font that was validated
|
|
408
|
+
# @param all_results [Array<Hash>] All check results
|
|
409
|
+
# @param elapsed [Float] Elapsed time in seconds
|
|
410
|
+
# @return [ValidationReport] Complete report
|
|
411
|
+
def build_report(font, all_results, elapsed)
|
|
412
|
+
# Extract font path from font object
|
|
413
|
+
font_path = if font.respond_to?(:path)
|
|
414
|
+
font.path
|
|
415
|
+
elsif font.respond_to?(:filename)
|
|
416
|
+
font.filename
|
|
417
|
+
elsif font.instance_variable_defined?(:@filename)
|
|
418
|
+
font.instance_variable_get(:@filename)
|
|
419
|
+
else
|
|
420
|
+
"unknown"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
report = Models::ValidationReport.new(
|
|
424
|
+
font_path: font_path,
|
|
425
|
+
valid: true,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Build CheckResult objects
|
|
429
|
+
all_results.each do |result|
|
|
430
|
+
check_result = Models::ValidationReport::CheckResult.new(
|
|
431
|
+
check_id: result[:check_id].to_s,
|
|
432
|
+
passed: result[:passed],
|
|
433
|
+
severity: result[:severity],
|
|
434
|
+
messages: result[:messages] || [],
|
|
435
|
+
table: result[:table],
|
|
436
|
+
field: result[:field],
|
|
437
|
+
)
|
|
438
|
+
report.check_results << check_result
|
|
439
|
+
|
|
440
|
+
# Add issues to main report
|
|
441
|
+
if result[:issues]
|
|
442
|
+
result[:issues].each do |issue_data|
|
|
443
|
+
case issue_data[:severity]
|
|
444
|
+
when "error", "fatal"
|
|
445
|
+
report.add_error(
|
|
446
|
+
issue_data[:category] || "validation",
|
|
447
|
+
issue_data[:message],
|
|
448
|
+
issue_data[:table] || issue_data[:field],
|
|
449
|
+
)
|
|
450
|
+
when "warning"
|
|
451
|
+
report.add_warning(
|
|
452
|
+
issue_data[:category] || "validation",
|
|
453
|
+
issue_data[:message],
|
|
454
|
+
issue_data[:table] || issue_data[:field],
|
|
455
|
+
)
|
|
456
|
+
when "info"
|
|
457
|
+
report.add_info(
|
|
458
|
+
issue_data[:category] || "validation",
|
|
459
|
+
issue_data[:message],
|
|
460
|
+
issue_data[:table] || issue_data[:field],
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Mark checks performed
|
|
468
|
+
report.checks_performed = all_results.map { |r| r[:check_id].to_s }
|
|
469
|
+
|
|
470
|
+
# Set status based on results
|
|
471
|
+
if report.has_errors?
|
|
472
|
+
report.status = "invalid"
|
|
473
|
+
report.valid = false
|
|
474
|
+
elsif report.has_warnings?
|
|
475
|
+
report.status = "valid_with_warnings"
|
|
476
|
+
else
|
|
477
|
+
report.status = "valid"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
report
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|