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.
Files changed (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +672 -69
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1477 -297
  6. data/Rakefile +63 -41
  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 +364 -4
  12. data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +24 -1
  18. data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +408 -0
  39. data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
  57. data/lib/fontisan/font_writer.rb +302 -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 +310 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
  65. data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
  88. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  89. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  90. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  91. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  92. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  93. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  94. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  95. data/lib/fontisan/outline_extractor.rb +423 -0
  96. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  97. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  98. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  99. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  100. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  101. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  102. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  103. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  104. data/lib/fontisan/subset/builder.rb +268 -0
  105. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  106. data/lib/fontisan/subset/options.rb +142 -0
  107. data/lib/fontisan/subset/profile.rb +152 -0
  108. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  109. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  110. data/lib/fontisan/svg/font_generator.rb +264 -0
  111. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  112. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  113. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  114. data/lib/fontisan/tables/cff/charset.rb +282 -0
  115. data/lib/fontisan/tables/cff/charstring.rb +934 -0
  116. data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
  117. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  118. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  119. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  120. data/lib/fontisan/tables/cff/dict.rb +351 -0
  121. data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
  122. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  123. data/lib/fontisan/tables/cff/header.rb +102 -0
  124. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  125. data/lib/fontisan/tables/cff/index.rb +237 -0
  126. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  127. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  128. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  129. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  130. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  131. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  132. data/lib/fontisan/tables/cff.rb +489 -0
  133. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  134. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  135. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  136. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  137. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  138. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  139. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  140. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  141. data/lib/fontisan/tables/cff2.rb +346 -0
  142. data/lib/fontisan/tables/cvar.rb +203 -0
  143. data/lib/fontisan/tables/fvar.rb +2 -2
  144. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  145. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  146. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  147. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  148. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  149. data/lib/fontisan/tables/glyf.rb +235 -0
  150. data/lib/fontisan/tables/gvar.rb +231 -0
  151. data/lib/fontisan/tables/hhea.rb +124 -0
  152. data/lib/fontisan/tables/hmtx.rb +287 -0
  153. data/lib/fontisan/tables/hvar.rb +191 -0
  154. data/lib/fontisan/tables/loca.rb +322 -0
  155. data/lib/fontisan/tables/maxp.rb +192 -0
  156. data/lib/fontisan/tables/mvar.rb +185 -0
  157. data/lib/fontisan/tables/name.rb +99 -30
  158. data/lib/fontisan/tables/variation_common.rb +346 -0
  159. data/lib/fontisan/tables/vvar.rb +234 -0
  160. data/lib/fontisan/true_type_collection.rb +156 -2
  161. data/lib/fontisan/true_type_font.rb +321 -20
  162. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  163. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  164. data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
  165. data/lib/fontisan/utils/thread_pool.rb +134 -0
  166. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  167. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  168. data/lib/fontisan/validation/structure_validator.rb +198 -0
  169. data/lib/fontisan/validation/table_validator.rb +158 -0
  170. data/lib/fontisan/validation/validator.rb +152 -0
  171. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  172. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  173. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  174. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  175. data/lib/fontisan/variable/instancer.rb +344 -0
  176. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  177. data/lib/fontisan/variable/region_matcher.rb +208 -0
  178. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  179. data/lib/fontisan/variable/table_updater.rb +219 -0
  180. data/lib/fontisan/variation/blend_applier.rb +199 -0
  181. data/lib/fontisan/variation/cache.rb +298 -0
  182. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  183. data/lib/fontisan/variation/converter.rb +375 -0
  184. data/lib/fontisan/variation/data_extractor.rb +86 -0
  185. data/lib/fontisan/variation/delta_applier.rb +266 -0
  186. data/lib/fontisan/variation/delta_parser.rb +228 -0
  187. data/lib/fontisan/variation/inspector.rb +275 -0
  188. data/lib/fontisan/variation/instance_generator.rb +273 -0
  189. data/lib/fontisan/variation/instance_writer.rb +341 -0
  190. data/lib/fontisan/variation/interpolator.rb +231 -0
  191. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  192. data/lib/fontisan/variation/optimizer.rb +418 -0
  193. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  194. data/lib/fontisan/variation/region_matcher.rb +221 -0
  195. data/lib/fontisan/variation/subsetter.rb +463 -0
  196. data/lib/fontisan/variation/table_accessor.rb +105 -0
  197. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  198. data/lib/fontisan/variation/validator.rb +345 -0
  199. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  200. data/lib/fontisan/variation/variation_context.rb +211 -0
  201. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  202. data/lib/fontisan/version.rb +1 -1
  203. data/lib/fontisan/version.rb.orig +9 -0
  204. data/lib/fontisan/woff2/directory.rb +257 -0
  205. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  206. data/lib/fontisan/woff2/header.rb +101 -0
  207. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  208. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  209. data/lib/fontisan/woff2_font.rb +717 -0
  210. data/lib/fontisan/woff_font.rb +488 -0
  211. data/lib/fontisan.rb +132 -0
  212. data/scripts/compare_stack_aware.rb +187 -0
  213. data/scripts/measure_optimization.rb +141 -0
  214. metadata +234 -4
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_generator"
4
+ require_relative "../converters/svg_generator"
5
+
6
+ module Fontisan
7
+ module Variation
8
+ # Generates SVG fonts from variable fonts at specific coordinates
9
+ #
10
+ # [`VariableSvgGenerator`](lib/fontisan/variation/variable_svg_generator.rb)
11
+ # combines instance generation with SVG conversion to create static SVG
12
+ # fonts from variable fonts at any point in the design space.
13
+ #
14
+ # Process:
15
+ # 1. Accept variable font + axis coordinates
16
+ # 2. Generate static instance using InstanceGenerator
17
+ # 3. Build temporary font from instance tables
18
+ # 4. Delegate to SvgGenerator for SVG creation
19
+ # 5. Return SVG with variation metadata
20
+ #
21
+ # This enables generating SVG fonts at specific weights, widths, or other
22
+ # variation axes without creating intermediate font files.
23
+ #
24
+ # @example Generate SVG at Bold weight
25
+ # generator = VariableSvgGenerator.new(variable_font, { "wght" => 700.0 })
26
+ # svg_result = generator.generate
27
+ # File.write("bold.svg", svg_result[:svg_xml])
28
+ #
29
+ # @example Generate SVG at specific width and weight
30
+ # coords = { "wght" => 700.0, "wdth" => 75.0 }
31
+ # generator = VariableSvgGenerator.new(variable_font, coords)
32
+ # svg_result = generator.generate(pretty_print: true)
33
+ class VariableSvgGenerator
34
+ # @return [TrueTypeFont, OpenTypeFont] Variable font
35
+ attr_reader :font
36
+
37
+ # @return [Hash<String, Float>] Design space coordinates
38
+ attr_reader :coordinates
39
+
40
+ # Initialize generator
41
+ #
42
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
43
+ # @param coordinates [Hash<String, Float>] Design space coordinates
44
+ # @raise [Error] If font is not a variable font
45
+ def initialize(font, coordinates = {})
46
+ @font = font
47
+ @coordinates = coordinates || {}
48
+
49
+ validate_variable_font!
50
+ end
51
+
52
+ # Generate SVG font at specified coordinates
53
+ #
54
+ # Creates a static instance at the given coordinates and converts
55
+ # it to SVG format. Returns the same format as SvgGenerator for
56
+ # consistency.
57
+ #
58
+ # @param options [Hash] SVG generation options
59
+ # @option options [Boolean] :pretty_print Pretty print XML (default: true)
60
+ # @option options [Array<Integer>] :glyph_ids Specific glyphs (default: all)
61
+ # @option options [Integer] :max_glyphs Maximum glyphs (default: all)
62
+ # @option options [String] :font_id Font ID for SVG
63
+ # @option options [Integer] :default_advance Default advance width
64
+ # @return [Hash] Hash with :svg_xml key containing SVG XML string
65
+ # @raise [Error] If generation fails
66
+ def generate(options = {})
67
+ # Generate static instance tables
68
+ instance_tables = generate_static_instance
69
+
70
+ # Build temporary font from instance tables
71
+ static_font = build_font_from_tables(instance_tables)
72
+
73
+ # Generate SVG using standard generator
74
+ svg_generator = Converters::SvgGenerator.new
75
+ result = svg_generator.convert(static_font, options)
76
+
77
+ # Add variation metadata to result
78
+ result[:variation_metadata] = {
79
+ coordinates: @coordinates,
80
+ source_font: extract_font_name,
81
+ }
82
+
83
+ result
84
+ end
85
+
86
+ # Generate SVG for a named instance
87
+ #
88
+ # @param instance_index [Integer] Index of named instance in fvar
89
+ # @param options [Hash] SVG generation options
90
+ # @return [Hash] Hash with :svg_xml key
91
+ def generate_named_instance(instance_index, options = {})
92
+ instance_generator = InstanceGenerator.new(@font)
93
+ instance_tables = instance_generator.generate_named_instance(instance_index)
94
+
95
+ static_font = build_font_from_tables(instance_tables)
96
+ svg_generator = Converters::SvgGenerator.new
97
+ result = svg_generator.convert(static_font, options)
98
+
99
+ # Add instance metadata
100
+ result[:variation_metadata] = {
101
+ instance_index: instance_index,
102
+ source_font: extract_font_name,
103
+ }
104
+
105
+ result
106
+ end
107
+
108
+ # Get default coordinates for font
109
+ #
110
+ # Returns all axes at their default values.
111
+ #
112
+ # @return [Hash<String, Float>] Default coordinates
113
+ def default_coordinates
114
+ return {} unless @font.has_table?("fvar")
115
+
116
+ fvar = @font.table("fvar")
117
+ return {} unless fvar
118
+
119
+ coords = {}
120
+ fvar.axes.each do |axis|
121
+ coords[axis.axis_tag] = axis.default_value
122
+ end
123
+ coords
124
+ end
125
+
126
+ # Get list of named instances
127
+ #
128
+ # @return [Array<Hash>] Array of instance info
129
+ def named_instances
130
+ return [] unless @font.has_table?("fvar")
131
+
132
+ fvar = @font.table("fvar")
133
+ return [] unless fvar
134
+
135
+ fvar.instances.map.with_index do |instance, index|
136
+ {
137
+ index: index,
138
+ name: instance[:subfamily_name_id],
139
+ coordinates: build_instance_coordinates(instance, fvar.axes),
140
+ }
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ # Validate that font is a variable font
147
+ #
148
+ # @raise [Error] If not a variable font
149
+ def validate_variable_font!
150
+ unless @font.has_table?("fvar")
151
+ raise Fontisan::Error,
152
+ "Font must be a variable font (missing fvar table)"
153
+ end
154
+
155
+ # Check for variation data
156
+ has_gvar = @font.has_table?("gvar")
157
+ has_cff2 = @font.has_table?("CFF2")
158
+
159
+ unless has_gvar || has_cff2
160
+ raise Fontisan::Error,
161
+ "Variable font must have gvar (TrueType) or CFF2 (PostScript) table"
162
+ end
163
+ end
164
+
165
+ # Generate static instance at current coordinates
166
+ #
167
+ # @return [Hash<String, String>] Instance tables
168
+ def generate_static_instance
169
+ # Use coordinates or defaults if none specified
170
+ coords = @coordinates.empty? ? default_coordinates : @coordinates
171
+
172
+ instance_generator = InstanceGenerator.new(@font, coords)
173
+ instance_generator.generate
174
+ end
175
+
176
+ # Build a font object from instance tables
177
+ #
178
+ # Creates a minimal font object that can be used by SvgGenerator.
179
+ # This is a lightweight wrapper around the table data.
180
+ #
181
+ # @param tables [Hash<String, String>] Font tables
182
+ # @return [Object] Font-like object
183
+ def build_font_from_tables(tables)
184
+ # Create a simple font wrapper that implements the minimal
185
+ # interface needed by SvgGenerator
186
+ InstanceFontWrapper.new(@font, tables)
187
+ end
188
+
189
+ # Extract font name for metadata
190
+ #
191
+ # @return [String] Font name
192
+ def extract_font_name
193
+ name_table = @font.table("name")
194
+ return "Unknown" unless name_table
195
+
196
+ # Try font family name
197
+ family = name_table.font_family.first
198
+ return family if family && !family.empty?
199
+
200
+ "Unknown"
201
+ rescue StandardError
202
+ "Unknown"
203
+ end
204
+
205
+ # Build coordinates from instance
206
+ #
207
+ # @param instance [Hash] Instance data
208
+ # @param axes [Array] Variation axes
209
+ # @return [Hash<String, Float>] Coordinates
210
+ def build_instance_coordinates(instance, axes)
211
+ coords = {}
212
+ instance[:coordinates].each_with_index do |value, index|
213
+ next if index >= axes.length
214
+
215
+ axis = axes[index]
216
+ coords[axis.axis_tag] = value
217
+ end
218
+ coords
219
+ end
220
+
221
+ # Wrapper class for instance font tables
222
+ #
223
+ # Provides minimal interface needed by SvgGenerator while using
224
+ # instance tables instead of original font tables.
225
+ class InstanceFontWrapper
226
+ # @return [Hash<String, String>] Font tables
227
+ attr_reader :table_data
228
+
229
+ # Initialize wrapper
230
+ #
231
+ # @param original_font [Object] Original variable font
232
+ # @param instance_tables [Hash<String, String>] Instance tables
233
+ def initialize(original_font, instance_tables)
234
+ @original_font = original_font
235
+ @table_data = instance_tables
236
+ end
237
+
238
+ # Get table by tag
239
+ #
240
+ # @param tag [String] Table tag
241
+ # @return [Object, nil] Table or nil
242
+ def table(tag)
243
+ # Use instance table if available, otherwise fall back to original
244
+ if @table_data.key?(tag)
245
+ end
246
+ @original_font.table(tag)
247
+ end
248
+
249
+ # Check if table exists
250
+ #
251
+ # @param tag [String] Table tag
252
+ # @return [Boolean] True if table exists
253
+ def has_table?(tag)
254
+ @table_data.key?(tag) || @original_font.has_table?(tag)
255
+ end
256
+
257
+ # Forward other methods to original font
258
+ def method_missing(method, ...)
259
+ @original_font.send(method, ...)
260
+ end
261
+
262
+ def respond_to_missing?(method, include_private = false)
263
+ @original_font.respond_to?(method, include_private) || super
264
+ end
265
+ end
266
+ end
267
+ end
268
+ 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