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,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "interpolator"
4
+ require_relative "region_matcher"
5
+ require_relative "table_accessor"
6
+
7
+ module Fontisan
8
+ module Variation
9
+ # Applies metrics variation deltas to font metrics tables
10
+ #
11
+ # This class handles applying variation deltas from HVAR, VVAR, and MVAR
12
+ # tables to the corresponding metrics tables (hmtx, vmtx, head, hhea, etc.).
13
+ #
14
+ # Process:
15
+ # 1. Parse ItemVariationStore from HVAR/VVAR/MVAR
16
+ # 2. Calculate scalars for current coordinates using regions
17
+ # 3. Apply deltas to base metrics
18
+ # 4. Update metrics tables with adjusted values
19
+ #
20
+ # @example Applying HVAR deltas
21
+ # adjuster = MetricsAdjuster.new(font, interpolator)
22
+ # adjuster.apply_hvar_deltas({ "wght" => 700.0 })
23
+ class MetricsAdjuster
24
+ include TableAccessor
25
+
26
+ # @return [TrueTypeFont, OpenTypeFont] Font instance
27
+ attr_reader :font
28
+
29
+ # @return [Interpolator] Coordinate interpolator
30
+ attr_reader :interpolator
31
+
32
+ # Initialize metrics adjuster
33
+ #
34
+ # @param font [TrueTypeFont, OpenTypeFont] Font instance
35
+ # @param interpolator [Interpolator] Coordinate interpolator
36
+ def initialize(font, interpolator)
37
+ @font = font
38
+ @interpolator = interpolator
39
+ @variation_tables = {}
40
+ end
41
+
42
+ # Apply HVAR deltas to horizontal metrics
43
+ #
44
+ # @param coordinates [Hash<String, Float>] Design space coordinates
45
+ # @return [Boolean] True if deltas were applied
46
+ def apply_hvar_deltas(coordinates)
47
+ return false unless has_variation_table?("HVAR")
48
+ return false unless has_variation_table?("hmtx")
49
+
50
+ hvar = variation_table("HVAR")
51
+ return false unless hvar&.item_variation_store
52
+
53
+ # Parse region list and calculate scalars
54
+ regions = extract_regions_from_store(hvar.item_variation_store)
55
+ return false if regions.empty?
56
+
57
+ scalars = @interpolator.calculate_scalars(coordinates, regions)
58
+
59
+ # Get hmtx table
60
+ hmtx = variation_table("hmtx")
61
+ return false unless hmtx&.parsed?
62
+
63
+ # Get glyph count
64
+ maxp = variation_table("maxp")
65
+ glyph_count = maxp ? maxp.num_glyphs : 0
66
+ return false if glyph_count.zero?
67
+
68
+ # Apply deltas to each glyph
69
+ adjusted_metrics = apply_horizontal_metrics_deltas(
70
+ hvar, hmtx, glyph_count, scalars
71
+ )
72
+
73
+ # Rebuild hmtx table with adjusted metrics
74
+ rebuild_hmtx_table(adjusted_metrics)
75
+
76
+ true
77
+ end
78
+
79
+ # Apply VVAR deltas to vertical metrics
80
+ #
81
+ # @param coordinates [Hash<String, Float>] Design space coordinates
82
+ # @return [Boolean] True if deltas were applied
83
+ def apply_vvar_deltas(coordinates)
84
+ return false unless has_variation_table?("VVAR")
85
+ return false unless has_variation_table?("vmtx")
86
+
87
+ vvar = variation_table("VVAR")
88
+ return false unless vvar&.item_variation_store
89
+
90
+ # Parse region list and calculate scalars
91
+ regions = extract_regions_from_store(vvar.item_variation_store)
92
+ return false if regions.empty?
93
+
94
+ @interpolator.calculate_scalars(coordinates, regions)
95
+
96
+ # Apply deltas to vmtx
97
+ # Similar to HVAR but for vertical metrics
98
+ # Placeholder for full implementation
99
+
100
+ true
101
+ end
102
+
103
+ # Apply MVAR deltas to font-wide metrics
104
+ #
105
+ # @param coordinates [Hash<String, Float>] Design space coordinates
106
+ # @return [Boolean] True if deltas were applied
107
+ def apply_mvar_deltas(coordinates)
108
+ return false unless has_variation_table?("MVAR")
109
+
110
+ mvar = variation_table("MVAR")
111
+ return false unless mvar&.item_variation_store
112
+
113
+ # Parse region list and calculate scalars
114
+ regions = extract_regions_from_store(mvar.item_variation_store)
115
+ return false if regions.empty?
116
+
117
+ scalars = @interpolator.calculate_scalars(coordinates, regions)
118
+
119
+ # Apply deltas to each metric tag
120
+ apply_font_wide_metrics_deltas(mvar, scalars)
121
+
122
+ true
123
+ end
124
+
125
+ private
126
+
127
+ # Extract regions from ItemVariationStore
128
+ #
129
+ # @param store [VariationCommon::ItemVariationStore] Variation store
130
+ # @return [Array<Hash>] Array of region definitions
131
+ def extract_regions_from_store(store)
132
+ region_list = store.variation_region_list
133
+ return [] unless region_list
134
+
135
+ regions = []
136
+ region_list.regions.each do |region_coords|
137
+ region = {}
138
+ region_coords.each_with_index do |axis_coords, axis_index|
139
+ next if axis_index >= @interpolator.axes.length
140
+
141
+ axis = @interpolator.axes[axis_index]
142
+ region[axis.axis_tag] = {
143
+ start: axis_coords.start,
144
+ peak: axis_coords.peak,
145
+ end: axis_coords.end_value,
146
+ }
147
+ end
148
+ regions << region
149
+ end
150
+
151
+ regions
152
+ end
153
+
154
+ # Apply horizontal metrics deltas
155
+ #
156
+ # @param hvar [Hvar] HVAR table
157
+ # @param hmtx [Hmtx] hmtx table
158
+ # @param glyph_count [Integer] Number of glyphs
159
+ # @param scalars [Array<Float>] Region scalars
160
+ # @return [Array<Hash>] Adjusted metrics
161
+ def apply_horizontal_metrics_deltas(hvar, hmtx, glyph_count, scalars)
162
+ adjusted_metrics = []
163
+
164
+ glyph_count.times do |glyph_id|
165
+ base_metric = hmtx.metric_for(glyph_id)
166
+ next unless base_metric
167
+
168
+ # Get advance width deltas
169
+ advance_deltas = hvar.advance_width_delta_set(glyph_id) || []
170
+ lsb_deltas = hvar.lsb_delta_set(glyph_id) || []
171
+
172
+ # Apply deltas using scalars
173
+ new_advance = @interpolator.interpolate_value(
174
+ base_metric[:advance_width],
175
+ advance_deltas,
176
+ scalars,
177
+ ).round
178
+
179
+ new_lsb = @interpolator.interpolate_value(
180
+ base_metric[:lsb],
181
+ lsb_deltas,
182
+ scalars,
183
+ ).round
184
+
185
+ adjusted_metrics << {
186
+ advance_width: new_advance,
187
+ lsb: new_lsb,
188
+ }
189
+ end
190
+
191
+ adjusted_metrics
192
+ end
193
+
194
+ # Apply font-wide metrics deltas
195
+ #
196
+ # @param mvar [Mvar] MVAR table
197
+ # @param scalars [Array<Float>] Region scalars
198
+ def apply_font_wide_metrics_deltas(mvar, scalars)
199
+ # Get all metric tags
200
+ mvar.metric_tags.each do |tag|
201
+ delta_set = mvar.metric_delta_set(tag)
202
+ next unless delta_set
203
+
204
+ # Get base value for this metric
205
+ base_value = get_base_metric_value(tag)
206
+ next unless base_value
207
+
208
+ # Apply deltas
209
+ new_value = @interpolator.interpolate_value(
210
+ base_value,
211
+ delta_set,
212
+ scalars,
213
+ ).round
214
+
215
+ # Update metric in appropriate table
216
+ update_font_metric(tag, new_value)
217
+ end
218
+ end
219
+
220
+ # Get base metric value by tag
221
+ #
222
+ # @param tag [String] Metric tag (e.g., "hasc", "hdsc")
223
+ # @return [Integer, nil] Base metric value
224
+ def get_base_metric_value(tag)
225
+ case tag
226
+ when "hasc"
227
+ variation_table("hhea")&.ascender
228
+ when "hdsc"
229
+ variation_table("hhea")&.descender
230
+ when "hlgp"
231
+ variation_table("hhea")&.line_gap
232
+ when "xhgt"
233
+ os2 = variation_table("OS/2")
234
+ os2&.s_x_height if os2.respond_to?(:s_x_height)
235
+ when "cpht"
236
+ os2 = variation_table("OS/2")
237
+ os2&.s_cap_height if os2.respond_to?(:s_cap_height)
238
+ # Add more metrics as needed
239
+ end
240
+ end
241
+
242
+ # Update font metric in appropriate table
243
+ #
244
+ # @param tag [String] Metric tag
245
+ # @param value [Integer] New metric value
246
+ def update_font_metric(tag, value)
247
+ # This is a placeholder - full implementation would:
248
+ # 1. Modify the appropriate table's binary data
249
+ # 2. Update the font's table data
250
+ # For now, we just log the update
251
+ # In production, this would rebuild the affected tables
252
+ end
253
+
254
+ # Rebuild hmtx table with adjusted metrics
255
+ #
256
+ # @param metrics [Array<Hash>] Adjusted metrics
257
+ def rebuild_hmtx_table(metrics)
258
+ # Build new hmtx binary data
259
+ data = build_hmtx_data(metrics)
260
+
261
+ # Update font's table data
262
+ @font.table_data["hmtx"] = data if data
263
+ end
264
+
265
+ # Build hmtx binary data from metrics
266
+ #
267
+ # @param metrics [Array<Hash>] Metrics to encode
268
+ # @return [String, nil] Binary data
269
+ def build_hmtx_data(metrics)
270
+ return nil if metrics.empty?
271
+
272
+ # Find last unique advance width
273
+ last_advance = metrics.last[:advance_width]
274
+ number_of_h_metrics = metrics.length
275
+
276
+ # Optimize: count from end while advance width is same
277
+ (metrics.length - 1).downto(1) do |i|
278
+ break if metrics[i][:advance_width] != last_advance
279
+
280
+ number_of_h_metrics = i
281
+ end
282
+
283
+ # Build binary data
284
+ data = String.new("", encoding: Encoding::BINARY)
285
+
286
+ # Write hMetrics array
287
+ number_of_h_metrics.times do |i|
288
+ metric = metrics[i]
289
+ data << [metric[:advance_width]].pack("n") # uint16
290
+ data << [metric[:lsb]].pack("n") # int16 (as uint16, will be interpreted as signed)
291
+ end
292
+
293
+ # Write remaining LSBs
294
+ (number_of_h_metrics...metrics.length).each do |i|
295
+ data << [metrics[i][:lsb]].pack("n") # int16
296
+ end
297
+
298
+ # Update hhea's numberOfHMetrics
299
+ update_hhea_number_of_h_metrics(number_of_h_metrics)
300
+
301
+ data
302
+ end
303
+
304
+ # Update hhea table's numberOfHMetrics field
305
+ #
306
+ # @param count [Integer] New numberOfHMetrics value
307
+ def update_hhea_number_of_h_metrics(count)
308
+ return unless has_variation_table?("hhea")
309
+
310
+ hhea = variation_table("hhea")
311
+ return unless hhea
312
+
313
+ # Update the field if hhea supports it
314
+ hhea.number_of_h_metrics = count if hhea.respond_to?(:number_of_h_metrics=)
315
+ end
316
+ end
317
+ end
318
+ end