fontisan 0.1.0 → 0.2.1
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 +672 -69
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1477 -297
- data/Rakefile +63 -41
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +364 -4
- data/lib/fontisan/collection/builder.rb +341 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +317 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +24 -1
- data/lib/fontisan/commands/convert_command.rb +218 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +286 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +203 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +79 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +408 -0
- data/lib/fontisan/converters/outline_converter.rb +998 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +122 -15
- data/lib/fontisan/font_writer.rb +302 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +310 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
- data/lib/fontisan/loading_modes.rb +115 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +405 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +321 -19
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +934 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +489 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +346 -0
- data/lib/fontisan/tables/cvar.rb +203 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +231 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +321 -20
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +375 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/variation/variation_preserver.rb +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +717 -0
- data/lib/fontisan/woff_font.rb +488 -0
- data/lib/fontisan.rb +132 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +234 -4
|
@@ -0,0 +1,198 @@
|
|
|
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
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|