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,375 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "interpolator"
4
+ require_relative "table_accessor"
5
+
6
+ module Fontisan
7
+ module Variation
8
+ # Converts variation data between TrueType (gvar) and CFF2 (blend) formats
9
+ #
10
+ # This class enables format conversion while preserving variation data:
11
+ # - gvar tuples → CFF2 blend operators
12
+ # - CFF2 blend operators → gvar tuples
13
+ #
14
+ # Process for gvar → blend:
15
+ # 1. Extract tuple variations from gvar
16
+ # 2. Map tuple regions to blend regions
17
+ # 3. Embed blend operators in CharStrings at control points
18
+ # 4. Encode delta values in blend format
19
+ #
20
+ # Process for blend → gvar:
21
+ # 1. Parse CharStrings with blend operators
22
+ # 2. Extract blend deltas and regions
23
+ # 3. Map to gvar tuple format
24
+ # 4. Build gvar table structure
25
+ #
26
+ # @example Converting gvar to CFF2 blend
27
+ # converter = Converter.new(font, axes)
28
+ # blend_data = converter.gvar_to_blend(glyph_id)
29
+ #
30
+ # @example Converting CFF2 blend to gvar
31
+ # converter = Converter.new(font, axes)
32
+ # tuple_data = converter.blend_to_gvar(glyph_id)
33
+ class Converter
34
+ include TableAccessor
35
+
36
+ # @return [TrueTypeFont, OpenTypeFont] Font instance
37
+ attr_reader :font
38
+
39
+ # @return [Array<VariationAxisRecord>] Variation axes
40
+ attr_reader :axes
41
+
42
+ # Initialize converter
43
+ #
44
+ # @param font [TrueTypeFont, OpenTypeFont] Font instance
45
+ # @param axes [Array<VariationAxisRecord>] Variation axes from fvar
46
+ def initialize(font, axes)
47
+ @font = font
48
+ @axes = axes || []
49
+ @variation_tables = {}
50
+ end
51
+
52
+ # Convert gvar tuples to CFF2 blend format for a glyph
53
+ #
54
+ # @param glyph_id [Integer] Glyph ID
55
+ # @return [Hash, nil] Blend data or nil
56
+ def gvar_to_blend(glyph_id)
57
+ return nil unless has_variation_table?("gvar")
58
+ return nil unless has_variation_table?("glyf")
59
+
60
+ gvar = variation_table("gvar")
61
+ return nil unless gvar
62
+
63
+ # Get tuple variations for this glyph
64
+ tuple_data = gvar.glyph_tuple_variations(glyph_id)
65
+ return nil unless tuple_data
66
+
67
+ # Convert tuples to blend format
68
+ convert_tuples_to_blend(tuple_data)
69
+ end
70
+
71
+ # Convert CFF2 blend operators to gvar tuple format for a glyph
72
+ #
73
+ # @param glyph_id [Integer] Glyph ID
74
+ # @return [Hash, nil] Tuple data or nil
75
+ def blend_to_gvar(glyph_id)
76
+ return nil unless has_variation_table?("CFF2")
77
+
78
+ cff2 = variation_table("CFF2")
79
+ return nil unless cff2
80
+
81
+ # Get CharString with blend operators
82
+ charstring = cff2.charstring_for_glyph(glyph_id)
83
+ return nil unless charstring
84
+
85
+ # Parse CharString to extract blend data
86
+ charstring.parse unless charstring.instance_variable_get(:@parsed)
87
+ blend_data = charstring.blend_data
88
+ return nil if blend_data.nil? || blend_data.empty?
89
+
90
+ # Convert blend data to tuple format
91
+ convert_blend_to_tuples_for_glyph(blend_data)
92
+ end
93
+
94
+ # Convert all glyphs from gvar to blend format
95
+ #
96
+ # @param glyph_count [Integer] Number of glyphs
97
+ # @return [Hash<Integer, Hash>] Map of glyph_id to blend data
98
+ def convert_all_gvar_to_blend(glyph_count)
99
+ return {} unless can_convert?
100
+
101
+ (0...glyph_count).each_with_object({}) do |glyph_id, result|
102
+ blend_data = gvar_to_blend(glyph_id)
103
+ result[glyph_id] = blend_data if blend_data
104
+ end
105
+ end
106
+
107
+ # Convert all glyphs from blend to gvar format
108
+ #
109
+ # @param glyph_count [Integer] Number of glyphs
110
+ # @return [Hash<Integer, Hash>] Map of glyph_id to tuple data
111
+ def convert_all_blend_to_gvar(glyph_count)
112
+ return {} unless can_convert?
113
+
114
+ (0...glyph_count).each_with_object({}) do |glyph_id, result|
115
+ tuple_data = blend_to_gvar(glyph_id)
116
+ result[glyph_id] = tuple_data if tuple_data
117
+ end
118
+ end
119
+
120
+ # Check if variation data can be converted
121
+ #
122
+ # @return [Boolean] True if conversion possible
123
+ def can_convert?
124
+ !@axes.empty? && (
125
+ has_variation_table?("gvar") ||
126
+ has_variation_table?("CFF2")
127
+ )
128
+ end
129
+
130
+ private
131
+
132
+ # Convert blend data from a glyph to tuple format
133
+ #
134
+ # @param blend_data [Array<Hash>] Array of blend operations
135
+ # @return [Hash] Tuple variation data
136
+ def convert_blend_to_tuples_for_glyph(blend_data)
137
+ # Each blend operation represents variation at different points
138
+ # We need to aggregate these into region-based tuples
139
+
140
+ # Extract all regions from blend operations
141
+ regions_map = {}
142
+ point_count = 0
143
+
144
+ blend_data.each_with_index do |blend_op, idx|
145
+ blend_op[:blends].each do |blend|
146
+ # Track the maximum point index we've seen
147
+ point_count = [point_count, idx + 1].max
148
+
149
+ # For each delta axis, we need to create or update a region
150
+ blend[:deltas].each_with_index do |delta, axis_index|
151
+ next if delta.zero? # Skip zero deltas
152
+
153
+ # Create region key based on unique delta pattern
154
+ region_key = "region_#{axis_index}"
155
+
156
+ regions_map[region_key] ||= {
157
+ axis_index: axis_index,
158
+ deltas_per_point: Array.new(point_count) { { x: 0, y: 0 } },
159
+ }
160
+
161
+ # Store this delta for this point
162
+ # Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
163
+ # This is a simplified mapping - full implementation would track
164
+ # which coordinates are being varied
165
+ regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0, y: 0 }
166
+ if idx.even?
167
+ regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
168
+ else
169
+ regions_map[region_key][:deltas_per_point][idx / 2][:y] = delta
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ # Convert regions to tuples
176
+ tuples = []
177
+ regions_map.each_value do |region_data|
178
+ axis_index = region_data[:axis_index]
179
+
180
+ # Build peak coordinates (one per axis)
181
+ peak = Array.new(@axes.length, 0.0)
182
+ peak[axis_index] = 1.0 if axis_index < @axes.length
183
+
184
+ # Build start/end (default full range)
185
+ start_vals = Array.new(@axes.length, -1.0)
186
+ end_vals = Array.new(@axes.length, 1.0)
187
+
188
+ tuples << {
189
+ peak: peak,
190
+ start: start_vals,
191
+ end: end_vals,
192
+ deltas: region_data[:deltas_per_point],
193
+ }
194
+ end
195
+
196
+ {
197
+ tuples: tuples,
198
+ point_count: point_count,
199
+ }
200
+ end
201
+
202
+ # Convert tuple variations to blend format
203
+ #
204
+ # @param tuple_data [Hash] Tuple variation data from gvar
205
+ # @return [Hash] Blend format data
206
+ def convert_tuples_to_blend(tuple_data)
207
+ tuples = tuple_data[:tuples] || []
208
+ point_count = tuple_data[:point_count] || 0
209
+
210
+ # Build blend regions from tuples
211
+ regions = tuples.map { |tuple| build_region_from_tuple(tuple) }
212
+
213
+ # Extract deltas for each point
214
+ point_deltas = extract_point_deltas(tuples, point_count)
215
+
216
+ {
217
+ regions: regions,
218
+ point_deltas: point_deltas,
219
+ num_regions: regions.length,
220
+ num_axes: @axes.length,
221
+ }
222
+ end
223
+
224
+ # Build region from tuple peak/start/end coordinates
225
+ #
226
+ # @param tuple [Hash] Tuple data with :peak, :start, :end
227
+ # @return [Hash] Region definition
228
+ def build_region_from_tuple(tuple)
229
+ region = {}
230
+
231
+ @axes.each_with_index do |axis, axis_index|
232
+ # Extract coordinates for this axis
233
+ peak = tuple[:peak] ? tuple[:peak][axis_index] : 0.0
234
+ start_val = tuple[:start] ? tuple[:start][axis_index] : -1.0
235
+ end_val = tuple[:end] ? tuple[:end][axis_index] : 1.0
236
+
237
+ region[axis.axis_tag] = {
238
+ start: start_val,
239
+ peak: peak,
240
+ end: end_val,
241
+ }
242
+ end
243
+
244
+ region
245
+ end
246
+
247
+ # Extract point deltas from all tuples
248
+ #
249
+ # @param tuples [Array<Hash>] Tuple variations
250
+ # @param point_count [Integer] Number of points
251
+ # @return [Array<Array<Hash>>] Deltas per point per tuple
252
+ def extract_point_deltas(tuples, point_count)
253
+ return [] if point_count.zero?
254
+
255
+ # Initialize deltas array
256
+ point_deltas = Array.new(point_count) { [] }
257
+
258
+ # For each tuple, extract deltas for all points
259
+ tuples.each do |tuple|
260
+ deltas = parse_tuple_deltas(tuple, point_count)
261
+
262
+ deltas.each_with_index do |delta, point_index|
263
+ point_deltas[point_index] << delta
264
+ end
265
+ end
266
+
267
+ point_deltas
268
+ end
269
+
270
+ # Parse deltas from a tuple
271
+ #
272
+ # @param tuple [Hash] Tuple data
273
+ # @param point_count [Integer] Number of points
274
+ # @return [Array<Hash>] Deltas with :x and :y
275
+ def parse_tuple_deltas(tuple, point_count)
276
+ # If tuple has deltas array, use it
277
+ if tuple[:deltas].is_a?(Array)
278
+ return tuple[:deltas].map do |delta|
279
+ { x: delta[:x] || 0, y: delta[:y] || 0 }
280
+ end
281
+ end
282
+
283
+ # Otherwise return zeros (placeholder for parsing raw delta data)
284
+ # Full implementation would:
285
+ # 1. Parse delta data from tuple[:data]
286
+ # 2. Decompress if needed
287
+ # 3. Return array of { x: dx, y: dy } for each point
288
+ Array.new(point_count) { { x: 0, y: 0 } }
289
+ end
290
+
291
+ # Convert blend data to tuple format
292
+ #
293
+ # @param blend_data [Hash] Blend format data
294
+ # @return [Hash] Tuple variation data
295
+ def convert_blend_to_tuples(blend_data)
296
+ regions = blend_data[:regions] || []
297
+ point_deltas = blend_data[:point_deltas] || []
298
+
299
+ # Build tuples from regions
300
+ tuples = regions.map.with_index do |region, region_index|
301
+ build_tuple_from_region(region, point_deltas, region_index)
302
+ end
303
+
304
+ {
305
+ tuples: tuples,
306
+ point_count: point_deltas.length,
307
+ }
308
+ end
309
+
310
+ # Build tuple from region and deltas
311
+ #
312
+ # @param region [Hash] Region definition
313
+ # @param point_deltas [Array<Array<Hash>>] Deltas per point
314
+ # @param region_index [Integer] Region index
315
+ # @return [Hash] Tuple data
316
+ def build_tuple_from_region(region, point_deltas, region_index)
317
+ # Extract peak, start, end for all axes
318
+ peak = Array.new(@axes.length, 0.0)
319
+ start_vals = Array.new(@axes.length, -1.0)
320
+ end_vals = Array.new(@axes.length, 1.0)
321
+
322
+ @axes.each_with_index do |axis, axis_index|
323
+ axis_region = region[axis.axis_tag]
324
+ next unless axis_region
325
+
326
+ peak[axis_index] = axis_region[:peak]
327
+ start_vals[axis_index] = axis_region[:start]
328
+ end_vals[axis_index] = axis_region[:end]
329
+ end
330
+
331
+ # Extract deltas for this region
332
+ deltas = point_deltas.map do |point_delta_set|
333
+ point_delta_set[region_index] || { x: 0, y: 0 }
334
+ end
335
+
336
+ {
337
+ peak: peak,
338
+ start: start_vals,
339
+ end: end_vals,
340
+ deltas: deltas,
341
+ }
342
+ end
343
+
344
+ # Encode deltas in CharString blend format
345
+ #
346
+ # @param base_value [Numeric] Base value
347
+ # @param deltas [Array<Numeric>] Delta values
348
+ # @return [Array<Numeric>] Blend operator arguments
349
+ def encode_blend_operator(base_value, deltas)
350
+ # CFF2 blend format: base_value delta1 delta2 ... K N blend
351
+ # Where K = number of deltas, N = number of blend operations
352
+ [base_value] + deltas + [deltas.length, 1]
353
+ end
354
+
355
+ # Decode blend operator arguments to base and deltas
356
+ #
357
+ # @param args [Array<Numeric>] Blend operator arguments
358
+ # @return [Hash] Base value and deltas
359
+ def decode_blend_operator(args)
360
+ return { base: 0, deltas: [] } if args.length < 3
361
+
362
+ # Last two values are K and N
363
+ k = args[-2]
364
+ _n = args[-1]
365
+
366
+ # Before K and N: base + deltas
367
+ values = args[0...-2]
368
+ base = values[0] || 0
369
+ deltas = values[1, k] || []
370
+
371
+ { base: base, deltas: deltas }
372
+ end
373
+ end
374
+ end
375
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "variation_context"
4
+
5
+ module Fontisan
6
+ module Variation
7
+ # Extracts variation data from OpenType variable fonts
8
+ #
9
+ # This class provides a unified interface to extract variation information
10
+ # from variable fonts, including:
11
+ # - Variation axes (from fvar table)
12
+ # - Named instances (from fvar table)
13
+ # - Variation type (TrueType gvar or PostScript CFF2)
14
+ #
15
+ # @example Extracting variation data
16
+ # extractor = Fontisan::Variation::DataExtractor.new(font)
17
+ # data = extractor.extract
18
+ # if data
19
+ # puts "Axes: #{data[:axes].map(&:axis_tag).join(', ')}"
20
+ # puts "Instances: #{data[:instances].length}"
21
+ # end
22
+ class DataExtractor
23
+ # Initialize extractor with a font
24
+ #
25
+ # @param font [TrueTypeFont, OpenTypeFont] Font to extract from
26
+ def initialize(font)
27
+ @font = font
28
+ @context = VariationContext.new(font)
29
+ end
30
+
31
+ # Extract variation data from the font
32
+ #
33
+ # @return [Hash, nil] Variation data or nil if not a variable font
34
+ def extract
35
+ return nil unless @context.variable_font?
36
+
37
+ {
38
+ axes: extract_axes,
39
+ instances: extract_instances,
40
+ has_gvar: @font.has_table?("gvar"),
41
+ has_cff2: @font.has_table?("CFF2"),
42
+ variation_type: @context.variation_type,
43
+ }
44
+ end
45
+
46
+ # Check if font is a variable font
47
+ #
48
+ # @return [Boolean] True if font has fvar table
49
+ def variable_font?
50
+ @context.variable_font?
51
+ end
52
+
53
+ private
54
+
55
+ # Extract variation axes from fvar table
56
+ #
57
+ # @return [Array<VariationAxisRecord>] Array of axis records
58
+ # @raise [VariationDataCorruptedError] If axes cannot be extracted
59
+ def extract_axes
60
+ return [] unless @context.fvar
61
+
62
+ @context.axes
63
+ rescue StandardError => e
64
+ raise VariationDataCorruptedError.new(
65
+ message: "Failed to extract variation axes: #{e.message}",
66
+ details: { error_class: e.class.name },
67
+ )
68
+ end
69
+
70
+ # Extract named instances from fvar table
71
+ #
72
+ # @return [Array<Hash>] Array of instance information
73
+ # @raise [VariationDataCorruptedError] If instances cannot be extracted
74
+ def extract_instances
75
+ return [] unless @context.fvar
76
+
77
+ @context.fvar.instances || []
78
+ rescue StandardError => e
79
+ raise VariationDataCorruptedError.new(
80
+ message: "Failed to extract instances: #{e.message}",
81
+ details: { error_class: e.class.name },
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end