fontisan 0.1.0 → 0.2.0
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 +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- 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 +309 -0
- data/lib/fontisan/collection/builder.rb +260 -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 +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -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 +295 -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 +178 -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 +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -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 +121 -12
- data/lib/fontisan/font_writer.rb +301 -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 +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -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 +233 -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 +296 -10
- 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/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 +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -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 +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -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/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -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.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -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 +270 -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 +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -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/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 +268 -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/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/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -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 == "CFF") && 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
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variable
|
|
7
|
+
# Normalizes user coordinates to design space
|
|
8
|
+
#
|
|
9
|
+
# Converts user-provided axis coordinates (e.g., wght=700) to normalized
|
|
10
|
+
# values in the range -1.0 to 1.0 based on axis definitions from the fvar table.
|
|
11
|
+
#
|
|
12
|
+
# The normalization algorithm follows the OpenType specification:
|
|
13
|
+
# - For values below default: normalized = (value - default) / (default - min)
|
|
14
|
+
# - For values above default: normalized = (value - default) / (max - default)
|
|
15
|
+
# - Values are clamped to the -1.0 to 1.0 range
|
|
16
|
+
#
|
|
17
|
+
# @example Normalize coordinates
|
|
18
|
+
# normalizer = AxisNormalizer.new(fvar_table)
|
|
19
|
+
# normalized = normalizer.normalize({ "wght" => 700, "wdth" => 100 })
|
|
20
|
+
# # => { "wght" => 0.5, "wdth" => 0.0 }
|
|
21
|
+
class AxisNormalizer
|
|
22
|
+
# @return [Hash] Configuration settings
|
|
23
|
+
attr_reader :config
|
|
24
|
+
|
|
25
|
+
# @return [Hash] Axis definitions from fvar table
|
|
26
|
+
attr_reader :axes
|
|
27
|
+
|
|
28
|
+
# Initialize the normalizer
|
|
29
|
+
#
|
|
30
|
+
# @param fvar [Fontisan::Tables::Fvar] Font variations table
|
|
31
|
+
# @param config [Hash] Optional configuration overrides
|
|
32
|
+
def initialize(fvar, config = {})
|
|
33
|
+
@fvar = fvar
|
|
34
|
+
@config = load_config.merge(config)
|
|
35
|
+
@axes = build_axis_map
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Normalize user coordinates to design space
|
|
39
|
+
#
|
|
40
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates by axis tag
|
|
41
|
+
# @return [Hash<String, Float>] Normalized coordinates (-1.0 to 1.0)
|
|
42
|
+
def normalize(user_coords)
|
|
43
|
+
result = {}
|
|
44
|
+
|
|
45
|
+
@axes.each do |tag, axis_info|
|
|
46
|
+
user_value = user_coords[tag] || user_coords[tag.to_sym]
|
|
47
|
+
|
|
48
|
+
# Use default if not provided and config allows
|
|
49
|
+
if user_value.nil?
|
|
50
|
+
user_value = if @config.dig(:coordinate_normalization,
|
|
51
|
+
:use_axis_defaults)
|
|
52
|
+
axis_info[:default]
|
|
53
|
+
else
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate and clamp if configured
|
|
59
|
+
validated_value = validate_coordinate(user_value, axis_info)
|
|
60
|
+
|
|
61
|
+
# Normalize the value
|
|
62
|
+
normalized = normalize_value(validated_value, axis_info)
|
|
63
|
+
|
|
64
|
+
result[tag] = normalized
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Normalize a single axis value
|
|
71
|
+
#
|
|
72
|
+
# @param value [Numeric] User coordinate value
|
|
73
|
+
# @param axis_tag [String] Axis tag
|
|
74
|
+
# @return [Float] Normalized value (-1.0 to 1.0)
|
|
75
|
+
def normalize_axis(value, axis_tag)
|
|
76
|
+
axis_info = @axes[axis_tag]
|
|
77
|
+
raise ArgumentError, "Unknown axis: #{axis_tag}" unless axis_info
|
|
78
|
+
|
|
79
|
+
validated_value = validate_coordinate(value, axis_info)
|
|
80
|
+
normalize_value(validated_value, axis_info)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get axis information
|
|
84
|
+
#
|
|
85
|
+
# @param axis_tag [String] Axis tag
|
|
86
|
+
# @return [Hash, nil] Axis information or nil
|
|
87
|
+
def axis_info(axis_tag)
|
|
88
|
+
@axes[axis_tag]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get all axis tags
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<String>] Array of axis tags
|
|
94
|
+
def axis_tags
|
|
95
|
+
@axes.keys
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Load configuration from YAML file
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash] Configuration hash
|
|
103
|
+
def load_config
|
|
104
|
+
config_path = File.join(__dir__, "..", "config",
|
|
105
|
+
"variable_settings.yml")
|
|
106
|
+
loaded = YAML.load_file(config_path)
|
|
107
|
+
# Convert string keys to symbol keys for consistency
|
|
108
|
+
deep_symbolize_keys(loaded)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
# Return default config if file doesn't exist
|
|
111
|
+
{
|
|
112
|
+
coordinate_normalization: {
|
|
113
|
+
normalize: true,
|
|
114
|
+
use_axis_defaults: true,
|
|
115
|
+
normalized_precision: 6,
|
|
116
|
+
},
|
|
117
|
+
delta_application: {
|
|
118
|
+
validate_coordinates: true,
|
|
119
|
+
clamp_coordinates: true,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Recursively convert hash keys to symbols
|
|
125
|
+
#
|
|
126
|
+
# @param hash [Hash] Hash with string keys
|
|
127
|
+
# @return [Hash] Hash with symbol keys
|
|
128
|
+
def deep_symbolize_keys(hash)
|
|
129
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
130
|
+
new_key = key.to_sym
|
|
131
|
+
new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
|
|
132
|
+
result[new_key] = new_value
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Build axis information map from fvar table
|
|
137
|
+
#
|
|
138
|
+
# @return [Hash<String, Hash>] Map of axis tag to axis info
|
|
139
|
+
def build_axis_map
|
|
140
|
+
return {} unless @fvar
|
|
141
|
+
|
|
142
|
+
@fvar.axes.each_with_object({}) do |axis, hash|
|
|
143
|
+
# Convert BinData::String to regular Ruby String for proper Hash key behavior
|
|
144
|
+
tag = axis.axis_tag.to_s
|
|
145
|
+
hash[tag] = {
|
|
146
|
+
min: axis.min_value,
|
|
147
|
+
default: axis.default_value,
|
|
148
|
+
max: axis.max_value,
|
|
149
|
+
name_id: axis.axis_name_id,
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Validate and optionally clamp coordinate value
|
|
155
|
+
#
|
|
156
|
+
# @param value [Numeric] User coordinate value
|
|
157
|
+
# @param axis_info [Hash] Axis information
|
|
158
|
+
# @return [Float] Validated value
|
|
159
|
+
def validate_coordinate(value, axis_info)
|
|
160
|
+
value = value.to_f
|
|
161
|
+
|
|
162
|
+
# Check if validation is enabled
|
|
163
|
+
if @config.dig(:delta_application, :validate_coordinates)
|
|
164
|
+
min = axis_info[:min]
|
|
165
|
+
max = axis_info[:max]
|
|
166
|
+
|
|
167
|
+
# Clamp if configured
|
|
168
|
+
if @config.dig(:delta_application, :clamp_coordinates)
|
|
169
|
+
value = [[value, min].max, max].min
|
|
170
|
+
elsif value < min || value > max
|
|
171
|
+
raise ArgumentError,
|
|
172
|
+
"Coordinate #{value} out of range [#{min}, #{max}]"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
value
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Normalize a value to -1.0 to 1.0 range
|
|
180
|
+
#
|
|
181
|
+
# @param value [Float] User coordinate value
|
|
182
|
+
# @param axis_info [Hash] Axis information
|
|
183
|
+
# @return [Float] Normalized value
|
|
184
|
+
def normalize_value(value, axis_info)
|
|
185
|
+
default = axis_info[:default]
|
|
186
|
+
|
|
187
|
+
# Value at default is always 0.0
|
|
188
|
+
return 0.0 if (value - default).abs < Float::EPSILON
|
|
189
|
+
|
|
190
|
+
if value < default
|
|
191
|
+
# Below default: negative range
|
|
192
|
+
min = axis_info[:min]
|
|
193
|
+
range = default - min
|
|
194
|
+
|
|
195
|
+
else
|
|
196
|
+
# Above default: positive range
|
|
197
|
+
max = axis_info[:max]
|
|
198
|
+
range = max - default
|
|
199
|
+
|
|
200
|
+
end
|
|
201
|
+
return 0.0 if range.abs < Float::EPSILON
|
|
202
|
+
|
|
203
|
+
normalized = (value - default) / range
|
|
204
|
+
|
|
205
|
+
# Clamp to -1.0 to 1.0
|
|
206
|
+
normalized = [[-1.0, normalized].max, 1.0].min
|
|
207
|
+
|
|
208
|
+
# Apply precision
|
|
209
|
+
precision = @config.dig(:coordinate_normalization,
|
|
210
|
+
:normalized_precision) || 6
|
|
211
|
+
normalized.round(precision)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|