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,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "axis_normalizer"
5
+ require_relative "region_matcher"
6
+ require_relative "glyph_delta_processor"
7
+ require_relative "metric_delta_processor"
8
+
9
+ module Fontisan
10
+ module Variable
11
+ # Main orchestrator for delta application in variable fonts
12
+ #
13
+ # Coordinates the entire delta application pipeline:
14
+ # 1. Normalizes user coordinates to design space
15
+ # 2. Calculates region scalars based on normalized coordinates
16
+ # 3. Applies glyph outline deltas via GlyphDeltaProcessor
17
+ # 4. Applies metric deltas via MetricDeltaProcessor
18
+ #
19
+ # This is the primary interface for applying variation deltas to fonts.
20
+ #
21
+ # @example Apply deltas at specific coordinates
22
+ # applicator = DeltaApplicator.new(font)
23
+ # result = applicator.apply({ "wght" => 700, "wdth" => 100 })
24
+ # # => { normalized_coords: {...}, region_scalars: [...],
25
+ # # glyph_deltas: {...}, metric_deltas: {...} }
26
+ class DeltaApplicator
27
+ # @return [Hash] Configuration settings
28
+ attr_reader :config
29
+
30
+ # @return [AxisNormalizer] Axis normalizer
31
+ attr_reader :axis_normalizer
32
+
33
+ # @return [RegionMatcher] Region matcher
34
+ attr_reader :region_matcher
35
+
36
+ # @return [GlyphDeltaProcessor] Glyph delta processor
37
+ attr_reader :glyph_delta_processor
38
+
39
+ # @return [MetricDeltaProcessor] Metric delta processor
40
+ attr_reader :metric_delta_processor
41
+
42
+ # Initialize the delta applicator
43
+ #
44
+ # @param font [TrueTypeFont, OpenTypeFont] Font object
45
+ # @param config [Hash] Optional configuration overrides
46
+ def initialize(font, config = {})
47
+ @font = font
48
+ @config = load_config.merge(config)
49
+
50
+ # Load variation tables
51
+ @fvar = load_table("fvar")
52
+ @gvar = load_table("gvar")
53
+ @hvar = load_table("HVAR")
54
+ @vvar = load_table("VVAR")
55
+ @mvar = load_table("MVAR")
56
+
57
+ # Initialize components
58
+ initialize_components
59
+ end
60
+
61
+ # Apply deltas at specified user coordinates
62
+ #
63
+ # @param user_coords [Hash<String, Numeric>] User coordinates
64
+ # @return [Hash] Delta application results
65
+ def apply(user_coords)
66
+ # Validate we have required tables
67
+ unless @fvar
68
+ raise ArgumentError,
69
+ "Font does not have fvar table (not a variable font)"
70
+ end
71
+
72
+ # Step 1: Normalize coordinates
73
+ normalized_coords = @axis_normalizer.normalize(user_coords)
74
+
75
+ # Step 2: Calculate region scalars
76
+ region_scalars = @region_matcher.match(normalized_coords)
77
+
78
+ # Step 3: Prepare result structure
79
+ result = {
80
+ user_coords: user_coords,
81
+ normalized_coords: normalized_coords,
82
+ region_scalars: region_scalars,
83
+ glyph_deltas: {},
84
+ metric_deltas: {},
85
+ font_metrics: {},
86
+ }
87
+
88
+ # Step 4: Apply font-level metrics if MVAR present
89
+ if @metric_delta_processor.has_mvar?
90
+ result[:font_metrics] =
91
+ @metric_delta_processor.apply_font_metrics(region_scalars)
92
+ end
93
+
94
+ result
95
+ end
96
+
97
+ # Apply deltas to a specific glyph
98
+ #
99
+ # @param glyph_id [Integer] Glyph ID
100
+ # @param user_coords [Hash<String, Numeric>] User coordinates
101
+ # @return [Hash] Glyph delta result
102
+ def apply_glyph(glyph_id, user_coords)
103
+ # Normalize and get region scalars
104
+ normalized_coords = @axis_normalizer.normalize(user_coords)
105
+ region_scalars = @region_matcher.match(normalized_coords)
106
+
107
+ result = {
108
+ glyph_id: glyph_id,
109
+ normalized_coords: normalized_coords,
110
+ }
111
+
112
+ # Apply glyph outline deltas if gvar present
113
+ if @glyph_delta_processor
114
+ result[:outline_deltas] = @glyph_delta_processor.apply_deltas(
115
+ glyph_id,
116
+ region_scalars,
117
+ )
118
+ end
119
+
120
+ # Apply metric deltas
121
+ result[:metric_deltas] = @metric_delta_processor.apply_deltas(
122
+ glyph_id,
123
+ region_scalars,
124
+ )
125
+
126
+ result
127
+ end
128
+
129
+ # Apply deltas to multiple glyphs
130
+ #
131
+ # @param glyph_ids [Array<Integer>] Glyph IDs
132
+ # @param user_coords [Hash<String, Numeric>] User coordinates
133
+ # @return [Hash<Integer, Hash>] Results by glyph ID
134
+ def apply_glyphs(glyph_ids, user_coords)
135
+ # Normalize once for all glyphs
136
+ normalized_coords = @axis_normalizer.normalize(user_coords)
137
+ region_scalars = @region_matcher.match(normalized_coords)
138
+
139
+ glyph_ids.each_with_object({}) do |glyph_id, results|
140
+ results[glyph_id] = {
141
+ outline_deltas: @glyph_delta_processor&.apply_deltas(glyph_id,
142
+ region_scalars),
143
+ metric_deltas: @metric_delta_processor.apply_deltas(glyph_id,
144
+ region_scalars),
145
+ }
146
+ end
147
+ end
148
+
149
+ # Get advance width delta for a glyph
150
+ #
151
+ # @param glyph_id [Integer] Glyph ID
152
+ # @param user_coords [Hash<String, Numeric>] User coordinates
153
+ # @return [Integer] Advance width delta
154
+ def advance_width_delta(glyph_id, user_coords)
155
+ normalized_coords = @axis_normalizer.normalize(user_coords)
156
+ region_scalars = @region_matcher.match(normalized_coords)
157
+
158
+ @metric_delta_processor.advance_width_delta(glyph_id, region_scalars)
159
+ end
160
+
161
+ # Check if font is a variable font
162
+ #
163
+ # @return [Boolean] True if variable font
164
+ def variable_font?
165
+ !@fvar.nil?
166
+ end
167
+
168
+ # Get axis information
169
+ #
170
+ # @return [Hash] Axis information from fvar
171
+ def axes
172
+ return {} unless @fvar
173
+
174
+ @fvar.axes.each_with_object({}) do |axis, hash|
175
+ # Convert BinData::String to regular Ruby String
176
+ tag = axis.axis_tag.to_s
177
+ hash[tag] = {
178
+ min: axis.min_value,
179
+ default: axis.default_value,
180
+ max: axis.max_value,
181
+ name_id: axis.axis_name_id,
182
+ }
183
+ end
184
+ end
185
+
186
+ # Get available axis tags
187
+ #
188
+ # @return [Array<String>] Axis tags
189
+ def axis_tags
190
+ @axis_normalizer.axis_tags
191
+ end
192
+
193
+ # Get number of variation regions
194
+ #
195
+ # @return [Integer] Region count
196
+ def region_count
197
+ @region_matcher.region_count
198
+ end
199
+
200
+ private
201
+
202
+ # Load configuration from YAML file
203
+ #
204
+ # @return [Hash] Configuration hash
205
+ def load_config
206
+ config_path = File.join(__dir__, "..", "config",
207
+ "variable_settings.yml")
208
+ loaded = YAML.load_file(config_path)
209
+ # Convert string keys to symbol keys for consistency
210
+ deep_symbolize_keys(loaded)
211
+ rescue StandardError
212
+ # Return default config
213
+ {
214
+ validation: {
215
+ validate_tables: true,
216
+ check_required_tables: true,
217
+ },
218
+ }
219
+ end
220
+
221
+ # Recursively convert hash keys to symbols
222
+ #
223
+ # @param hash [Hash] Hash with string keys
224
+ # @return [Hash] Hash with symbol keys
225
+ def deep_symbolize_keys(hash)
226
+ hash.each_with_object({}) do |(key, value), result|
227
+ new_key = key.to_sym
228
+ new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
229
+ result[new_key] = new_value
230
+ end
231
+ end
232
+
233
+ # Initialize all components
234
+ def initialize_components
235
+ # Initialize axis normalizer
236
+ @axis_normalizer = AxisNormalizer.new(@fvar, @config)
237
+
238
+ # Get axis tags for region matcher - convert BinData::String to String
239
+ axis_tags = @fvar ? @fvar.axes.map { |axis| axis.axis_tag.to_s } : []
240
+
241
+ # Initialize region matcher
242
+ # Get variation region list from one of the tables
243
+ variation_region_list = get_variation_region_list
244
+ @region_matcher = RegionMatcher.new(variation_region_list, axis_tags,
245
+ @config)
246
+
247
+ # Initialize glyph delta processor
248
+ @glyph_delta_processor = if @gvar
249
+ GlyphDeltaProcessor.new(@gvar,
250
+ @config)
251
+ end
252
+
253
+ # Initialize metric delta processor
254
+ @metric_delta_processor = MetricDeltaProcessor.new(
255
+ hvar: @hvar,
256
+ vvar: @vvar,
257
+ mvar: @mvar,
258
+ config: @config,
259
+ )
260
+ end
261
+
262
+ # Load a font table
263
+ #
264
+ # @param tag [String] Table tag
265
+ # @return [Object, nil] Table object or nil
266
+ def load_table(tag)
267
+ return nil unless @font.respond_to?(:table_data)
268
+
269
+ data = @font.table_data(tag)
270
+ return nil if data.nil? || data.empty?
271
+
272
+ # Map tag to table class
273
+ table_class = case tag
274
+ when "fvar" then Tables::Fvar
275
+ when "gvar" then Tables::Gvar
276
+ when "HVAR" then Tables::Hvar
277
+ when "VVAR" then Tables::Vvar
278
+ when "MVAR" then Tables::Mvar
279
+ else return nil
280
+ end
281
+
282
+ table_class.read(data)
283
+ rescue StandardError => e
284
+ warn "Failed to load #{tag} table: #{e.message}" if @config.dig(
285
+ :validation, :validate_tables
286
+ )
287
+ nil
288
+ end
289
+
290
+ # Get variation region list from available tables
291
+ #
292
+ # @return [VariationCommon::VariationRegionList, nil] Region list
293
+ def get_variation_region_list
294
+ # Try to get from HVAR first (most common)
295
+ if @hvar&.item_variation_store
296
+ return @hvar.item_variation_store.variation_region_list
297
+ end
298
+
299
+ # Try VVAR
300
+ if @vvar.respond_to?(:item_variation_store) && @vvar.item_variation_store
301
+ return @vvar.item_variation_store.variation_region_list
302
+ end
303
+
304
+ # Try MVAR
305
+ if @mvar&.item_variation_store
306
+ return @mvar.item_variation_store.variation_region_list
307
+ end
308
+
309
+ nil
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Fontisan
6
+ module Variable
7
+ # Applies glyph outline deltas from gvar table
8
+ #
9
+ # Processes delta values for glyph control points and phantom points,
10
+ # applying them based on region scalars to modify glyph outlines.
11
+ #
12
+ # Handles both simple and compound glyphs, and processes phantom points
13
+ # which affect glyph metrics.
14
+ #
15
+ # @example Apply deltas to a glyph
16
+ # processor = GlyphDeltaProcessor.new(gvar_table, shared_tuples)
17
+ # modified = processor.apply_deltas(glyph_id, region_scalars)
18
+ # # => { x_deltas: [...], y_deltas: [...], phantom_deltas: [...] }
19
+ class GlyphDeltaProcessor
20
+ # @return [Hash] Configuration settings
21
+ attr_reader :config
22
+
23
+ # Initialize the processor
24
+ #
25
+ # @param gvar [Fontisan::Tables::Gvar] Glyph variations table
26
+ # @param config [Hash] Optional configuration overrides
27
+ def initialize(gvar, config = {})
28
+ @gvar = gvar
29
+ @config = load_config.merge(config)
30
+ @shared_tuples = gvar&.shared_tuples || []
31
+ end
32
+
33
+ # Apply deltas to a glyph
34
+ #
35
+ # @param glyph_id [Integer] Glyph ID
36
+ # @param region_scalars [Array<Float>] Scalar for each region
37
+ # @return [Hash, nil] Delta information or nil
38
+ def apply_deltas(glyph_id, region_scalars)
39
+ return nil unless @gvar
40
+
41
+ # Get tuple variations for this glyph
42
+ tuple_info = @gvar.glyph_tuple_variations(glyph_id)
43
+ return nil unless tuple_info
44
+
45
+ # Calculate accumulated deltas
46
+ calculate_accumulated_deltas(tuple_info, region_scalars)
47
+ end
48
+
49
+ # Check if glyph has variation data
50
+ #
51
+ # @param glyph_id [Integer] Glyph ID
52
+ # @return [Boolean] True if glyph has variations
53
+ def has_variations?(glyph_id)
54
+ return false unless @gvar
55
+
56
+ data = @gvar.glyph_variation_data(glyph_id)
57
+ !data.nil? && !data.empty?
58
+ end
59
+
60
+ # Get number of glyphs with variations
61
+ #
62
+ # @return [Integer] Glyph count
63
+ def glyph_count
64
+ @gvar&.glyph_count || 0
65
+ end
66
+
67
+ private
68
+
69
+ # Load configuration from YAML file
70
+ #
71
+ # @return [Hash] Configuration hash
72
+ def load_config
73
+ config_path = File.join(__dir__, "..", "config",
74
+ "variable_settings.yml")
75
+ loaded = YAML.load_file(config_path)
76
+ # Convert string keys to symbol keys for consistency
77
+ deep_symbolize_keys(loaded)
78
+ rescue StandardError
79
+ # Return default config
80
+ {
81
+ glyph_deltas: {
82
+ apply_to_simple: true,
83
+ apply_to_compound: true,
84
+ process_phantom_points: true,
85
+ phantom_point_count: 4,
86
+ },
87
+ delta_application: {
88
+ rounding_mode: "round",
89
+ },
90
+ }
91
+ end
92
+
93
+ # Recursively convert hash keys to symbols
94
+ #
95
+ # @param hash [Hash] Hash with string keys
96
+ # @return [Hash] Hash with symbol keys
97
+ def deep_symbolize_keys(hash)
98
+ hash.each_with_object({}) do |(key, value), result|
99
+ new_key = key.to_sym
100
+ new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
101
+ result[new_key] = new_value
102
+ end
103
+ end
104
+
105
+ # Calculate accumulated deltas for a glyph
106
+ #
107
+ # @param tuple_info [Hash] Tuple variation information
108
+ # @param region_scalars [Array<Float>] Region scalars
109
+ # @return [Hash] Accumulated deltas
110
+ def calculate_accumulated_deltas(tuple_info, region_scalars)
111
+ tuples = tuple_info[:tuples]
112
+ return nil if tuples.nil? || tuples.empty?
113
+
114
+ # Result structure
115
+ result = {
116
+ x_deltas: [],
117
+ y_deltas: [],
118
+ phantom_deltas: [],
119
+ point_count: 0,
120
+ }
121
+
122
+ # Process each tuple
123
+ tuples.each_with_index do |tuple, tuple_index|
124
+ # Get peak coordinates for this tuple
125
+ peak_coords = if tuple[:embedded_peak]
126
+ tuple[:peak]
127
+ else
128
+ @shared_tuples[tuple[:shared_index]]
129
+ end
130
+
131
+ next unless peak_coords
132
+
133
+ # Calculate scalar for this tuple
134
+ scalar = calculate_tuple_scalar(tuple, peak_coords, region_scalars)
135
+ next if scalar.zero?
136
+
137
+ # This is a simplified version - actual implementation would need
138
+ # to unpack the delta data from the gvar table
139
+ # For now, we just indicate which tuples contribute
140
+ result[:contributing_tuples] ||= []
141
+ result[:contributing_tuples] << {
142
+ index: tuple_index,
143
+ scalar: scalar,
144
+ peak: peak_coords,
145
+ }
146
+ end
147
+
148
+ result
149
+ end
150
+
151
+ # Calculate scalar for a tuple
152
+ #
153
+ # @param tuple [Hash] Tuple information
154
+ # @param peak_coords [Array<Float>] Peak coordinates
155
+ # @param region_scalars [Array<Float>] Region scalars
156
+ # @return [Float] Tuple scalar
157
+ def calculate_tuple_scalar(tuple, peak_coords, region_scalars)
158
+ # For embedded tuples, calculate scalar based on peak/start/end
159
+ if tuple[:embedded_peak]
160
+ return calculate_embedded_tuple_scalar(tuple, peak_coords)
161
+ end
162
+
163
+ # For shared tuples, use the corresponding region scalar
164
+ shared_index = tuple[:shared_index]
165
+ return 0.0 if shared_index >= region_scalars.length
166
+
167
+ region_scalars[shared_index]
168
+ end
169
+
170
+ # Calculate scalar for embedded tuple
171
+ #
172
+ # @param tuple [Hash] Tuple information
173
+ # @param peak_coords [Array<Float>] Peak coordinates
174
+ # @return [Float] Tuple scalar
175
+ def calculate_embedded_tuple_scalar(_tuple, peak_coords)
176
+ # Simplified - would need current normalized coordinates
177
+ # For now, return 1.0 if peak coords are present
178
+ peak_coords.any? { |c| c.abs > Float::EPSILON } ? 1.0 : 0.0
179
+ end
180
+
181
+ # Apply rounding to delta value
182
+ #
183
+ # @param delta [Float] Delta value
184
+ # @return [Integer] Rounded delta
185
+ def apply_rounding(delta)
186
+ mode = @config.dig(:delta_application, :rounding_mode) || "round"
187
+
188
+ case mode
189
+ when "round"
190
+ delta.round
191
+ when "floor"
192
+ delta.floor
193
+ when "ceil"
194
+ delta.ceil
195
+ when "truncate"
196
+ delta.to_i
197
+ else
198
+ delta.round
199
+ end
200
+ end
201
+
202
+ # Unpack point deltas from packed data
203
+ #
204
+ # This is a complex operation that requires understanding the
205
+ # gvar delta encoding format. Simplified placeholder.
206
+ #
207
+ # @param data [String] Packed delta data
208
+ # @param point_count [Integer] Number of points
209
+ # @return [Hash] X and Y deltas
210
+ def unpack_point_deltas(_data, point_count)
211
+ {
212
+ x_deltas: Array.new(point_count, 0),
213
+ y_deltas: Array.new(point_count, 0),
214
+ }
215
+ end
216
+ end
217
+ end
218
+ end