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,344 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "delta_applicator"
4
+ require_relative "static_font_builder"
5
+
6
+ module Fontisan
7
+ module Variable
8
+ # Main entry point for variable font instancing
9
+ #
10
+ # This class orchestrates the complete process of generating a static
11
+ # font instance from a variable font at specified coordinates:
12
+ #
13
+ # 1. Validates the font is a variable font (has fvar table)
14
+ # 2. Normalizes user coordinates using AxisNormalizer
15
+ # 3. Calculates region scalars using RegionMatcher
16
+ # 4. Applies deltas using DeltaApplicator
17
+ # 5. Builds static font using StaticFontBuilder
18
+ #
19
+ # @example Generate instance at specific coordinates
20
+ # instancer = Instancer.new(variable_font)
21
+ # static_binary = instancer.instance({ "wght" => 700 })
22
+ # File.binwrite("bold.ttf", static_binary)
23
+ #
24
+ # @example Generate instance for named instance
25
+ # instancer = Instancer.new(variable_font)
26
+ # static_binary = instancer.instance_named("Bold")
27
+ class Instancer
28
+ # @return [Object] The variable font object
29
+ attr_reader :font
30
+
31
+ # @return [DeltaApplicator] Delta applicator
32
+ attr_reader :delta_applicator
33
+
34
+ # @return [StaticFontBuilder] Static font builder
35
+ attr_reader :static_font_builder
36
+
37
+ # Initialize the instancer
38
+ #
39
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font object
40
+ # @raise [ArgumentError] If font is not a variable font
41
+ def initialize(font)
42
+ @font = font
43
+ validate_variable_font!
44
+
45
+ @delta_applicator = DeltaApplicator.new(font)
46
+ @static_font_builder = StaticFontBuilder.new(font)
47
+ end
48
+
49
+ # Generate static font instance at specified coordinates
50
+ #
51
+ # @param user_coords [Hash<String, Numeric>] User coordinates
52
+ # { "wght" => 700, "wdth" => 100 }
53
+ # @param options [Hash] Instance options
54
+ # @option options [Boolean] :update_modified Update head modified timestamp
55
+ # @return [String] Complete static font binary
56
+ #
57
+ # @example
58
+ # binary = instancer.instance({ "wght" => 700, "wdth" => 100 })
59
+ def instance(user_coords, options = {})
60
+ # Apply deltas to get all varied data
61
+ delta_result = @delta_applicator.apply(user_coords)
62
+
63
+ # Collect varied metrics for all glyphs
64
+ varied_metrics = collect_varied_glyph_metrics(
65
+ delta_result[:normalized_coords],
66
+ delta_result[:region_scalars],
67
+ )
68
+
69
+ # Extract font-level metrics
70
+ font_metrics = delta_result[:font_metrics]
71
+
72
+ # Build static font
73
+ @static_font_builder.build(varied_metrics, font_metrics, options)
74
+ end
75
+
76
+ # Generate static font instance and write to file
77
+ #
78
+ # @param output_path [String] Output file path
79
+ # @param user_coords [Hash<String, Numeric>] User coordinates
80
+ # @param options [Hash] Instance options
81
+ # @return [Integer] Number of bytes written
82
+ #
83
+ # @example
84
+ # instancer.instance_to_file("bold.ttf", { "wght" => 700 })
85
+ def instance_to_file(output_path, user_coords, options = {})
86
+ binary = instance(user_coords, options)
87
+ File.binwrite(output_path, binary)
88
+ end
89
+
90
+ # Generate static font instance for a named instance
91
+ #
92
+ # @param instance_name [String] Named instance name (from fvar)
93
+ # @param options [Hash] Instance options
94
+ # @return [String] Complete static font binary
95
+ # @raise [ArgumentError] If named instance not found
96
+ #
97
+ # @example
98
+ # binary = instancer.instance_named("Bold")
99
+ def instance_named(instance_name, options = {})
100
+ coords = find_named_instance_coords(instance_name)
101
+ instance(coords, options)
102
+ end
103
+
104
+ # Generate static font instance for a named instance and write to file
105
+ #
106
+ # @param output_path [String] Output file path
107
+ # @param instance_name [String] Named instance name
108
+ # @param options [Hash] Instance options
109
+ # @return [Integer] Number of bytes written
110
+ #
111
+ # @example
112
+ # instancer.instance_named_to_file("bold.ttf", "Bold")
113
+ def instance_named_to_file(output_path, instance_name, options = {})
114
+ binary = instance_named(instance_name, options)
115
+ File.binwrite(output_path, binary)
116
+ end
117
+
118
+ # Get list of available named instances
119
+ #
120
+ # @return [Array<Hash>] Array of named instance information
121
+ # [{ name: "Bold", coords: { "wght" => 700 } }, ...]
122
+ def named_instances
123
+ fvar = load_fvar_table
124
+ return [] unless fvar
125
+
126
+ # Get name table for instance names
127
+ name_table = load_name_table
128
+
129
+ fvar.instances.map do |instance|
130
+ coords = extract_instance_coords(instance, fvar)
131
+ name = if name_table
132
+ get_instance_name(instance[:name_id],
133
+ name_table)
134
+ end
135
+
136
+ {
137
+ name_id: instance[:name_id],
138
+ name: name || "Instance #{instance[:name_id]}",
139
+ coordinates: coords,
140
+ }
141
+ end
142
+ end
143
+
144
+ # Check if font is a variable font
145
+ #
146
+ # @return [Boolean] True if variable font
147
+ def variable_font?
148
+ @delta_applicator.variable_font?
149
+ end
150
+
151
+ # Get available axis information
152
+ #
153
+ # @return [Hash] Axis information
154
+ def axes
155
+ @delta_applicator.axes
156
+ end
157
+
158
+ # Get available axis tags
159
+ #
160
+ # @return [Array<String>] Array of axis tags
161
+ def axis_tags
162
+ @delta_applicator.axis_tags
163
+ end
164
+
165
+ private
166
+
167
+ # Validate that font is a variable font
168
+ #
169
+ # @raise [ArgumentError] If not a variable font
170
+ def validate_variable_font!
171
+ fvar_data = @font.table_data("fvar")
172
+ return unless fvar_data.nil? || fvar_data.empty?
173
+
174
+ raise ArgumentError, "Font is not a variable font (missing fvar table)"
175
+ end
176
+
177
+ # Collect varied metrics for all glyphs
178
+ #
179
+ # @param normalized_coords [Hash] Normalized coordinates
180
+ # @param region_scalars [Array<Float>] Region scalars
181
+ # @return [Hash<Integer, Hash>] Varied metrics by glyph ID
182
+ def collect_varied_glyph_metrics(normalized_coords, _region_scalars)
183
+ varied_metrics = {}
184
+
185
+ # Get number of glyphs
186
+ maxp = load_maxp_table
187
+ return varied_metrics unless maxp
188
+
189
+ num_glyphs = maxp.num_glyphs
190
+
191
+ # Get original hmtx metrics
192
+ hmtx = load_hmtx_table
193
+ return varied_metrics unless hmtx
194
+
195
+ # For each glyph, calculate varied metrics
196
+ num_glyphs.times do |glyph_id|
197
+ original_metric = hmtx.metric_for(glyph_id)
198
+ next unless original_metric
199
+
200
+ # Get deltas from delta applicator
201
+ deltas = @delta_applicator.apply_glyph(glyph_id, normalized_coords)
202
+ metric_deltas = deltas[:metric_deltas]
203
+
204
+ # Calculate varied values
205
+ varied_metric = {
206
+ advance_width: original_metric[:advance_width],
207
+ lsb: original_metric[:lsb],
208
+ }
209
+
210
+ # Apply horizontal deltas if present
211
+ if metric_deltas[:horizontal]
212
+ if metric_deltas[:horizontal][:advance_width]
213
+ varied_metric[:advance_width] += metric_deltas[:horizontal][:advance_width]
214
+ end
215
+
216
+ if metric_deltas[:horizontal][:lsb]
217
+ varied_metric[:lsb] += metric_deltas[:horizontal][:lsb]
218
+ end
219
+ end
220
+
221
+ varied_metrics[glyph_id] = varied_metric
222
+ end
223
+
224
+ varied_metrics
225
+ end
226
+
227
+ # Find coordinates for a named instance
228
+ #
229
+ # @param instance_name [String] Instance name
230
+ # @return [Hash<String, Float>] Coordinates
231
+ # @raise [ArgumentError] If instance not found
232
+ def find_named_instance_coords(instance_name)
233
+ instances = named_instances
234
+ instance = instances.find { |inst| inst[:name] == instance_name }
235
+
236
+ unless instance
237
+ raise ArgumentError,
238
+ "Named instance '#{instance_name}' not found"
239
+ end
240
+
241
+ instance[:coordinates]
242
+ end
243
+
244
+ # Extract coordinates from instance record
245
+ #
246
+ # @param instance [Hash] Instance record
247
+ # @param fvar [Fvar] fvar table
248
+ # @return [Hash<String, Float>] Coordinates by axis tag
249
+ def extract_instance_coords(instance, fvar)
250
+ coords = {}
251
+ instance[:coordinates].each_with_index do |value, index|
252
+ axis = fvar.axes[index]
253
+ coords[axis.axis_tag.to_s] = value if axis
254
+ end
255
+ coords
256
+ end
257
+
258
+ # Get instance name from name table
259
+ #
260
+ # @param name_id [Integer] Name table ID
261
+ # @param name_table [Name] name table
262
+ # @return [String, nil] Instance name
263
+ def get_instance_name(name_id, name_table)
264
+ # Try to get English name
265
+ record = name_table.records.find do |r|
266
+ r.name_id == name_id && r.language_id == 0x0409 # English (US)
267
+ end
268
+
269
+ record ||= name_table.records.find { |r| r.name_id == name_id }
270
+ record&.value
271
+ rescue StandardError
272
+ nil
273
+ end
274
+
275
+ # Load fvar table
276
+ #
277
+ # @return [Fvar, nil] fvar table or nil
278
+ def load_fvar_table
279
+ data = @font.table_data("fvar")
280
+ return nil if data.nil? || data.empty?
281
+
282
+ Tables::Fvar.read(data)
283
+ rescue StandardError
284
+ nil
285
+ end
286
+
287
+ # Load name table
288
+ #
289
+ # @return [Name, nil] name table or nil
290
+ def load_name_table
291
+ data = @font.table_data("name")
292
+ return nil if data.nil? || data.empty?
293
+
294
+ Tables::Name.read(data)
295
+ rescue StandardError
296
+ nil
297
+ end
298
+
299
+ # Load maxp table
300
+ #
301
+ # @return [Maxp, nil] maxp table or nil
302
+ def load_maxp_table
303
+ data = @font.table_data("maxp")
304
+ return nil if data.nil? || data.empty?
305
+
306
+ Tables::Maxp.read(data)
307
+ rescue StandardError
308
+ nil
309
+ end
310
+
311
+ # Load hmtx table
312
+ #
313
+ # @return [Hmtx, nil] hmtx table or nil
314
+ def load_hmtx_table
315
+ data = @font.table_data("hmtx")
316
+ return nil if data.nil? || data.empty?
317
+
318
+ hmtx = Tables::Hmtx.read(data)
319
+
320
+ # Parse with context
321
+ hhea = load_hhea_table
322
+ maxp = load_maxp_table
323
+ return nil unless hhea && maxp
324
+
325
+ hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
326
+ hmtx
327
+ rescue StandardError
328
+ nil
329
+ end
330
+
331
+ # Load hhea table
332
+ #
333
+ # @return [Hhea, nil] hhea table or nil
334
+ def load_hhea_table
335
+ data = @font.table_data("hhea")
336
+ return nil if data.nil? || data.empty?
337
+
338
+ Tables::Hhea.read(data)
339
+ rescue StandardError
340
+ nil
341
+ end
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Fontisan
6
+ module Variable
7
+ # Applies metric deltas from HVAR, VVAR, and MVAR tables
8
+ #
9
+ # Processes variation data for font metrics including:
10
+ # - Horizontal metrics (advance widths, LSB, RSB) via HVAR
11
+ # - Vertical metrics (advance heights, TSB, BSB) via VVAR
12
+ # - Font-level metrics (ascent, descent, line gap, etc.) via MVAR
13
+ #
14
+ # Uses ItemVariationStore and region scalars to calculate accumulated
15
+ # deltas which are then applied to original metric values.
16
+ #
17
+ # @example Apply metric deltas
18
+ # processor = MetricDeltaProcessor.new(hvar, vvar, mvar)
19
+ # deltas = processor.apply_deltas(glyph_id, region_scalars)
20
+ # # => { advance_width: 10, lsb: -2, ... }
21
+ class MetricDeltaProcessor
22
+ # @return [Hash] Configuration settings
23
+ attr_reader :config
24
+
25
+ # Initialize the processor
26
+ #
27
+ # @param hvar [Fontisan::Tables::Hvar, nil] Horizontal variations table
28
+ # @param vvar [Fontisan::Tables::Vvar, nil] Vertical variations table
29
+ # @param mvar [Fontisan::Tables::Mvar, nil] Metrics variations table
30
+ # @param config [Hash] Optional configuration overrides
31
+ def initialize(hvar: nil, vvar: nil, mvar: nil, config: {})
32
+ @hvar = hvar
33
+ @vvar = vvar
34
+ @mvar = mvar
35
+ @config = load_config.merge(config)
36
+ end
37
+
38
+ # Apply all metric deltas for a glyph
39
+ #
40
+ # @param glyph_id [Integer] Glyph ID
41
+ # @param region_scalars [Array<Float>] Scalar for each region
42
+ # @return [Hash] Metric deltas
43
+ def apply_deltas(glyph_id, region_scalars)
44
+ result = {}
45
+
46
+ # Apply horizontal metric deltas if HVAR present
47
+ if @hvar && @config.dig(:metric_deltas, :apply_hvar)
48
+ result[:horizontal] = apply_hvar_deltas(glyph_id, region_scalars)
49
+ end
50
+
51
+ # Apply vertical metric deltas if VVAR present
52
+ if @vvar && @config.dig(:metric_deltas, :apply_vvar)
53
+ result[:vertical] = apply_vvar_deltas(glyph_id, region_scalars)
54
+ end
55
+
56
+ result
57
+ end
58
+
59
+ # Apply font-level metric deltas
60
+ #
61
+ # @param region_scalars [Array<Float>] Scalar for each region
62
+ # @return [Hash] Font-level metric deltas
63
+ def apply_font_metrics(region_scalars)
64
+ return {} unless @mvar && @config.dig(:metric_deltas, :apply_mvar)
65
+
66
+ result = {}
67
+
68
+ # Process each metric tag in MVAR
69
+ @mvar.metric_tags.each do |tag|
70
+ delta_set = @mvar.metric_delta_set(tag)
71
+ next unless delta_set
72
+
73
+ # Calculate accumulated delta
74
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
75
+ result[tag] = apply_rounding(accumulated)
76
+ end
77
+
78
+ result
79
+ end
80
+
81
+ # Get advance width delta for a glyph
82
+ #
83
+ # @param glyph_id [Integer] Glyph ID
84
+ # @param region_scalars [Array<Float>] Region scalars
85
+ # @return [Integer] Advance width delta
86
+ def advance_width_delta(glyph_id, region_scalars)
87
+ return 0 unless @hvar
88
+
89
+ delta_set = @hvar.advance_width_delta_set(glyph_id)
90
+ return 0 unless delta_set
91
+
92
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
93
+ apply_rounding(accumulated)
94
+ end
95
+
96
+ # Get LSB delta for a glyph
97
+ #
98
+ # @param glyph_id [Integer] Glyph ID
99
+ # @param region_scalars [Array<Float>] Region scalars
100
+ # @return [Integer] LSB delta
101
+ def lsb_delta(glyph_id, region_scalars)
102
+ return 0 unless @hvar
103
+
104
+ delta_set = @hvar.lsb_delta_set(glyph_id)
105
+ return 0 unless delta_set
106
+
107
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
108
+ apply_rounding(accumulated)
109
+ end
110
+
111
+ # Get RSB delta for a glyph
112
+ #
113
+ # @param glyph_id [Integer] Glyph ID
114
+ # @param region_scalars [Array<Float>] Region scalars
115
+ # @return [Integer] RSB delta
116
+ def rsb_delta(glyph_id, region_scalars)
117
+ return 0 unless @hvar
118
+
119
+ delta_set = @hvar.rsb_delta_set(glyph_id)
120
+ return 0 unless delta_set
121
+
122
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
123
+ apply_rounding(accumulated)
124
+ end
125
+
126
+ # Check if horizontal variations are present
127
+ #
128
+ # @return [Boolean] True if HVAR present
129
+ def has_hvar?
130
+ !@hvar.nil?
131
+ end
132
+
133
+ # Check if vertical variations are present
134
+ #
135
+ # @return [Boolean] True if VVAR present
136
+ def has_vvar?
137
+ !@vvar.nil?
138
+ end
139
+
140
+ # Check if font metric variations are present
141
+ #
142
+ # @return [Boolean] True if MVAR present
143
+ def has_mvar?
144
+ !@mvar.nil?
145
+ end
146
+
147
+ private
148
+
149
+ # Load configuration from YAML file
150
+ #
151
+ # @return [Hash] Configuration hash
152
+ def load_config
153
+ config_path = File.join(__dir__, "..", "config",
154
+ "variable_settings.yml")
155
+ loaded = YAML.load_file(config_path)
156
+ # Convert string keys to symbol keys for consistency
157
+ deep_symbolize_keys(loaded)
158
+ rescue StandardError
159
+ # Return default config
160
+ {
161
+ metric_deltas: {
162
+ apply_hvar: true,
163
+ apply_vvar: true,
164
+ apply_mvar: true,
165
+ update_dependent_metrics: true,
166
+ },
167
+ delta_application: {
168
+ rounding_mode: "round",
169
+ },
170
+ }
171
+ end
172
+
173
+ # Recursively convert hash keys to symbols
174
+ #
175
+ # @param hash [Hash] Hash with string keys
176
+ # @return [Hash] Hash with symbol keys
177
+ def deep_symbolize_keys(hash)
178
+ hash.each_with_object({}) do |(key, value), result|
179
+ new_key = key.to_sym
180
+ new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
181
+ result[new_key] = new_value
182
+ end
183
+ end
184
+
185
+ # Apply HVAR deltas for a glyph
186
+ #
187
+ # @param glyph_id [Integer] Glyph ID
188
+ # @param region_scalars [Array<Float>] Region scalars
189
+ # @return [Hash] Horizontal metric deltas
190
+ def apply_hvar_deltas(glyph_id, region_scalars)
191
+ result = {}
192
+
193
+ # Advance width delta
194
+ if (delta_set = @hvar.advance_width_delta_set(glyph_id))
195
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
196
+ result[:advance_width] = apply_rounding(accumulated)
197
+ end
198
+
199
+ # LSB delta
200
+ if (delta_set = @hvar.lsb_delta_set(glyph_id))
201
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
202
+ result[:lsb] = apply_rounding(accumulated)
203
+ end
204
+
205
+ # RSB delta
206
+ if (delta_set = @hvar.rsb_delta_set(glyph_id))
207
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
208
+ result[:rsb] = apply_rounding(accumulated)
209
+ end
210
+
211
+ result
212
+ end
213
+
214
+ # Apply VVAR deltas for a glyph
215
+ #
216
+ # @param glyph_id [Integer] Glyph ID
217
+ # @param region_scalars [Array<Float>] Region scalars
218
+ # @return [Hash] Vertical metric deltas
219
+ def apply_vvar_deltas(glyph_id, region_scalars)
220
+ result = {}
221
+
222
+ # Similar to HVAR but for vertical metrics
223
+ # VVAR has the same structure as HVAR
224
+ if @vvar.respond_to?(:advance_height_delta_set) && (delta_set = @vvar.advance_height_delta_set(glyph_id))
225
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
226
+ result[:advance_height] = apply_rounding(accumulated)
227
+ end
228
+
229
+ if @vvar.respond_to?(:tsb_delta_set) && (delta_set = @vvar.tsb_delta_set(glyph_id))
230
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
231
+ result[:tsb] = apply_rounding(accumulated)
232
+ end
233
+
234
+ if @vvar.respond_to?(:bsb_delta_set) && (delta_set = @vvar.bsb_delta_set(glyph_id))
235
+ accumulated = calculate_accumulated_delta(delta_set, region_scalars)
236
+ result[:bsb] = apply_rounding(accumulated)
237
+ end
238
+
239
+ result
240
+ end
241
+
242
+ # Calculate accumulated delta from delta set and region scalars
243
+ #
244
+ # @param delta_set [Array<Integer>] Delta values for each region
245
+ # @param region_scalars [Array<Float>] Scalar for each region
246
+ # @return [Float] Accumulated delta
247
+ def calculate_accumulated_delta(delta_set, region_scalars)
248
+ accumulated = 0.0
249
+
250
+ delta_set.each_with_index do |delta, index|
251
+ next if index >= region_scalars.length
252
+
253
+ scalar = region_scalars[index]
254
+ accumulated += delta * scalar
255
+ end
256
+
257
+ accumulated
258
+ end
259
+
260
+ # Apply rounding to delta value
261
+ #
262
+ # @param delta [Float] Delta value
263
+ # @return [Integer] Rounded delta
264
+ def apply_rounding(delta)
265
+ mode = @config.dig(:delta_application, :rounding_mode) || "round"
266
+
267
+ case mode
268
+ when "round"
269
+ delta.round
270
+ when "floor"
271
+ delta.floor
272
+ when "ceil"
273
+ delta.ceil
274
+ when "truncate"
275
+ delta.to_i
276
+ else
277
+ delta.round
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end