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,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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variation
|
|
7
|
+
# Tuple variation header structure
|
|
8
|
+
#
|
|
9
|
+
# Used by both gvar and cvar tables to describe variation tuples.
|
|
10
|
+
# Each tuple header contains metadata about peak coordinates,
|
|
11
|
+
# intermediate regions, and point number handling.
|
|
12
|
+
class TupleVariationHeader < Binary::BaseRecord
|
|
13
|
+
uint16 :variation_data_size
|
|
14
|
+
uint16 :tuple_index
|
|
15
|
+
|
|
16
|
+
# Tuple index flags
|
|
17
|
+
EMBEDDED_PEAK_TUPLE = 0x8000
|
|
18
|
+
INTERMEDIATE_REGION = 0x4000
|
|
19
|
+
PRIVATE_POINT_NUMBERS = 0x2000
|
|
20
|
+
TUPLE_INDEX_MASK = 0x0FFF
|
|
21
|
+
|
|
22
|
+
# Check if tuple has embedded peak coordinates
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean] True if embedded
|
|
25
|
+
def embedded_peak_tuple?
|
|
26
|
+
(tuple_index & EMBEDDED_PEAK_TUPLE) != 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if tuple has intermediate region
|
|
30
|
+
#
|
|
31
|
+
# @return [Boolean] True if intermediate region
|
|
32
|
+
def intermediate_region?
|
|
33
|
+
(tuple_index & INTERMEDIATE_REGION) != 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if tuple has private point numbers
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] True if private points
|
|
39
|
+
def private_point_numbers?
|
|
40
|
+
(tuple_index & PRIVATE_POINT_NUMBERS) != 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get shared tuple index
|
|
44
|
+
#
|
|
45
|
+
# @return [Integer] Tuple index
|
|
46
|
+
def shared_tuple_index
|
|
47
|
+
tuple_index & TUPLE_INDEX_MASK
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
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
|