fontisan 0.2.3 → 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 +221 -49
- data/README.adoc +519 -5
- data/Rakefile +20 -7
- data/lib/fontisan/cli.rb +67 -6
- 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/commands/validate_command.rb +107 -151
- 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 +84 -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/models/validation_report.rb +227 -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/pipeline/transformation_pipeline.rb +4 -8
- 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/cmap.rb +82 -2
- 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/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/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/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/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 +90 -6
- metadata +20 -9
- 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
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Validation
|
|
5
|
-
# ConsistencyValidator validates cross-table consistency
|
|
6
|
-
#
|
|
7
|
-
# This validator ensures that references between tables are valid,
|
|
8
|
-
# such as cmap glyph references, hmtx entry counts, and variable
|
|
9
|
-
# font table consistency.
|
|
10
|
-
#
|
|
11
|
-
# Single Responsibility: Cross-table data consistency validation
|
|
12
|
-
#
|
|
13
|
-
# @example Validating consistency
|
|
14
|
-
# validator = ConsistencyValidator.new(rules)
|
|
15
|
-
# issues = validator.validate(font)
|
|
16
|
-
class ConsistencyValidator
|
|
17
|
-
# Initialize consistency validator
|
|
18
|
-
#
|
|
19
|
-
# @param rules [Hash] Validation rules configuration
|
|
20
|
-
def initialize(rules)
|
|
21
|
-
@rules = rules
|
|
22
|
-
@consistency_config = rules["consistency_checks"] || {}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Validate font consistency
|
|
26
|
-
#
|
|
27
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
28
|
-
# @return [Array<Hash>] Array of validation issues
|
|
29
|
-
def validate(font)
|
|
30
|
-
issues = []
|
|
31
|
-
|
|
32
|
-
# Check hmtx consistency if enabled
|
|
33
|
-
issues.concat(check_hmtx_consistency(font)) if should_check?("check_hmtx_consistency")
|
|
34
|
-
|
|
35
|
-
# Check name table consistency if enabled
|
|
36
|
-
issues.concat(check_name_consistency(font)) if should_check?("check_name_consistency")
|
|
37
|
-
|
|
38
|
-
# Check variable font consistency if enabled
|
|
39
|
-
issues.concat(check_variable_consistency(font)) if should_check?("check_variable_consistency")
|
|
40
|
-
|
|
41
|
-
issues
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
# Check if a validation should be performed
|
|
47
|
-
#
|
|
48
|
-
# @param check_name [String] The check name
|
|
49
|
-
# @return [Boolean] true if check should be performed
|
|
50
|
-
def should_check?(check_name)
|
|
51
|
-
@rules.dig("validation_levels", "standard", check_name)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Check hmtx entry count matches glyph count
|
|
55
|
-
#
|
|
56
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
57
|
-
# @return [Array<Hash>] Array of hmtx consistency issues
|
|
58
|
-
def check_hmtx_consistency(font)
|
|
59
|
-
issues = []
|
|
60
|
-
|
|
61
|
-
hmtx = font.table(Constants::HMTX_TAG)
|
|
62
|
-
maxp = font.table(Constants::MAXP_TAG)
|
|
63
|
-
hhea = font.table(Constants::HHEA_TAG)
|
|
64
|
-
|
|
65
|
-
return issues unless hmtx && maxp && hhea
|
|
66
|
-
|
|
67
|
-
glyph_count = maxp.num_glyphs
|
|
68
|
-
num_of_long_hor_metrics = hhea.number_of_h_metrics
|
|
69
|
-
|
|
70
|
-
# Verify the structure makes sense
|
|
71
|
-
if num_of_long_hor_metrics > glyph_count
|
|
72
|
-
issues << {
|
|
73
|
-
severity: "error",
|
|
74
|
-
category: "consistency",
|
|
75
|
-
message: "hhea number_of_h_metrics (#{num_of_long_hor_metrics}) exceeds glyph count (#{glyph_count})",
|
|
76
|
-
location: "hhea/hmtx tables",
|
|
77
|
-
}
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
if num_of_long_hor_metrics < 1
|
|
81
|
-
issues << {
|
|
82
|
-
severity: "error",
|
|
83
|
-
category: "consistency",
|
|
84
|
-
message: "hhea number_of_h_metrics is #{num_of_long_hor_metrics}, must be at least 1",
|
|
85
|
-
location: "hhea table",
|
|
86
|
-
}
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
issues
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Check name table for consistency issues
|
|
93
|
-
#
|
|
94
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
95
|
-
# @return [Array<Hash>] Array of name table issues
|
|
96
|
-
def check_name_consistency(font)
|
|
97
|
-
issues = []
|
|
98
|
-
|
|
99
|
-
name = font.table(Constants::NAME_TAG)
|
|
100
|
-
return issues unless name
|
|
101
|
-
|
|
102
|
-
# Check that required name IDs are present
|
|
103
|
-
required_name_ids = [
|
|
104
|
-
Tables::Name::FAMILY, # 1
|
|
105
|
-
Tables::Name::SUBFAMILY, # 2
|
|
106
|
-
Tables::Name::FULL_NAME, # 4
|
|
107
|
-
Tables::Name::VERSION, # 5
|
|
108
|
-
Tables::Name::POSTSCRIPT_NAME, # 6
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
required_name_ids.each do |name_id|
|
|
112
|
-
has_entry = name.name_records.any? do |record|
|
|
113
|
-
record.name_id == name_id
|
|
114
|
-
end
|
|
115
|
-
unless has_entry
|
|
116
|
-
issues << {
|
|
117
|
-
severity: "warning",
|
|
118
|
-
category: "consistency",
|
|
119
|
-
message: "Missing recommended name ID #{name_id}",
|
|
120
|
-
location: "name table",
|
|
121
|
-
}
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Check for duplicate entries (same platform/encoding/language/nameID)
|
|
126
|
-
seen = {}
|
|
127
|
-
name.name_records.each do |record|
|
|
128
|
-
key = [record.platform_id, record.encoding_id, record.language_id,
|
|
129
|
-
record.name_id]
|
|
130
|
-
if seen[key]
|
|
131
|
-
issues << {
|
|
132
|
-
severity: "warning",
|
|
133
|
-
category: "consistency",
|
|
134
|
-
message: "Duplicate name record: platform=#{record.platform_id}, encoding=#{record.encoding_id}, language=#{record.language_id}, nameID=#{record.name_id}",
|
|
135
|
-
location: "name table",
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
seen[key] = true
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
issues
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Check variable font table consistency
|
|
145
|
-
#
|
|
146
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
147
|
-
# @return [Array<Hash>] Array of variable font issues
|
|
148
|
-
def check_variable_consistency(font)
|
|
149
|
-
issues = []
|
|
150
|
-
|
|
151
|
-
# Only check if this is a variable font
|
|
152
|
-
return issues unless font.has_table?(Constants::FVAR_TAG)
|
|
153
|
-
|
|
154
|
-
fvar = font.table(Constants::FVAR_TAG)
|
|
155
|
-
return issues unless fvar
|
|
156
|
-
|
|
157
|
-
axis_count = fvar.axes.length
|
|
158
|
-
|
|
159
|
-
# For TrueType variable fonts, check gvar consistency
|
|
160
|
-
if font.has_table?(Constants::GVAR_TAG)
|
|
161
|
-
gvar = font.table(Constants::GVAR_TAG)
|
|
162
|
-
gvar_axis_count = gvar.axis_count
|
|
163
|
-
if gvar_axis_count != axis_count
|
|
164
|
-
issues << {
|
|
165
|
-
severity: "error",
|
|
166
|
-
category: "consistency",
|
|
167
|
-
message: "fvar axis count (#{axis_count}) doesn't match gvar axis count (#{gvar_axis_count})",
|
|
168
|
-
location: "fvar/gvar tables",
|
|
169
|
-
}
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Check that recommended variation tables are present
|
|
174
|
-
unless font.has_table?(Constants::GVAR_TAG) || font.has_table?(Constants::CFF2_TAG)
|
|
175
|
-
issues << {
|
|
176
|
-
severity: "error",
|
|
177
|
-
category: "consistency",
|
|
178
|
-
message: "Variable font missing gvar (TrueType) or CFF2 (CFF) table",
|
|
179
|
-
location: "variable font",
|
|
180
|
-
}
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Check for recommended metrics variation tables
|
|
184
|
-
unless font.has_table?(Constants::HVAR_TAG)
|
|
185
|
-
issues << {
|
|
186
|
-
severity: "info",
|
|
187
|
-
category: "consistency",
|
|
188
|
-
message: "Variable font missing HVAR table (recommended for better rendering)",
|
|
189
|
-
location: nil,
|
|
190
|
-
}
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
issues
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
end
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Validation
|
|
5
|
-
# StructureValidator validates the structural integrity of fonts
|
|
6
|
-
#
|
|
7
|
-
# This validator checks the SFNT structure, table offsets, table ordering,
|
|
8
|
-
# and other structural properties that ensure the font file is well-formed.
|
|
9
|
-
#
|
|
10
|
-
# Single Responsibility: Font structure and SFNT format validation
|
|
11
|
-
#
|
|
12
|
-
# @example Validating structure
|
|
13
|
-
# validator = StructureValidator.new(rules)
|
|
14
|
-
# issues = validator.validate(font)
|
|
15
|
-
class StructureValidator
|
|
16
|
-
# Initialize structure validator
|
|
17
|
-
#
|
|
18
|
-
# @param rules [Hash] Validation rules configuration
|
|
19
|
-
def initialize(rules)
|
|
20
|
-
@rules = rules
|
|
21
|
-
@structure_config = rules["structure_validation"] || {}
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Validate font structure
|
|
25
|
-
#
|
|
26
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
27
|
-
# @return [Array<Hash>] Array of validation issues
|
|
28
|
-
def validate(font)
|
|
29
|
-
issues = []
|
|
30
|
-
|
|
31
|
-
# Check glyph count consistency
|
|
32
|
-
issues.concat(check_glyph_consistency(font))
|
|
33
|
-
|
|
34
|
-
# Check table offsets
|
|
35
|
-
issues.concat(check_table_offsets(font)) if @rules.dig(
|
|
36
|
-
"validation_levels", "standard", "check_table_offsets"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
# Check table ordering (optional optimization check)
|
|
40
|
-
issues.concat(check_table_ordering(font)) if @rules.dig(
|
|
41
|
-
"validation_levels", "standard", "check_table_ordering"
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
issues
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# Check glyph count consistency across tables
|
|
50
|
-
#
|
|
51
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
52
|
-
# @return [Array<Hash>] Array of consistency issues
|
|
53
|
-
def check_glyph_consistency(font)
|
|
54
|
-
issues = []
|
|
55
|
-
|
|
56
|
-
# Get glyph count from maxp table
|
|
57
|
-
maxp = font.table(Constants::MAXP_TAG)
|
|
58
|
-
return issues unless maxp
|
|
59
|
-
|
|
60
|
-
expected_count = maxp.num_glyphs
|
|
61
|
-
|
|
62
|
-
# For TrueType fonts, check glyf table glyph count
|
|
63
|
-
if font.has_table?(Constants::GLYF_TAG)
|
|
64
|
-
glyf = font.table(Constants::GLYF_TAG)
|
|
65
|
-
actual_count = glyf.glyphs.length if glyf.respond_to?(:glyphs)
|
|
66
|
-
|
|
67
|
-
if actual_count && actual_count != expected_count
|
|
68
|
-
issues << {
|
|
69
|
-
severity: "error",
|
|
70
|
-
category: "structure",
|
|
71
|
-
message: "Glyph count mismatch: maxp=#{expected_count}, glyf=#{actual_count}",
|
|
72
|
-
location: "glyf table",
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Check glyph count bounds with safe defaults
|
|
78
|
-
min_glyph_count = @structure_config["min_glyph_count"] || 1
|
|
79
|
-
max_glyph_count = @structure_config["max_glyph_count"] || 65536
|
|
80
|
-
|
|
81
|
-
if expected_count < min_glyph_count
|
|
82
|
-
issues << {
|
|
83
|
-
severity: "error",
|
|
84
|
-
category: "structure",
|
|
85
|
-
message: "Glyph count (#{expected_count}) below minimum (#{min_glyph_count})",
|
|
86
|
-
location: "maxp table",
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
if expected_count > max_glyph_count
|
|
91
|
-
issues << {
|
|
92
|
-
severity: "error",
|
|
93
|
-
category: "structure",
|
|
94
|
-
message: "Glyph count (#{expected_count}) exceeds maximum (#{max_glyph_count})",
|
|
95
|
-
location: "maxp table",
|
|
96
|
-
}
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
issues
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Check that table offsets are valid
|
|
103
|
-
#
|
|
104
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
105
|
-
# @return [Array<Hash>] Array of offset issues
|
|
106
|
-
def check_table_offsets(font)
|
|
107
|
-
issues = []
|
|
108
|
-
|
|
109
|
-
min_offset = @structure_config["min_table_offset"] || 12
|
|
110
|
-
max_size = @structure_config["max_table_size"] || 104857600
|
|
111
|
-
|
|
112
|
-
font.tables.each do |table_entry|
|
|
113
|
-
tag = table_entry.tag
|
|
114
|
-
offset = table_entry.offset
|
|
115
|
-
length = table_entry.table_length
|
|
116
|
-
|
|
117
|
-
# Check minimum offset
|
|
118
|
-
if offset < min_offset
|
|
119
|
-
issues << {
|
|
120
|
-
severity: "error",
|
|
121
|
-
category: "structure",
|
|
122
|
-
message: "Table '#{tag}' has invalid offset: #{offset} (minimum: #{min_offset})",
|
|
123
|
-
location: "#{tag} table directory",
|
|
124
|
-
}
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Check for reasonable table size
|
|
128
|
-
if length > max_size
|
|
129
|
-
issues << {
|
|
130
|
-
severity: "warning",
|
|
131
|
-
category: "structure",
|
|
132
|
-
message: "Table '#{tag}' has unusually large size: #{length} bytes",
|
|
133
|
-
location: "#{tag} table",
|
|
134
|
-
}
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Check alignment (tables should be 4-byte aligned)
|
|
138
|
-
alignment = @structure_config["table_alignment"] || 4
|
|
139
|
-
if offset % alignment != 0
|
|
140
|
-
issues << {
|
|
141
|
-
severity: "warning",
|
|
142
|
-
category: "structure",
|
|
143
|
-
message: "Table '#{tag}' is not #{alignment}-byte aligned (offset: #{offset})",
|
|
144
|
-
location: "#{tag} table directory",
|
|
145
|
-
}
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
issues
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Check table ordering (optimization check, not critical)
|
|
153
|
-
#
|
|
154
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
155
|
-
# @return [Array<Hash>] Array of ordering issues
|
|
156
|
-
def check_table_ordering(font)
|
|
157
|
-
issues = []
|
|
158
|
-
|
|
159
|
-
# Recommended table order for optimal loading
|
|
160
|
-
recommended_order = [
|
|
161
|
-
Constants::HEAD_TAG,
|
|
162
|
-
Constants::HHEA_TAG,
|
|
163
|
-
Constants::MAXP_TAG,
|
|
164
|
-
Constants::OS2_TAG,
|
|
165
|
-
Constants::NAME_TAG,
|
|
166
|
-
Constants::CMAP_TAG,
|
|
167
|
-
Constants::POST_TAG,
|
|
168
|
-
Constants::GLYF_TAG,
|
|
169
|
-
Constants::LOCA_TAG,
|
|
170
|
-
Constants::HMTX_TAG,
|
|
171
|
-
]
|
|
172
|
-
|
|
173
|
-
# Get actual table order
|
|
174
|
-
actual_order = font.table_names
|
|
175
|
-
|
|
176
|
-
# Check if critical tables are in recommended order
|
|
177
|
-
critical_tables = recommended_order.take(7) # head through post
|
|
178
|
-
actual_critical = actual_order.select do |tag|
|
|
179
|
-
critical_tables.include?(tag)
|
|
180
|
-
end
|
|
181
|
-
expected_critical = critical_tables.select do |tag|
|
|
182
|
-
actual_order.include?(tag)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
if actual_critical != expected_critical
|
|
186
|
-
issues << {
|
|
187
|
-
severity: "info",
|
|
188
|
-
category: "structure",
|
|
189
|
-
message: "Tables not in optimal order for performance",
|
|
190
|
-
location: nil,
|
|
191
|
-
}
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
issues
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
module Validation
|
|
7
|
-
# TableValidator validates the presence and correctness of font tables
|
|
8
|
-
#
|
|
9
|
-
# This validator checks that all required tables are present in the font
|
|
10
|
-
# based on the font type (TrueType, OpenType/CFF, Variable) and validates
|
|
11
|
-
# table-specific properties like versioning.
|
|
12
|
-
#
|
|
13
|
-
# Single Responsibility: Table presence and table-level validation
|
|
14
|
-
#
|
|
15
|
-
# @example Validating tables
|
|
16
|
-
# validator = TableValidator.new(rules)
|
|
17
|
-
# issues = validator.validate(font)
|
|
18
|
-
class TableValidator
|
|
19
|
-
# Initialize table validator
|
|
20
|
-
#
|
|
21
|
-
# @param rules [Hash] Validation rules configuration
|
|
22
|
-
def initialize(rules)
|
|
23
|
-
@rules = rules
|
|
24
|
-
@required_tables = rules["required_tables"]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Validate font tables
|
|
28
|
-
#
|
|
29
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
30
|
-
# @return [Array<Hash>] Array of validation issues
|
|
31
|
-
def validate(font)
|
|
32
|
-
issues = []
|
|
33
|
-
|
|
34
|
-
# Determine font type
|
|
35
|
-
font_type = determine_font_type(font)
|
|
36
|
-
|
|
37
|
-
# Check required tables based on font type
|
|
38
|
-
issues.concat(check_required_tables(font, font_type))
|
|
39
|
-
|
|
40
|
-
# Check table-specific validations if tables exist
|
|
41
|
-
issues.concat(check_table_versions(font)) if @rules["check_table_versions"]
|
|
42
|
-
|
|
43
|
-
issues
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
# Determine the font type
|
|
49
|
-
#
|
|
50
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
51
|
-
# @return [Symbol] :truetype, :opentype_cff, or :variable
|
|
52
|
-
def determine_font_type(font)
|
|
53
|
-
if font.has_table?(Constants::FVAR_TAG)
|
|
54
|
-
:variable
|
|
55
|
-
elsif font.has_table?(Constants::CFF_TAG) || font.has_table?("CFF2")
|
|
56
|
-
:opentype_cff
|
|
57
|
-
else
|
|
58
|
-
:truetype
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Check that required tables are present
|
|
63
|
-
#
|
|
64
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
65
|
-
# @param font_type [Symbol] The font type
|
|
66
|
-
# @return [Array<Hash>] Array of missing table issues
|
|
67
|
-
def check_required_tables(font, font_type)
|
|
68
|
-
issues = []
|
|
69
|
-
|
|
70
|
-
# Get required tables for this font type
|
|
71
|
-
required = @required_tables["all"].dup
|
|
72
|
-
|
|
73
|
-
case font_type
|
|
74
|
-
when :truetype
|
|
75
|
-
required.concat(@required_tables["truetype"])
|
|
76
|
-
when :opentype_cff
|
|
77
|
-
required.concat(@required_tables["opentype_cff"])
|
|
78
|
-
when :variable
|
|
79
|
-
required.concat(@required_tables["variable"])
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Check each required table
|
|
83
|
-
required.each do |table_tag|
|
|
84
|
-
next if font.has_table?(table_tag)
|
|
85
|
-
|
|
86
|
-
# Special case: CFF or CFF2 are alternatives
|
|
87
|
-
if (table_tag == Constants::CFF_TAG) && font.has_table?("CFF2")
|
|
88
|
-
next
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
issues << {
|
|
92
|
-
severity: "error",
|
|
93
|
-
category: "tables",
|
|
94
|
-
message: "Missing required table: #{table_tag}",
|
|
95
|
-
location: nil,
|
|
96
|
-
}
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
issues
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Check table version compatibility
|
|
103
|
-
#
|
|
104
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
105
|
-
# @return [Array<Hash>] Array of version issues
|
|
106
|
-
def check_table_versions(font)
|
|
107
|
-
issues = []
|
|
108
|
-
|
|
109
|
-
# Check head table version
|
|
110
|
-
if font.has_table?(Constants::HEAD_TAG)
|
|
111
|
-
head = font.table(Constants::HEAD_TAG)
|
|
112
|
-
unless valid_head_version?(head)
|
|
113
|
-
issues << {
|
|
114
|
-
severity: "warning",
|
|
115
|
-
category: "tables",
|
|
116
|
-
message: "Unsupported head table version: #{head.major_version}.#{head.minor_version}",
|
|
117
|
-
location: Constants::HEAD_TAG,
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Check maxp table version
|
|
123
|
-
if font.has_table?(Constants::MAXP_TAG)
|
|
124
|
-
maxp = font.table(Constants::MAXP_TAG)
|
|
125
|
-
unless valid_maxp_version?(maxp)
|
|
126
|
-
issues << {
|
|
127
|
-
severity: "warning",
|
|
128
|
-
category: "tables",
|
|
129
|
-
message: "Unsupported maxp table version: #{maxp.version}",
|
|
130
|
-
location: Constants::MAXP_TAG,
|
|
131
|
-
}
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
issues
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Check if head table version is valid
|
|
139
|
-
#
|
|
140
|
-
# @param head [Tables::Head] The head table
|
|
141
|
-
# @return [Boolean] true if version is valid
|
|
142
|
-
def valid_head_version?(head)
|
|
143
|
-
# Head table version should be 1.0
|
|
144
|
-
head.major_version == 1 && head.minor_version.zero?
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Check if maxp table version is valid
|
|
148
|
-
#
|
|
149
|
-
# @param maxp [Tables::Maxp] The maxp table
|
|
150
|
-
# @return [Boolean] true if version is valid
|
|
151
|
-
def valid_maxp_version?(maxp)
|
|
152
|
-
# Version 0.5 for CFF fonts, 1.0 for TrueType fonts
|
|
153
|
-
version = maxp.version
|
|
154
|
-
[0x00005000, 0x00010000].include?(version)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
require_relative "../models/validation_report"
|
|
5
|
-
require_relative "table_validator"
|
|
6
|
-
require_relative "structure_validator"
|
|
7
|
-
require_relative "consistency_validator"
|
|
8
|
-
require_relative "checksum_validator"
|
|
9
|
-
|
|
10
|
-
module Fontisan
|
|
11
|
-
module Validation
|
|
12
|
-
# Validator is the main orchestrator for font validation
|
|
13
|
-
#
|
|
14
|
-
# This class coordinates all validation checks (tables, structure,
|
|
15
|
-
# consistency, checksums) and produces a comprehensive ValidationReport.
|
|
16
|
-
#
|
|
17
|
-
# Single Responsibility: Orchestration of validation workflow
|
|
18
|
-
#
|
|
19
|
-
# @example Validating a font
|
|
20
|
-
# validator = Validator.new(level: :standard)
|
|
21
|
-
# report = validator.validate(font, font_path)
|
|
22
|
-
# puts report.text_summary
|
|
23
|
-
class Validator
|
|
24
|
-
# Validation levels
|
|
25
|
-
LEVELS = %i[strict standard lenient].freeze
|
|
26
|
-
|
|
27
|
-
# Initialize validator
|
|
28
|
-
#
|
|
29
|
-
# @param level [Symbol] Validation level (:strict, :standard, :lenient)
|
|
30
|
-
# @param rules_path [String, nil] Path to custom rules file
|
|
31
|
-
def initialize(level: :standard, rules_path: nil)
|
|
32
|
-
@level = level
|
|
33
|
-
validate_level!
|
|
34
|
-
|
|
35
|
-
@rules = load_rules(rules_path)
|
|
36
|
-
@table_validator = TableValidator.new(@rules)
|
|
37
|
-
@structure_validator = StructureValidator.new(@rules)
|
|
38
|
-
@consistency_validator = ConsistencyValidator.new(@rules)
|
|
39
|
-
@checksum_validator = ChecksumValidator.new(@rules)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Validate a font
|
|
43
|
-
#
|
|
44
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
45
|
-
# @param font_path [String] Path to the font file
|
|
46
|
-
# @return [Models::ValidationReport] Validation report
|
|
47
|
-
def validate(font, font_path)
|
|
48
|
-
report = Models::ValidationReport.new(
|
|
49
|
-
font_path: font_path,
|
|
50
|
-
valid: true,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
begin
|
|
54
|
-
# Run all validation checks
|
|
55
|
-
all_issues = []
|
|
56
|
-
|
|
57
|
-
# 1. Table validation
|
|
58
|
-
all_issues.concat(@table_validator.validate(font))
|
|
59
|
-
|
|
60
|
-
# 2. Structure validation
|
|
61
|
-
all_issues.concat(@structure_validator.validate(font))
|
|
62
|
-
|
|
63
|
-
# 3. Consistency validation
|
|
64
|
-
all_issues.concat(@consistency_validator.validate(font))
|
|
65
|
-
|
|
66
|
-
# 4. Checksum validation (requires file path)
|
|
67
|
-
all_issues.concat(@checksum_validator.validate(font, font_path))
|
|
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("validation", "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
|
-
raise "Validation rules file not found: #{path}"
|
|
121
|
-
rescue Psych::SyntaxError => e
|
|
122
|
-
raise "Invalid validation rules YAML: #{e.message}"
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Get the default rules path
|
|
126
|
-
#
|
|
127
|
-
# @return [String] Path to default rules file
|
|
128
|
-
def default_rules_path
|
|
129
|
-
File.join(__dir__, "..", "config", "validation_rules.yml")
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Determine if font is valid based on validation level
|
|
133
|
-
#
|
|
134
|
-
# @param report [Models::ValidationReport] The validation report
|
|
135
|
-
# @return [Boolean] true if font is valid for the given level
|
|
136
|
-
def determine_validity(report)
|
|
137
|
-
case @level
|
|
138
|
-
when :strict
|
|
139
|
-
# Strict: no errors, no warnings
|
|
140
|
-
!report.has_errors? && !report.has_warnings?
|
|
141
|
-
when :standard
|
|
142
|
-
# Standard: no errors (warnings allowed)
|
|
143
|
-
!report.has_errors?
|
|
144
|
-
when :lenient
|
|
145
|
-
# Lenient: no critical errors (some errors may be acceptable)
|
|
146
|
-
# For now, treat lenient same as standard
|
|
147
|
-
!report.has_errors?
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|