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,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Validation
|
|
5
|
+
# VariableFontValidator validates variable font structure
|
|
6
|
+
#
|
|
7
|
+
# Validates:
|
|
8
|
+
# - fvar table structure
|
|
9
|
+
# - Axis definitions and ranges
|
|
10
|
+
# - Instance definitions
|
|
11
|
+
# - Variation table consistency
|
|
12
|
+
# - Metrics variation tables
|
|
13
|
+
#
|
|
14
|
+
# @example Validate a variable font
|
|
15
|
+
# validator = VariableFontValidator.new(font)
|
|
16
|
+
# errors = validator.validate
|
|
17
|
+
# puts "Found #{errors.length} errors" if errors.any?
|
|
18
|
+
class VariableFontValidator
|
|
19
|
+
# Initialize validator with font
|
|
20
|
+
#
|
|
21
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
22
|
+
def initialize(font)
|
|
23
|
+
@font = font
|
|
24
|
+
@errors = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Validate variable font
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<String>] Array of error messages
|
|
30
|
+
def validate
|
|
31
|
+
return [] unless @font.has_table?("fvar")
|
|
32
|
+
|
|
33
|
+
validate_fvar_structure
|
|
34
|
+
validate_axes
|
|
35
|
+
validate_instances
|
|
36
|
+
validate_variation_tables
|
|
37
|
+
validate_metrics_variation
|
|
38
|
+
|
|
39
|
+
@errors
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Validate fvar table structure
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
47
|
+
def validate_fvar_structure
|
|
48
|
+
fvar = @font.table("fvar")
|
|
49
|
+
return unless fvar
|
|
50
|
+
|
|
51
|
+
if !fvar.respond_to?(:axes) || fvar.axes.nil? || fvar.axes.empty?
|
|
52
|
+
@errors << "fvar: No axes defined"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if fvar.respond_to?(:axis_count) && fvar.axis_count != fvar.axes.length
|
|
57
|
+
@errors << "fvar: Axis count mismatch (expected #{fvar.axis_count}, got #{fvar.axes.length})"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate all axes
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
def validate_axes
|
|
65
|
+
fvar = @font.table("fvar")
|
|
66
|
+
return unless fvar.respond_to?(:axes)
|
|
67
|
+
|
|
68
|
+
fvar.axes.each_with_index do |axis, index|
|
|
69
|
+
validate_axis_range(axis, index)
|
|
70
|
+
validate_axis_tag(axis, index)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Validate axis range values
|
|
75
|
+
#
|
|
76
|
+
# @param axis [Object] Axis object
|
|
77
|
+
# @param index [Integer] Axis index
|
|
78
|
+
# @return [void]
|
|
79
|
+
def validate_axis_range(axis, index)
|
|
80
|
+
return unless axis.respond_to?(:min_value) && axis.respond_to?(:max_value)
|
|
81
|
+
|
|
82
|
+
if axis.min_value > axis.max_value
|
|
83
|
+
tag = axis.respond_to?(:axis_tag) ? axis.axis_tag : "axis #{index}"
|
|
84
|
+
@errors << "Axis #{tag}: min_value (#{axis.min_value}) > max_value (#{axis.max_value})"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if axis.respond_to?(:default_value) && (axis.default_value < axis.min_value || axis.default_value > axis.max_value)
|
|
88
|
+
tag = axis.respond_to?(:axis_tag) ? axis.axis_tag : "axis #{index}"
|
|
89
|
+
@errors << "Axis #{tag}: default_value (#{axis.default_value}) out of range [#{axis.min_value}, #{axis.max_value}]"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate axis tag format
|
|
94
|
+
#
|
|
95
|
+
# @param axis [Object] Axis object
|
|
96
|
+
# @param index [Integer] Axis index
|
|
97
|
+
# @return [void]
|
|
98
|
+
def validate_axis_tag(axis, index)
|
|
99
|
+
return unless axis.respond_to?(:axis_tag)
|
|
100
|
+
|
|
101
|
+
tag = axis.axis_tag
|
|
102
|
+
unless tag.is_a?(String) && tag.length == 4 && tag =~ /^[a-zA-Z]{4}$/
|
|
103
|
+
@errors << "Axis #{index}: invalid tag '#{tag}' (must be 4 ASCII letters)"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validate named instances
|
|
108
|
+
#
|
|
109
|
+
# @return [void]
|
|
110
|
+
def validate_instances
|
|
111
|
+
fvar = @font.table("fvar")
|
|
112
|
+
return unless fvar.respond_to?(:instances)
|
|
113
|
+
return unless fvar.instances
|
|
114
|
+
|
|
115
|
+
fvar.instances.each_with_index do |instance, idx|
|
|
116
|
+
validate_instance_coordinates(instance, idx, fvar)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Validate instance coordinates
|
|
121
|
+
#
|
|
122
|
+
# @param instance [Object] Instance object
|
|
123
|
+
# @param idx [Integer] Instance index
|
|
124
|
+
# @param fvar [Object] fvar table
|
|
125
|
+
# @return [void]
|
|
126
|
+
def validate_instance_coordinates(instance, idx, fvar)
|
|
127
|
+
return unless instance.is_a?(Hash) && instance[:coordinates]
|
|
128
|
+
|
|
129
|
+
coords = instance[:coordinates]
|
|
130
|
+
axis_count = fvar.respond_to?(:axis_count) ? fvar.axis_count : fvar.axes.length
|
|
131
|
+
|
|
132
|
+
if coords.length != axis_count
|
|
133
|
+
@errors << "Instance #{idx}: coordinate count mismatch (expected #{axis_count}, got #{coords.length})"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
coords.each_with_index do |value, axis_idx|
|
|
137
|
+
next if axis_idx >= fvar.axes.length
|
|
138
|
+
|
|
139
|
+
axis = fvar.axes[axis_idx]
|
|
140
|
+
next unless axis.respond_to?(:min_value) && axis.respond_to?(:max_value)
|
|
141
|
+
|
|
142
|
+
if value < axis.min_value || value > axis.max_value
|
|
143
|
+
tag = axis.respond_to?(:axis_tag) ? axis.axis_tag : "axis #{axis_idx}"
|
|
144
|
+
@errors << "Instance #{idx}: coordinate for #{tag} (#{value}) out of range [#{axis.min_value}, #{axis.max_value}]"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Validate variation tables
|
|
150
|
+
#
|
|
151
|
+
# @return [void]
|
|
152
|
+
def validate_variation_tables
|
|
153
|
+
has_gvar = @font.has_table?("gvar")
|
|
154
|
+
has_cff2 = @font.has_table?("CFF2")
|
|
155
|
+
has_glyf = @font.has_table?("glyf")
|
|
156
|
+
has_cff = @font.has_table?("CFF ")
|
|
157
|
+
|
|
158
|
+
# TrueType variable fonts should have gvar
|
|
159
|
+
if has_glyf && !has_gvar
|
|
160
|
+
@errors << "TrueType variable font missing gvar table"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# CFF variable fonts should have CFF2
|
|
164
|
+
if has_cff && !has_cff2
|
|
165
|
+
@errors << "CFF variable font missing CFF2 table"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Can't have both gvar and CFF2
|
|
169
|
+
if has_gvar && has_cff2
|
|
170
|
+
@errors << "Font has both gvar and CFF2 tables (incompatible)"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Validate metrics variation tables
|
|
175
|
+
#
|
|
176
|
+
# @return [void]
|
|
177
|
+
def validate_metrics_variation
|
|
178
|
+
validate_hvar if @font.has_table?("HVAR")
|
|
179
|
+
validate_vvar if @font.has_table?("VVAR")
|
|
180
|
+
validate_mvar if @font.has_table?("MVAR")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Validate HVAR table
|
|
184
|
+
#
|
|
185
|
+
# @return [void]
|
|
186
|
+
def validate_hvar
|
|
187
|
+
# HVAR validation would go here
|
|
188
|
+
# For now, just check it exists
|
|
189
|
+
hvar = @font.table_data["HVAR"]
|
|
190
|
+
if hvar.nil? || hvar.empty?
|
|
191
|
+
@errors << "HVAR table is empty"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Validate VVAR table
|
|
196
|
+
#
|
|
197
|
+
# @return [void]
|
|
198
|
+
def validate_vvar
|
|
199
|
+
# VVAR validation would go here
|
|
200
|
+
vvar = @font.table_data["VVAR"]
|
|
201
|
+
if vvar.nil? || vvar.empty?
|
|
202
|
+
@errors << "VVAR table is empty"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Validate MVAR table
|
|
207
|
+
#
|
|
208
|
+
# @return [void]
|
|
209
|
+
def validate_mvar
|
|
210
|
+
# MVAR validation would go here
|
|
211
|
+
mvar = @font.table_data["MVAR"]
|
|
212
|
+
if mvar.nil? || mvar.empty?
|
|
213
|
+
@errors << "MVAR table is empty"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
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
|