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,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
# Provides unified table access for variation classes
|
|
6
|
+
#
|
|
7
|
+
# This module centralizes table loading logic with optional caching
|
|
8
|
+
# and consistent error handling. It should be included in variation
|
|
9
|
+
# classes that need to access font tables.
|
|
10
|
+
#
|
|
11
|
+
# @example Using TableAccessor in a variation class
|
|
12
|
+
# class MyVariationClass
|
|
13
|
+
# include TableAccessor
|
|
14
|
+
#
|
|
15
|
+
# def initialize(font)
|
|
16
|
+
# @font = font
|
|
17
|
+
# @variation_tables = {}
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def process
|
|
21
|
+
# gvar = variation_table("gvar")
|
|
22
|
+
# fvar = require_variation_table("fvar")
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
module TableAccessor
|
|
26
|
+
# Get a variation table with optional caching
|
|
27
|
+
#
|
|
28
|
+
# Loads and optionally caches a font table. Returns nil if table
|
|
29
|
+
# doesn't exist. Use when table presence is optional.
|
|
30
|
+
#
|
|
31
|
+
# @param tag [String] Table tag (e.g., "gvar", "fvar")
|
|
32
|
+
# @param lazy [Boolean] Enable lazy loading (default: true)
|
|
33
|
+
# @return [Object, nil] Parsed table object or nil
|
|
34
|
+
#
|
|
35
|
+
# @example Get optional table
|
|
36
|
+
# gvar = variation_table("gvar")
|
|
37
|
+
# return unless gvar
|
|
38
|
+
def variation_table(tag, lazy: true)
|
|
39
|
+
# Return cached table if available
|
|
40
|
+
return @variation_tables[tag] if @variation_tables&.key?(tag)
|
|
41
|
+
|
|
42
|
+
# Check table exists
|
|
43
|
+
return nil unless @font.has_table?(tag)
|
|
44
|
+
|
|
45
|
+
# Initialize cache if needed
|
|
46
|
+
@variation_tables ||= {}
|
|
47
|
+
|
|
48
|
+
# Load and cache table
|
|
49
|
+
@variation_tables[tag] = @font.table(tag)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get a required variation table
|
|
53
|
+
#
|
|
54
|
+
# Loads a table that must exist. Raises error if table is missing.
|
|
55
|
+
# Use when table presence is required for operation.
|
|
56
|
+
#
|
|
57
|
+
# @param tag [String] Table tag
|
|
58
|
+
# @return [Object] Parsed table object
|
|
59
|
+
# @raise [MissingVariationTableError] If table doesn't exist
|
|
60
|
+
#
|
|
61
|
+
# @example Require table
|
|
62
|
+
# fvar = require_variation_table("fvar")
|
|
63
|
+
# # Guaranteed to have fvar or error raised
|
|
64
|
+
def require_variation_table(tag)
|
|
65
|
+
table = variation_table(tag)
|
|
66
|
+
return table if table
|
|
67
|
+
|
|
68
|
+
raise MissingVariationTableError.new(
|
|
69
|
+
table: tag,
|
|
70
|
+
message: "Required variation table '#{tag}' not found in font",
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if variation table exists
|
|
75
|
+
#
|
|
76
|
+
# @param tag [String] Table tag
|
|
77
|
+
# @return [Boolean] True if table exists
|
|
78
|
+
#
|
|
79
|
+
# @example Check table presence
|
|
80
|
+
# if has_variation_table?("gvar")
|
|
81
|
+
# # Process gvar
|
|
82
|
+
# end
|
|
83
|
+
def has_variation_table?(tag)
|
|
84
|
+
@font.has_table?(tag)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Clear variation table cache
|
|
88
|
+
#
|
|
89
|
+
# Useful when font tables are modified and need to be reloaded.
|
|
90
|
+
#
|
|
91
|
+
# @return [void]
|
|
92
|
+
def clear_variation_cache
|
|
93
|
+
@variation_tables&.clear
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Clear specific cached table
|
|
97
|
+
#
|
|
98
|
+
# @param tag [String] Table tag to clear
|
|
99
|
+
# @return [void]
|
|
100
|
+
def clear_variation_table(tag)
|
|
101
|
+
@variation_tables&.delete(tag)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
# Validates variable font structure and consistency
|
|
6
|
+
#
|
|
7
|
+
# This class performs comprehensive validation of variable font tables
|
|
8
|
+
# to ensure structural integrity and catch common issues before instance
|
|
9
|
+
# generation or subsetting operations.
|
|
10
|
+
#
|
|
11
|
+
# Validation checks:
|
|
12
|
+
# 1. Table consistency - Verify axis counts match across tables
|
|
13
|
+
# 2. Delta integrity - Check delta sets are complete
|
|
14
|
+
# 3. Region coverage - Ensure regions cover design space
|
|
15
|
+
# 4. Instance definitions - Validate instance coordinates
|
|
16
|
+
#
|
|
17
|
+
# @example Validating a variable font
|
|
18
|
+
# validator = Fontisan::Variation::Validator.new(font)
|
|
19
|
+
# report = validator.validate
|
|
20
|
+
# if report[:valid]
|
|
21
|
+
# puts "Font is valid"
|
|
22
|
+
# else
|
|
23
|
+
# report[:errors].each { |err| puts "Error: #{err}" }
|
|
24
|
+
# end
|
|
25
|
+
class Validator
|
|
26
|
+
# @return [TrueTypeFont, OpenTypeFont] Font being validated
|
|
27
|
+
attr_reader :font
|
|
28
|
+
|
|
29
|
+
# @return [Array<String>] Validation errors
|
|
30
|
+
attr_reader :errors
|
|
31
|
+
|
|
32
|
+
# @return [Array<String>] Validation warnings
|
|
33
|
+
attr_reader :warnings
|
|
34
|
+
|
|
35
|
+
# Initialize validator
|
|
36
|
+
#
|
|
37
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font to validate
|
|
38
|
+
def initialize(font)
|
|
39
|
+
@font = font
|
|
40
|
+
@errors = []
|
|
41
|
+
@warnings = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Perform full validation
|
|
45
|
+
#
|
|
46
|
+
# Runs all validation checks and returns a detailed report.
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash] Validation report with :valid, :errors, :warnings
|
|
49
|
+
def validate
|
|
50
|
+
@errors.clear
|
|
51
|
+
@warnings.clear
|
|
52
|
+
|
|
53
|
+
check_is_variable_font
|
|
54
|
+
check_table_consistency if @errors.empty?
|
|
55
|
+
check_delta_integrity if @errors.empty?
|
|
56
|
+
check_region_coverage if @errors.empty?
|
|
57
|
+
check_instance_definitions if @errors.empty?
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
valid: @errors.empty?,
|
|
61
|
+
errors: @errors.dup,
|
|
62
|
+
warnings: @warnings.dup,
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Quick validation (essential checks only)
|
|
67
|
+
#
|
|
68
|
+
# @return [Boolean] True if font passes basic validation
|
|
69
|
+
def valid?
|
|
70
|
+
validate[:valid]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Check if font is actually a variable font
|
|
76
|
+
def check_is_variable_font
|
|
77
|
+
unless @font.has_table?("fvar")
|
|
78
|
+
@errors << "Missing required 'fvar' table - not a variable font"
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
fvar = @font.table("fvar")
|
|
83
|
+
unless fvar&.axis_count&.positive?
|
|
84
|
+
@errors << "fvar table has no axes defined"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check consistency across all variation tables
|
|
89
|
+
def check_table_consistency
|
|
90
|
+
fvar = @font.table("fvar")
|
|
91
|
+
return unless fvar
|
|
92
|
+
|
|
93
|
+
axis_count = fvar.axis_count
|
|
94
|
+
|
|
95
|
+
# Check gvar axis count if present
|
|
96
|
+
if @font.has_table?("gvar")
|
|
97
|
+
gvar = @font.table("gvar")
|
|
98
|
+
if gvar && gvar.axis_count != axis_count
|
|
99
|
+
@errors << "gvar axis count (#{gvar.axis_count}) doesn't match fvar (#{axis_count})"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check CFF2 if present
|
|
104
|
+
if @font.has_table?("CFF2")
|
|
105
|
+
cff2 = @font.table("CFF2")
|
|
106
|
+
if cff2.respond_to?(:num_axes)
|
|
107
|
+
cff2_axes = cff2.num_axes || 0
|
|
108
|
+
if cff2_axes != axis_count && cff2_axes.positive?
|
|
109
|
+
@errors << "CFF2 axis count (#{cff2_axes}) doesn't match fvar (#{axis_count})"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check HVAR region count if present
|
|
115
|
+
check_metrics_table_consistency("HVAR", axis_count)
|
|
116
|
+
check_metrics_table_consistency("VVAR", axis_count)
|
|
117
|
+
check_metrics_table_consistency("MVAR", axis_count)
|
|
118
|
+
|
|
119
|
+
# Verify at least one variation table exists
|
|
120
|
+
has_outline_var = @font.has_table?("gvar") || @font.has_table?("CFF2")
|
|
121
|
+
has_metrics_var = @font.has_table?("HVAR") || @font.has_table?("VVAR") || @font.has_table?("MVAR")
|
|
122
|
+
|
|
123
|
+
unless has_outline_var || has_metrics_var
|
|
124
|
+
@warnings << "No variation tables found (gvar/CFF2/HVAR/VVAR/MVAR)"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check metrics table consistency
|
|
129
|
+
#
|
|
130
|
+
# @param table_tag [String] Table tag (HVAR, VVAR, MVAR)
|
|
131
|
+
# @param expected_axes [Integer] Expected axis count
|
|
132
|
+
def check_metrics_table_consistency(table_tag, expected_axes)
|
|
133
|
+
return unless @font.has_table?(table_tag)
|
|
134
|
+
|
|
135
|
+
table = @font.table(table_tag)
|
|
136
|
+
return unless table.respond_to?(:item_variation_store)
|
|
137
|
+
|
|
138
|
+
store = table.item_variation_store
|
|
139
|
+
return unless store
|
|
140
|
+
|
|
141
|
+
# Check region list axis count
|
|
142
|
+
if store.respond_to?(:region_list) && store.region_list
|
|
143
|
+
region_list = store.region_list
|
|
144
|
+
if region_list.respond_to?(:axis_count)
|
|
145
|
+
region_axes = region_list.axis_count
|
|
146
|
+
if region_axes != expected_axes
|
|
147
|
+
@errors << "#{table_tag} region axis count (#{region_axes}) doesn't match fvar (#{expected_axes})"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check delta integrity
|
|
154
|
+
def check_delta_integrity
|
|
155
|
+
# Check gvar delta completeness
|
|
156
|
+
if @font.has_table?("gvar") && @font.has_table?("maxp")
|
|
157
|
+
check_gvar_delta_integrity
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check HVAR delta coverage
|
|
161
|
+
if @font.has_table?("HVAR")
|
|
162
|
+
check_hvar_delta_integrity
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check gvar delta sets are complete
|
|
167
|
+
def check_gvar_delta_integrity
|
|
168
|
+
gvar = @font.table("gvar")
|
|
169
|
+
maxp = @font.table("maxp")
|
|
170
|
+
return unless gvar && maxp
|
|
171
|
+
|
|
172
|
+
glyph_count = maxp.num_glyphs
|
|
173
|
+
gvar_count = gvar.glyph_count
|
|
174
|
+
|
|
175
|
+
if gvar_count != glyph_count
|
|
176
|
+
@errors << "gvar glyph count (#{gvar_count}) doesn't match maxp (#{glyph_count})"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Sample check: verify first and last glyphs have accessible data
|
|
180
|
+
if gvar_count.positive?
|
|
181
|
+
first_data = gvar.glyph_variation_data(0)
|
|
182
|
+
@warnings << "First glyph has no variation data" if first_data.nil?
|
|
183
|
+
|
|
184
|
+
if gvar_count > 1
|
|
185
|
+
last_data = gvar.glyph_variation_data(gvar_count - 1)
|
|
186
|
+
@warnings << "Last glyph has no variation data" if last_data.nil?
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
@errors << "Failed to check gvar delta integrity: #{e.message}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Check HVAR delta coverage
|
|
194
|
+
def check_hvar_delta_integrity
|
|
195
|
+
hvar = @font.table("HVAR")
|
|
196
|
+
return unless hvar
|
|
197
|
+
|
|
198
|
+
# Check for item_variation_store
|
|
199
|
+
unless hvar.respond_to?(:item_variation_store)
|
|
200
|
+
@warnings << "HVAR table doesn't support item_variation_store"
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
store = hvar.item_variation_store
|
|
205
|
+
unless store
|
|
206
|
+
@warnings << "HVAR has no item variation store"
|
|
207
|
+
return
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Check that variation data exists
|
|
211
|
+
if store.respond_to?(:item_variation_data)
|
|
212
|
+
data = store.item_variation_data
|
|
213
|
+
if data.nil? || data.empty?
|
|
214
|
+
@warnings << "HVAR has no variation data"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
@warnings << "Failed to check HVAR delta integrity: #{e.message}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Check region coverage
|
|
222
|
+
def check_region_coverage
|
|
223
|
+
fvar = @font.table("fvar")
|
|
224
|
+
return unless fvar
|
|
225
|
+
|
|
226
|
+
axes = fvar.axes
|
|
227
|
+
return if axes.empty?
|
|
228
|
+
|
|
229
|
+
# Check gvar regions if present
|
|
230
|
+
if @font.has_table?("gvar")
|
|
231
|
+
check_gvar_region_coverage(axes)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Check metrics table regions
|
|
235
|
+
check_metrics_region_coverage("HVAR", axes) if @font.has_table?("HVAR")
|
|
236
|
+
check_metrics_region_coverage("VVAR", axes) if @font.has_table?("VVAR")
|
|
237
|
+
check_metrics_region_coverage("MVAR", axes) if @font.has_table?("MVAR")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Check gvar region coverage
|
|
241
|
+
#
|
|
242
|
+
# @param axes [Array] Variation axes
|
|
243
|
+
def check_gvar_region_coverage(axes)
|
|
244
|
+
gvar = @font.table("gvar")
|
|
245
|
+
return unless gvar
|
|
246
|
+
|
|
247
|
+
# Check shared tuples are within axis ranges
|
|
248
|
+
shared = gvar.shared_tuples
|
|
249
|
+
return if shared.empty?
|
|
250
|
+
|
|
251
|
+
shared.each_with_index do |tuple, idx|
|
|
252
|
+
next unless tuple
|
|
253
|
+
|
|
254
|
+
tuple.each_with_index do |coord, axis_idx|
|
|
255
|
+
next if axis_idx >= axes.length
|
|
256
|
+
next unless coord
|
|
257
|
+
|
|
258
|
+
axes[axis_idx]
|
|
259
|
+
# Normalized coords should be in [-1, 1] range
|
|
260
|
+
if coord < -1.0 || coord > 1.0
|
|
261
|
+
@warnings << "gvar shared tuple #{idx} axis #{axis_idx} out of range: #{coord}"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
rescue StandardError => e
|
|
266
|
+
@warnings << "Failed to check gvar region coverage: #{e.message}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Check metrics table region coverage
|
|
270
|
+
#
|
|
271
|
+
# @param table_tag [String] Table tag
|
|
272
|
+
# @param axes [Array] Variation axes
|
|
273
|
+
def check_metrics_region_coverage(table_tag, axes)
|
|
274
|
+
table = @font.table(table_tag)
|
|
275
|
+
return unless table.respond_to?(:item_variation_store)
|
|
276
|
+
|
|
277
|
+
store = table.item_variation_store
|
|
278
|
+
return unless store.respond_to?(:region_list)
|
|
279
|
+
|
|
280
|
+
region_list = store.region_list
|
|
281
|
+
return unless region_list.respond_to?(:regions)
|
|
282
|
+
|
|
283
|
+
# Check each region
|
|
284
|
+
regions = region_list.regions
|
|
285
|
+
regions.each_with_index do |region, idx|
|
|
286
|
+
next unless region.respond_to?(:region_axes)
|
|
287
|
+
|
|
288
|
+
region.region_axes.each_with_index do |reg_axis, axis_idx|
|
|
289
|
+
next if axis_idx >= axes.length
|
|
290
|
+
next unless reg_axis
|
|
291
|
+
|
|
292
|
+
# Check coordinates are in valid range [-1, 1]
|
|
293
|
+
%i[start_coord peak_coord end_coord].each do |coord_method|
|
|
294
|
+
next unless reg_axis.respond_to?(coord_method)
|
|
295
|
+
|
|
296
|
+
coord = reg_axis.send(coord_method)
|
|
297
|
+
if coord < -1.0 || coord > 1.0
|
|
298
|
+
@warnings << "#{table_tag} region #{idx} axis #{axis_idx} #{coord_method} out of range: #{coord}"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
@warnings << "Failed to check #{table_tag} region coverage: #{e.message}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Check instance definitions
|
|
308
|
+
def check_instance_definitions
|
|
309
|
+
fvar = @font.table("fvar")
|
|
310
|
+
return unless fvar
|
|
311
|
+
|
|
312
|
+
axes = fvar.axes
|
|
313
|
+
instances = fvar.instances
|
|
314
|
+
|
|
315
|
+
return if instances.empty?
|
|
316
|
+
|
|
317
|
+
instances.each_with_index do |instance, idx|
|
|
318
|
+
next unless instance
|
|
319
|
+
|
|
320
|
+
# Check coordinate count matches axis count
|
|
321
|
+
coords = instance[:coordinates]
|
|
322
|
+
if coords.length != axes.length
|
|
323
|
+
@errors << "Instance #{idx} has #{coords.length} coordinates but #{axes.length} axes"
|
|
324
|
+
next
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Check each coordinate is in axis range
|
|
328
|
+
coords.each_with_index do |coord, axis_idx|
|
|
329
|
+
axis = axes[axis_idx]
|
|
330
|
+
next unless axis
|
|
331
|
+
|
|
332
|
+
min = axis.min_value
|
|
333
|
+
max = axis.max_value
|
|
334
|
+
|
|
335
|
+
if coord < min || coord > max
|
|
336
|
+
@warnings << "Instance #{idx} axis #{axis.axis_tag} coordinate #{coord} outside range [#{min}, #{max}]"
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
rescue StandardError => e
|
|
341
|
+
@errors << "Failed to check instance definitions: #{e.message}"
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interpolator"
|
|
4
|
+
require_relative "region_matcher"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Provides shared context for variation operations
|
|
9
|
+
#
|
|
10
|
+
# This class centralizes the initialization of common variation components
|
|
11
|
+
# (axes, interpolator, region matcher) that are needed by most variation
|
|
12
|
+
# operations. It ensures consistent initialization and validation.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating a variation context
|
|
15
|
+
# context = VariationContext.new(font)
|
|
16
|
+
# context.validate!
|
|
17
|
+
# puts "Axes: #{context.axes.map(&:axis_tag)}"
|
|
18
|
+
#
|
|
19
|
+
# @example Using in a variation class
|
|
20
|
+
# class MyGenerator
|
|
21
|
+
# def initialize(font)
|
|
22
|
+
# @context = VariationContext.new(font)
|
|
23
|
+
# @context.validate!
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def generate
|
|
27
|
+
# @context.interpolator.normalize_coordinate(value, "wght")
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
class VariationContext
|
|
31
|
+
# @return [TrueTypeFont, OpenTypeFont] Font instance
|
|
32
|
+
attr_reader :font
|
|
33
|
+
|
|
34
|
+
# @return [Fvar, nil] fvar table
|
|
35
|
+
attr_reader :fvar
|
|
36
|
+
|
|
37
|
+
# @return [Array<VariationAxisRecord>] Variation axes
|
|
38
|
+
attr_reader :axes
|
|
39
|
+
|
|
40
|
+
# @return [Interpolator] Coordinate interpolator
|
|
41
|
+
attr_reader :interpolator
|
|
42
|
+
|
|
43
|
+
# @return [RegionMatcher] Region matcher
|
|
44
|
+
attr_reader :region_matcher
|
|
45
|
+
|
|
46
|
+
# Initialize variation context
|
|
47
|
+
#
|
|
48
|
+
# Loads fvar table and initializes all common variation components.
|
|
49
|
+
# Does not validate - call validate! explicitly if needed.
|
|
50
|
+
#
|
|
51
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
52
|
+
def initialize(font)
|
|
53
|
+
@font = font
|
|
54
|
+
@fvar = font.has_table?("fvar") ? font.table("fvar") : nil
|
|
55
|
+
@axes = @fvar ? @fvar.axes : []
|
|
56
|
+
@interpolator = Interpolator.new(@axes)
|
|
57
|
+
@region_matcher = RegionMatcher.new(@axes)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Validate that font is a proper variable font
|
|
61
|
+
#
|
|
62
|
+
# Checks for fvar table and axes definition. Raises errors if
|
|
63
|
+
# font is not a valid variable font.
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [MissingVariationTableError] If fvar table missing
|
|
67
|
+
# @raise [InvalidVariationDataError] If no axes defined
|
|
68
|
+
#
|
|
69
|
+
# @example Validate before processing
|
|
70
|
+
# context = VariationContext.new(font)
|
|
71
|
+
# context.validate!
|
|
72
|
+
# # Safe to proceed
|
|
73
|
+
def validate!
|
|
74
|
+
unless @fvar
|
|
75
|
+
raise MissingVariationTableError.new(
|
|
76
|
+
table: "fvar",
|
|
77
|
+
message: "Font is not a variable font (missing fvar table)",
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if @axes.empty?
|
|
82
|
+
raise InvalidVariationDataError.new(
|
|
83
|
+
message: "Variable font has no axes defined in fvar table",
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if font is a variable font
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean] True if fvar table exists
|
|
91
|
+
def variable_font?
|
|
92
|
+
!@fvar.nil?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get number of axes
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer] Axis count
|
|
98
|
+
def axis_count
|
|
99
|
+
@axes.length
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Find axis by tag
|
|
103
|
+
#
|
|
104
|
+
# @param axis_tag [String] Axis tag (e.g., "wght", "wdth")
|
|
105
|
+
# @return [VariationAxisRecord, nil] Axis or nil if not found
|
|
106
|
+
#
|
|
107
|
+
# @example Find weight axis
|
|
108
|
+
# wght_axis = context.find_axis("wght")
|
|
109
|
+
# puts "Range: #{wght_axis.min_value} - #{wght_axis.max_value}"
|
|
110
|
+
def find_axis(axis_tag)
|
|
111
|
+
@axes.find { |axis| axis.axis_tag == axis_tag }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get axis tags
|
|
115
|
+
#
|
|
116
|
+
# @return [Array<String>] Array of axis tags
|
|
117
|
+
def axis_tags
|
|
118
|
+
@axes.map(&:axis_tag)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Validate coordinates against axes
|
|
122
|
+
#
|
|
123
|
+
# Checks that all coordinate values are within valid axis ranges.
|
|
124
|
+
#
|
|
125
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates
|
|
126
|
+
# @return [void]
|
|
127
|
+
# @raise [InvalidCoordinatesError] If any coordinate out of range
|
|
128
|
+
#
|
|
129
|
+
# @example Validate coordinates
|
|
130
|
+
# context.validate_coordinates({ "wght" => 700 })
|
|
131
|
+
def validate_coordinates(coordinates)
|
|
132
|
+
coordinates.each do |axis_tag, value|
|
|
133
|
+
axis = find_axis(axis_tag)
|
|
134
|
+
|
|
135
|
+
unless axis
|
|
136
|
+
raise InvalidCoordinatesError.new(
|
|
137
|
+
axis: axis_tag,
|
|
138
|
+
value: value,
|
|
139
|
+
range: [],
|
|
140
|
+
message: "Unknown axis '#{axis_tag}'",
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if value < axis.min_value || value > axis.max_value
|
|
145
|
+
raise InvalidCoordinatesError.new(
|
|
146
|
+
axis: axis_tag,
|
|
147
|
+
value: value,
|
|
148
|
+
range: [axis.min_value, axis.max_value],
|
|
149
|
+
message: "Coordinate #{value} for axis '#{axis_tag}' outside valid range [#{axis.min_value}, #{axis.max_value}]",
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get default coordinates
|
|
156
|
+
#
|
|
157
|
+
# Returns coordinates at default values for all axes.
|
|
158
|
+
#
|
|
159
|
+
# @return [Hash<String, Float>] Default coordinates
|
|
160
|
+
def default_coordinates
|
|
161
|
+
coordinates = {}
|
|
162
|
+
@axes.each do |axis|
|
|
163
|
+
coordinates[axis.axis_tag] = axis.default_value
|
|
164
|
+
end
|
|
165
|
+
coordinates
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Normalize coordinates to [-1, 1] range
|
|
169
|
+
#
|
|
170
|
+
# Convenience method that delegates to interpolator.
|
|
171
|
+
#
|
|
172
|
+
# @param coordinates [Hash<String, Float>] User-space coordinates
|
|
173
|
+
# @return [Hash<String, Float>] Normalized coordinates
|
|
174
|
+
def normalize_coordinates(coordinates)
|
|
175
|
+
@interpolator.normalize_coordinates(coordinates)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get variation type
|
|
179
|
+
#
|
|
180
|
+
# Determines whether font uses TrueType (gvar) or PostScript (CFF2)
|
|
181
|
+
# variation format.
|
|
182
|
+
#
|
|
183
|
+
# @return [Symbol] :truetype, :postscript, or :none
|
|
184
|
+
def variation_type
|
|
185
|
+
if @font.has_table?("CFF2")
|
|
186
|
+
:postscript
|
|
187
|
+
elsif @font.has_table?("gvar")
|
|
188
|
+
:truetype
|
|
189
|
+
else
|
|
190
|
+
:none
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check if font has glyph variations
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean] True if gvar or CFF2 present
|
|
197
|
+
def has_glyph_variations?
|
|
198
|
+
@font.has_table?("gvar") || @font.has_table?("CFF2")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Check if font has metrics variations
|
|
202
|
+
#
|
|
203
|
+
# @return [Boolean] True if HVAR, VVAR, or MVAR present
|
|
204
|
+
def has_metrics_variations?
|
|
205
|
+
@font.has_table?("HVAR") ||
|
|
206
|
+
@font.has_table?("VVAR") ||
|
|
207
|
+
@font.has_table?("MVAR")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
data/lib/fontisan/version.rb
CHANGED