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,418 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../optimizers/pattern_analyzer"
4
+ require_relative "../optimizers/subroutine_optimizer"
5
+
6
+ module Fontisan
7
+ module Variation
8
+ # Optimizes CFF subroutines for variable fonts
9
+ #
10
+ # This class analyzes CharStrings in CFF2 variable fonts and optimizes
11
+ # blend operations by extracting common blend sequences into subroutines,
12
+ # deduplicating variation regions, and minimizing ItemVariationStore data.
13
+ #
14
+ # Optimization strategies:
15
+ # 1. Blend pattern extraction - Find repeating blend sequences
16
+ # 2. Region deduplication - Merge identical variation regions
17
+ # 3. ItemVariationStore optimization - Compact delta storage
18
+ # 4. Subroutine reordering - Place frequent blends in low IDs
19
+ #
20
+ # @example Optimizing a variable font
21
+ # optimizer = Fontisan::Variation::Optimizer.new(cff2_table)
22
+ # optimized = optimizer.optimize
23
+ # # => Optimized CFF2 table with reduced file size
24
+ #
25
+ # @see docs/SUBROUTINE_ARCHITECTURE.md
26
+ # @see docs/CFF2_ARCHITECTURE.md
27
+ class Optimizer
28
+ # @return [CFF2] CFF2 table being optimized
29
+ attr_reader :cff2
30
+
31
+ # @return [Hash] Optimization statistics
32
+ attr_reader :stats
33
+
34
+ # Initialize optimizer
35
+ #
36
+ # @param cff2 [CFF2] CFF2 table with blend operators
37
+ # @param options [Hash] Optimization options
38
+ # @option options [Integer] :max_subrs Maximum subroutines (default: 65535)
39
+ # @option options [Float] :region_threshold Region similarity threshold (default: 0.001)
40
+ # @option options [Boolean] :deduplicate_regions Enable region deduplication (default: true)
41
+ def initialize(cff2, options = {})
42
+ @cff2 = cff2
43
+ @options = {
44
+ max_subrs: 65535,
45
+ region_threshold: 0.001,
46
+ deduplicate_regions: true,
47
+ }.merge(options)
48
+
49
+ @stats = {
50
+ original_size: 0,
51
+ optimized_size: 0,
52
+ blend_patterns_found: 0,
53
+ subroutines_created: 0,
54
+ regions_deduplicated: 0,
55
+ }
56
+ end
57
+
58
+ # Optimize CFF2 table
59
+ #
60
+ # Performs all optimization passes and returns optimized table.
61
+ #
62
+ # @return [CFF2] Optimized CFF2 table
63
+ def optimize
64
+ @stats[:original_size] = estimate_table_size(@cff2)
65
+
66
+ # Step 1: Analyze blend patterns across all CharStrings
67
+ blend_patterns = analyze_blend_patterns
68
+
69
+ # Step 2: Extract common blend sequences into subroutines
70
+ subroutines = extract_blend_subroutines(blend_patterns)
71
+
72
+ # Step 3: Deduplicate variation regions
73
+ deduplicate_regions if @options[:deduplicate_regions]
74
+
75
+ # Step 4: Optimize ItemVariationStore
76
+ optimize_item_variation_store
77
+
78
+ # Step 5: Rebuild CharStrings with subroutine calls
79
+ rebuild_charstrings(subroutines)
80
+
81
+ @stats[:optimized_size] = estimate_table_size(@cff2)
82
+ @stats[:savings_percent] = calculate_savings_percent
83
+
84
+ @cff2
85
+ end
86
+
87
+ # Analyze blend patterns in CharStrings
88
+ #
89
+ # Scans all CharStrings to find repeating blend operator sequences
90
+ # that can be extracted into subroutines.
91
+ #
92
+ # @return [Array<BlendPattern>] Identified blend patterns
93
+ def analyze_blend_patterns
94
+ patterns = []
95
+ glyph_count = @cff2.glyph_count
96
+
97
+ glyph_count.times do |glyph_id|
98
+ charstring = @cff2.charstring(glyph_id)
99
+ next unless charstring
100
+
101
+ # Extract blend operator sequences
102
+ blend_sequences = extract_blend_sequences(charstring)
103
+ patterns.concat(blend_sequences)
104
+ end
105
+
106
+ # Group identical patterns
107
+ grouped = group_patterns(patterns)
108
+ @stats[:blend_patterns_found] = grouped.length
109
+
110
+ grouped
111
+ end
112
+
113
+ # Extract blend sequences from CharString
114
+ #
115
+ # @param charstring [String] Binary CharString data
116
+ # @return [Array<BlendPattern>] Blend patterns found
117
+ def extract_blend_sequences(charstring)
118
+ patterns = []
119
+ operators = parse_charstring_operators(charstring)
120
+
121
+ operators.each_with_index do |op, index|
122
+ next unless blend_operator?(op)
123
+
124
+ # Extract blend and surrounding context
125
+ pattern = extract_pattern_context(operators, index)
126
+ patterns << pattern if pattern
127
+ end
128
+
129
+ patterns
130
+ end
131
+
132
+ # Extract common blend sequences into subroutines
133
+ #
134
+ # @param patterns [Array<BlendPattern>] Blend patterns
135
+ # @return [Array<Subroutine>] Created subroutines
136
+ def extract_blend_subroutines(patterns)
137
+ # Filter patterns by frequency and savings
138
+ candidates = patterns.select do |pattern|
139
+ pattern[:frequency] >= 2 && pattern[:savings].positive?
140
+ end
141
+
142
+ # Convert patterns to format expected by SubroutineOptimizer
143
+ # The optimizer expects objects with methods, but we have hashes
144
+ # For now, just select and order them directly
145
+ selected = candidates.sort_by { |p| -p[:savings] }
146
+ .take(@options[:max_subrs])
147
+
148
+ # Order by frequency for efficient encoding
149
+ ordered = selected.sort_by { |p| -p[:frequency] }
150
+
151
+ @stats[:subroutines_created] = ordered.length
152
+ ordered
153
+ end
154
+
155
+ # Deduplicate variation regions
156
+ #
157
+ # Merges regions that are functionally identical (within threshold).
158
+ def deduplicate_regions
159
+ return unless @cff2.variation_store
160
+
161
+ regions = @cff2.variation_store.region_list
162
+ original_count = regions.length
163
+
164
+ # Find duplicate regions
165
+ unique_regions = []
166
+ region_mapping = {}
167
+
168
+ regions.each_with_index do |region, index|
169
+ # Check if region matches any existing unique region
170
+ match_index = find_matching_region(region, unique_regions)
171
+
172
+ if match_index
173
+ region_mapping[index] = match_index
174
+ else
175
+ region_mapping[index] = unique_regions.length
176
+ unique_regions << region
177
+ end
178
+ end
179
+
180
+ # Update references in ItemVariationStore
181
+ update_region_references(region_mapping) if regions.length > unique_regions.length
182
+
183
+ @cff2.variation_store.region_list = unique_regions
184
+ @stats[:regions_deduplicated] = original_count - unique_regions.length
185
+ end
186
+
187
+ # Find matching region within threshold
188
+ #
189
+ # @param region [RegionAxisCoordinates] Region to match
190
+ # @param unique_regions [Array<RegionAxisCoordinates>] Existing unique regions
191
+ # @return [Integer, nil] Index of matching region or nil
192
+ def find_matching_region(region, unique_regions)
193
+ unique_regions.each_with_index do |unique, index|
194
+ return index if regions_match?(region, unique)
195
+ end
196
+ nil
197
+ end
198
+
199
+ # Check if two regions match within threshold
200
+ #
201
+ # @param r1 [RegionAxisCoordinates] First region
202
+ # @param r2 [RegionAxisCoordinates] Second region
203
+ # @return [Boolean] True if regions match
204
+ def regions_match?(r1, r2)
205
+ return false unless r1.axis_count == r2.axis_count
206
+
207
+ r1.axis_count.times do |i|
208
+ coords1 = r1.region_axes[i]
209
+ coords2 = r2.region_axes[i]
210
+
211
+ # Compare start, peak, end coordinates
212
+ return false unless coords_similar?(coords1.start_coord, coords2.start_coord)
213
+ return false unless coords_similar?(coords1.peak_coord, coords2.peak_coord)
214
+ return false unless coords_similar?(coords1.end_coord, coords2.end_coord)
215
+ end
216
+
217
+ true
218
+ end
219
+
220
+ # Check if coordinates are similar within threshold
221
+ #
222
+ # @param c1 [Float] First coordinate
223
+ # @param c2 [Float] Second coordinate
224
+ # @return [Boolean] True if similar
225
+ def coords_similar?(c1, c2)
226
+ (c1 - c2).abs <= @options[:region_threshold]
227
+ end
228
+
229
+ # Optimize ItemVariationStore
230
+ #
231
+ # Compacts delta storage by removing unused data and optimizing encoding.
232
+ def optimize_item_variation_store
233
+ return unless @cff2.variation_store
234
+
235
+ store = @cff2.variation_store
236
+
237
+ # Remove unused variation data
238
+ compact_variation_data(store)
239
+
240
+ # Optimize delta encoding (use shortest representation)
241
+ optimize_delta_encoding(store)
242
+ end
243
+
244
+ # Compact variation data by removing unused entries
245
+ #
246
+ # @param store [ItemVariationStore] Variation store
247
+ def compact_variation_data(store)
248
+ # Identify used variation indices from CharStrings
249
+ used_indices = collect_used_variation_indices
250
+
251
+ # Remove unused data
252
+ store.item_variation_data.each do |data|
253
+ data.compact_unused(used_indices)
254
+ end
255
+ end
256
+
257
+ # Optimize delta encoding for efficiency
258
+ #
259
+ # @param store [ItemVariationStore] Variation store
260
+ def optimize_delta_encoding(store)
261
+ store.item_variation_data.each(&:optimize_encoding)
262
+ end
263
+
264
+ # Rebuild CharStrings with subroutine calls
265
+ #
266
+ # @param subroutines [Array<Subroutine>] Subroutines to use
267
+ def rebuild_charstrings(subroutines)
268
+ return if subroutines.empty?
269
+
270
+ glyph_count = @cff2.glyph_count
271
+
272
+ glyph_count.times do |glyph_id|
273
+ charstring = @cff2.charstring(glyph_id)
274
+ next unless charstring
275
+
276
+ # Rewrite CharString to use subroutines
277
+ optimized = rewrite_with_subroutines(charstring, subroutines)
278
+ @cff2.set_charstring(glyph_id, optimized)
279
+ end
280
+
281
+ # Update subroutine index in CFF2
282
+ @cff2.local_subr_index = subroutines
283
+ end
284
+
285
+ # Get optimization statistics
286
+ #
287
+ # @return [Hash] Statistics about optimization
288
+ def statistics
289
+ @stats
290
+ end
291
+
292
+ private
293
+
294
+ # Parse CharString to operators
295
+ #
296
+ # @param charstring [String] Binary CharString data
297
+ # @return [Array<Hash>] Operators with operands
298
+ def parse_charstring_operators(_charstring)
299
+ # Placeholder - would parse binary CharString format
300
+ # Returns array of { operator:, operands:, position: }
301
+ []
302
+ end
303
+
304
+ # Check if operator is a blend operator
305
+ #
306
+ # @param operator [Hash] Operator data
307
+ # @return [Boolean] True if blend operator
308
+ def blend_operator?(operator)
309
+ operator[:operator] == :blend
310
+ end
311
+
312
+ # Extract pattern with surrounding context
313
+ #
314
+ # @param operators [Array<Hash>] All operators
315
+ # @param blend_index [Integer] Index of blend operator
316
+ # @return [Hash, nil] Pattern data
317
+ def extract_pattern_context(_operators, _blend_index)
318
+ # Extract blend and preceding operands
319
+ # Returns { sequence:, frequency:, savings:, positions: }
320
+ nil
321
+ end
322
+
323
+ # Group identical patterns
324
+ #
325
+ # @param patterns [Array<BlendPattern>] Raw patterns
326
+ # @return [Array<BlendPattern>] Grouped patterns with frequency
327
+ def group_patterns(patterns)
328
+ grouped = {}
329
+
330
+ patterns.each do |pattern|
331
+ key = pattern_key(pattern)
332
+ grouped[key] ||= pattern.dup
333
+ grouped[key][:frequency] ||= 0
334
+ grouped[key][:frequency] += 1
335
+ end
336
+
337
+ grouped.values
338
+ end
339
+
340
+ # Generate key for pattern grouping
341
+ #
342
+ # @param pattern [Hash] Pattern data
343
+ # @return [String] Unique key
344
+ def pattern_key(pattern)
345
+ pattern[:sequence].join(",")
346
+ end
347
+
348
+ # Collect variation indices used in CharStrings
349
+ #
350
+ # @return [Set<Integer>] Set of used indices
351
+ def collect_used_variation_indices
352
+ require "set"
353
+ used = Set.new
354
+
355
+ glyph_count = @cff2.glyph_count
356
+ glyph_count.times do |glyph_id|
357
+ charstring = @cff2.charstring(glyph_id)
358
+ next unless charstring
359
+
360
+ # Extract variation indices from CharString
361
+ indices = extract_variation_indices(charstring)
362
+ used.merge(indices)
363
+ end
364
+
365
+ used
366
+ end
367
+
368
+ # Extract variation indices from CharString
369
+ #
370
+ # @param charstring [String] Binary CharString
371
+ # @return [Array<Integer>] Variation indices
372
+ def extract_variation_indices(_charstring)
373
+ # Placeholder - would parse vsindex operators
374
+ []
375
+ end
376
+
377
+ # Update region references after deduplication
378
+ #
379
+ # @param mapping [Hash<Integer, Integer>] Old index => new index
380
+ def update_region_references(mapping)
381
+ store = @cff2.variation_store
382
+
383
+ store.item_variation_data.each do |data|
384
+ data.update_region_indices(mapping)
385
+ end
386
+ end
387
+
388
+ # Rewrite CharString with subroutine calls
389
+ #
390
+ # @param charstring [String] Original CharString
391
+ # @param subroutines [Array<Subroutine>] Available subroutines
392
+ # @return [String] Optimized CharString
393
+ def rewrite_with_subroutines(charstring, _subroutines)
394
+ # Placeholder - would replace patterns with callsubr operators
395
+ charstring
396
+ end
397
+
398
+ # Estimate table size in bytes
399
+ #
400
+ # @param cff2 [CFF2] CFF2 table
401
+ # @return [Integer] Estimated size
402
+ def estimate_table_size(_cff2)
403
+ # Placeholder - would calculate actual binary size
404
+ 0
405
+ end
406
+
407
+ # Calculate savings percentage
408
+ #
409
+ # @return [Float] Percentage saved
410
+ def calculate_savings_percent
411
+ return 0.0 if @stats[:original_size].zero?
412
+
413
+ saved = @stats[:original_size] - @stats[:optimized_size]
414
+ (saved.to_f / @stats[:original_size]) * 100.0
415
+ end
416
+ end
417
+ end
418
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_generator"
4
+ require_relative "cache"
5
+ require_relative "../utils/thread_pool"
6
+ require "etc"
7
+
8
+ module Fontisan
9
+ module Variation
10
+ # Generates multiple font instances in parallel
11
+ #
12
+ # Uses thread pool for efficient batch processing with caching.
13
+ # Supports progress tracking and graceful error handling per instance.
14
+ #
15
+ # @example Basic batch generation
16
+ # generator = ParallelGenerator.new(font)
17
+ # coordinates_list = [
18
+ # { "wght" => 300 },
19
+ # { "wght" => 700 }
20
+ # ]
21
+ # instances = generator.generate_batch(coordinates_list)
22
+ #
23
+ # @example With progress callback
24
+ # generator.generate_batch(coordinates_list) do |index, total|
25
+ # puts "Generated #{index}/#{total}"
26
+ # end
27
+ #
28
+ # @example Custom thread count
29
+ # generator = ParallelGenerator.new(font, threads: 8)
30
+ class ParallelGenerator
31
+ # @return [TrueTypeFont, OpenTypeFont] Variable font
32
+ attr_reader :font
33
+
34
+ # @return [ThreadSafeCache] Thread-safe cache
35
+ attr_reader :cache
36
+
37
+ # @return [Integer] Number of threads
38
+ attr_reader :thread_count
39
+
40
+ # Initialize parallel generator
41
+ #
42
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
43
+ # @param options [Hash] Options
44
+ # @option options [ThreadSafeCache] :cache Cache instance (creates new if not provided)
45
+ # @option options [Integer] :threads Thread count (default: max(4, processor_count))
46
+ def initialize(font, options = {})
47
+ @font = font
48
+ @cache = options[:cache] || ThreadSafeCache.new
49
+ @thread_count = options[:threads] || [4, Etc.nprocessors].max
50
+ end
51
+
52
+ # Generate multiple instances in parallel
53
+ #
54
+ # Processes each coordinate set in parallel using thread pool.
55
+ # Returns results in same order as input coordinates.
56
+ #
57
+ # @param coordinates_list [Array<Hash>] List of coordinate sets
58
+ # @yield [index, total] Progress callback (optional)
59
+ # @yieldparam index [Integer] Current completed count
60
+ # @yieldparam total [Integer] Total count
61
+ # @return [Array<Hash>] Generated instances with metadata
62
+ def generate_batch(coordinates_list, &progress_callback)
63
+ return [] if coordinates_list.empty?
64
+
65
+ total = coordinates_list.length
66
+ results = Array.new(total)
67
+ completed = 0
68
+ mutex = Mutex.new
69
+
70
+ # Create thread pool
71
+ pool = Fontisan::Utils::ThreadPool.new(@thread_count)
72
+
73
+ # Schedule all jobs
74
+ futures = coordinates_list.map.with_index do |coordinates, index|
75
+ pool.schedule do
76
+ {
77
+ index: index,
78
+ result: generate_with_cache(coordinates),
79
+ }
80
+ end
81
+ end
82
+
83
+ # Collect results
84
+ futures.each do |future|
85
+ job_result = future.value
86
+ results[job_result[:index]] = job_result[:result]
87
+
88
+ # Update progress
89
+ if progress_callback
90
+ mutex.synchronize do
91
+ completed += 1
92
+ yield(completed, total)
93
+ end
94
+ end
95
+ end
96
+
97
+ # Shutdown pool
98
+ pool.shutdown
99
+
100
+ results
101
+ end
102
+
103
+ # Generate instance with caching
104
+ #
105
+ # Uses cache to avoid regenerating identical instances.
106
+ #
107
+ # @param coordinates [Hash<String, Float>] Design space coordinates
108
+ # @return [Hash] Instance data with metadata
109
+ def generate_with_cache(coordinates)
110
+ font_checksum = calculate_font_checksum
111
+
112
+ begin
113
+ tables = @cache.fetch_instance(font_checksum, coordinates) do
114
+ generator = InstanceGenerator.new(@font, coordinates)
115
+ generator.generate
116
+ end
117
+
118
+ {
119
+ success: true,
120
+ coordinates: coordinates,
121
+ tables: tables,
122
+ error: nil,
123
+ }
124
+ rescue StandardError => e
125
+ {
126
+ success: false,
127
+ coordinates: coordinates,
128
+ tables: nil,
129
+ error: {
130
+ message: e.message,
131
+ class: e.class.name,
132
+ backtrace: e.backtrace&.first(5),
133
+ },
134
+ }
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ # Calculate font checksum for cache key
141
+ #
142
+ # @return [String] Font identifier
143
+ def calculate_font_checksum
144
+ # Use combination of table checksums for quick identification
145
+ # In production, might use actual checksum from head table
146
+ "font_#{@font.object_id}"
147
+ end
148
+ end
149
+ end
150
+ end