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.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end