fontisan 0.2.0 → 0.2.2

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +119 -308
  3. data/README.adoc +1525 -1323
  4. data/Rakefile +45 -47
  5. data/benchmark/variation_quick_bench.rb +4 -4
  6. data/docs/FONT_HINTING.adoc +562 -0
  7. data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
  8. data/lib/fontisan/cli.rb +92 -34
  9. data/lib/fontisan/collection/builder.rb +82 -0
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  12. data/lib/fontisan/commands/base_command.rb +21 -2
  13. data/lib/fontisan/commands/convert_command.rb +96 -165
  14. data/lib/fontisan/commands/info_command.rb +111 -5
  15. data/lib/fontisan/commands/instance_command.rb +77 -85
  16. data/lib/fontisan/commands/validate_command.rb +28 -0
  17. data/lib/fontisan/config/validation_rules.yml +1 -1
  18. data/lib/fontisan/constants.rb +34 -24
  19. data/lib/fontisan/converters/format_converter.rb +154 -1
  20. data/lib/fontisan/converters/outline_converter.rb +101 -34
  21. data/lib/fontisan/converters/woff_writer.rb +9 -4
  22. data/lib/fontisan/font_loader.rb +14 -9
  23. data/lib/fontisan/font_writer.rb +9 -6
  24. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  25. data/lib/fontisan/hints/hint_converter.rb +131 -2
  26. data/lib/fontisan/hints/hint_validator.rb +284 -0
  27. data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
  28. data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
  29. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  30. data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
  31. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  32. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  33. data/lib/fontisan/loading_modes.rb +6 -4
  34. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +183 -12
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/open_type_font.rb +28 -10
  39. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  40. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  41. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  42. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  43. data/lib/fontisan/pipeline/output_writer.rb +159 -0
  44. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  45. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  46. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  47. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  48. data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
  49. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  51. data/lib/fontisan/tables/cff/charstring.rb +58 -3
  52. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  53. data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
  54. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  55. data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
  56. data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
  57. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  58. data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
  59. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  60. data/lib/fontisan/tables/cff.rb +2 -0
  61. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  62. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
  63. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  64. data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
  65. data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
  66. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  67. data/lib/fontisan/tables/cff2.rb +10 -5
  68. data/lib/fontisan/tables/cvar.rb +2 -41
  69. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  70. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  71. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  72. data/lib/fontisan/tables/gvar.rb +2 -41
  73. data/lib/fontisan/tables/name.rb +4 -4
  74. data/lib/fontisan/true_type_font.rb +27 -10
  75. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  76. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  77. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  78. data/lib/fontisan/validation/table_validator.rb +1 -1
  79. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  80. data/lib/fontisan/variation/cache.rb +3 -1
  81. data/lib/fontisan/variation/converter.rb +121 -13
  82. data/lib/fontisan/variation/delta_applier.rb +2 -1
  83. data/lib/fontisan/variation/inspector.rb +2 -1
  84. data/lib/fontisan/variation/instance_generator.rb +2 -1
  85. data/lib/fontisan/variation/instance_writer.rb +341 -0
  86. data/lib/fontisan/variation/optimizer.rb +6 -3
  87. data/lib/fontisan/variation/subsetter.rb +32 -10
  88. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  89. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  90. data/lib/fontisan/variation/variation_preserver.rb +291 -0
  91. data/lib/fontisan/version.rb +1 -1
  92. data/lib/fontisan/version.rb.orig +9 -0
  93. data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
  94. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  95. data/lib/fontisan/woff2_font.rb +489 -468
  96. data/lib/fontisan/woff_font.rb +16 -11
  97. data/lib/fontisan.rb +54 -2
  98. data/scripts/measure_optimization.rb +15 -7
  99. metadata +37 -2
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_writer"
4
+ require_relative "../converters/outline_converter"
5
+ require_relative "../converters/woff_writer"
6
+ require_relative "../error"
7
+
8
+ module Fontisan
9
+ module Variation
10
+ # Writes generated static font instances to files in various formats
11
+ #
12
+ # [`InstanceWriter`](lib/fontisan/variation/instance_writer.rb) takes
13
+ # instance tables generated by
14
+ # [`InstanceGenerator`](lib/fontisan/variation/instance_generator.rb) and
15
+ # writes them to files in the desired output format. It handles:
16
+ # - Format detection from file extension
17
+ # - Format conversion when needed (e.g., glyf → CFF for OTF)
18
+ # - SFNT version selection based on output format
19
+ # - Integration with FontWriter for binary output
20
+ # - Integration with OutlineConverter for format conversion
21
+ # - Integration with WoffWriter for WOFF packaging
22
+ #
23
+ # **Supported Output Formats:**
24
+ # - TTF (TrueType with glyf outlines)
25
+ # - OTF (OpenType with CFF outlines)
26
+ # - WOFF (Web Open Font Format)
27
+ # - WOFF2 (Web Open Font Format 2.0, future)
28
+ #
29
+ # @example Write instance to TTF
30
+ # tables = generator.generate
31
+ # InstanceWriter.write(tables, 'bold.ttf')
32
+ #
33
+ # @example Write instance to OTF with format conversion
34
+ # tables = generator.generate # from variable TTF
35
+ # InstanceWriter.write(tables, 'bold.otf', source_format: :ttf)
36
+ #
37
+ # @example Write instance to WOFF
38
+ # tables = generator.generate
39
+ # InstanceWriter.write(tables, 'bold.woff')
40
+ class InstanceWriter
41
+ # Supported output formats
42
+ SUPPORTED_FORMATS = %i[ttf otf woff woff2].freeze
43
+
44
+ # SFNT version constants
45
+ SFNT_VERSION_TRUETYPE = 0x00010000 # TrueType with glyf
46
+ SFNT_VERSION_CFF = 0x4F54544F # 'OTTO' for CFF
47
+
48
+ # Write instance tables to file
49
+ #
50
+ # @param tables [Hash<String, String>] Instance tables from
51
+ # InstanceGenerator
52
+ # @param output_path [String] Output file path
53
+ # @param options [Hash] Options
54
+ # @option options [Symbol] :format Output format (:ttf, :otf, :woff,
55
+ # :woff2)
56
+ # @option options [Symbol] :source_format Source format before instancing
57
+ # (:ttf or :otf)
58
+ # @option options [Boolean] :optimize Enable CFF optimization for OTF
59
+ # (default: false)
60
+ # @option options [Integer] :sfnt_version Override SFNT version
61
+ # @return [Integer] Number of bytes written
62
+ # @raise [ArgumentError] If parameters are invalid
63
+ # @raise [Error] If format conversion fails
64
+ def self.write(tables, output_path, options = {})
65
+ new(tables, options).write(output_path)
66
+ end
67
+
68
+ # @return [Hash<String, String>] Instance tables
69
+ attr_reader :tables
70
+
71
+ # @return [Hash] Writer options
72
+ attr_reader :options
73
+
74
+ # Initialize writer with instance tables
75
+ #
76
+ # @param tables [Hash<String, String>] Instance tables from
77
+ # InstanceGenerator
78
+ # @param options [Hash] Writer options
79
+ # @option options [Symbol] :source_format Source format before instancing
80
+ # @option options [Boolean] :optimize Enable CFF optimization
81
+ def initialize(tables, options = {})
82
+ @tables = tables
83
+ @options = options
84
+ validate_tables!
85
+ end
86
+
87
+ # Write instance to file
88
+ #
89
+ # @param output_path [String] Output file path
90
+ # @return [Integer] Number of bytes written
91
+ def write(output_path)
92
+ # Detect output format
93
+ format = detect_output_format(output_path)
94
+ validate_format!(format)
95
+
96
+ # Detect source format from tables
97
+ source_format = detect_source_format(@tables)
98
+
99
+ # Convert format if needed
100
+ output_tables = if format_conversion_needed?(source_format, format)
101
+ convert_format(source_format, format)
102
+ else
103
+ @tables
104
+ end
105
+
106
+ # Write to file based on format
107
+ case format
108
+ when :ttf, :otf
109
+ write_sfnt(output_tables, output_path, format)
110
+ when :woff
111
+ write_woff(output_tables, output_path, source_format)
112
+ when :woff2
113
+ raise Fontisan::Error,
114
+ "WOFF2 output not yet implemented (planned for Phase 6)"
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ # Validate instance tables
121
+ #
122
+ # @raise [ArgumentError] If tables are invalid
123
+ def validate_tables!
124
+ raise ArgumentError, "Tables cannot be nil" if @tables.nil?
125
+
126
+ unless @tables.is_a?(Hash)
127
+ raise ArgumentError,
128
+ "Tables must be a Hash, got: #{@tables.class}"
129
+ end
130
+
131
+ if @tables.empty?
132
+ raise ArgumentError, "Tables cannot be empty"
133
+ end
134
+
135
+ # Check for required tables
136
+ required_tables = %w[head hhea maxp]
137
+ required_tables.each do |tag|
138
+ unless @tables.key?(tag)
139
+ raise ArgumentError, "Missing required table: #{tag}"
140
+ end
141
+ end
142
+ end
143
+
144
+ # Detect output format from file path
145
+ #
146
+ # @param path [String] Output file path
147
+ # @return [Symbol] Format (:ttf, :otf, :woff, :woff2)
148
+ def detect_output_format(path)
149
+ return @options[:format] if @options[:format]
150
+
151
+ ext = File.extname(path).downcase
152
+ case ext
153
+ when ".ttf" then :ttf
154
+ when ".otf" then :otf
155
+ when ".woff" then :woff
156
+ when ".woff2" then :woff2
157
+ else
158
+ raise ArgumentError,
159
+ "Cannot determine format from extension: #{ext}. " \
160
+ "Supported: .ttf, .otf, .woff, .woff2"
161
+ end
162
+ end
163
+
164
+ # Validate output format
165
+ #
166
+ # @param format [Symbol] Format to validate
167
+ # @raise [ArgumentError] If format is not supported
168
+ def validate_format!(format)
169
+ unless SUPPORTED_FORMATS.include?(format)
170
+ raise ArgumentError,
171
+ "Unsupported format: #{format}. " \
172
+ "Supported: #{SUPPORTED_FORMATS.join(', ')}"
173
+ end
174
+ end
175
+
176
+ # Detect source format from instance tables
177
+ #
178
+ # @param tables [Hash<String, String>] Instance tables
179
+ # @return [Symbol] Source format (:ttf or :otf)
180
+ def detect_source_format(tables)
181
+ # Check for outline tables
182
+ if tables.key?("CFF ") || tables.key?("CFF2")
183
+ :otf
184
+ elsif tables.key?("glyf")
185
+ :ttf
186
+ else
187
+ # If no outline tables, use option or default to TTF
188
+ @options[:source_format] || :ttf
189
+ end
190
+ end
191
+
192
+ # Check if format conversion is needed
193
+ #
194
+ # @param source_format [Symbol] Source format
195
+ # @param target_format [Symbol] Target format
196
+ # @return [Boolean] True if conversion needed
197
+ def format_conversion_needed?(source_format, target_format)
198
+ # WOFF doesn't need outline conversion
199
+ return false if %i[woff woff2].include?(target_format)
200
+
201
+ # Check if outline formats differ
202
+ source_format != target_format
203
+ end
204
+
205
+ # Convert instance tables from source format to target format
206
+ #
207
+ # @param source_format [Symbol] Source format
208
+ # @param target_format [Symbol] Target format
209
+ # @return [Hash<String, String>] Converted tables
210
+ # @raise [Error] If conversion fails
211
+ def convert_format(source_format, target_format)
212
+ # Create temporary font object for conversion
213
+ temp_font = create_temp_font(@tables, source_format)
214
+
215
+ # Use OutlineConverter for format conversion
216
+ converter = Converters::OutlineConverter.new
217
+ converter.convert(
218
+ temp_font,
219
+ target_format: target_format,
220
+ optimize_cff: @options[:optimize] || false,
221
+ )
222
+ rescue StandardError => e
223
+ raise Fontisan::Error,
224
+ "Failed to convert instance from #{source_format} to " \
225
+ "#{target_format}: #{e.message}"
226
+ end
227
+
228
+ # Create temporary font object from tables
229
+ #
230
+ # @param tables [Hash<String, String>] Font tables
231
+ # @param format [Symbol] Font format
232
+ # @return [Object] Font object
233
+ def create_temp_font(tables, format)
234
+ # Create minimal font object that responds to required methods
235
+ font_class = format == :otf ? OpenTypeFont : TrueTypeFont
236
+ font = font_class.new
237
+
238
+ # Set table data
239
+ font.instance_variable_set(:@table_data, tables)
240
+
241
+ # Define required methods
242
+ font.define_singleton_method(:table_data) { tables }
243
+ font.define_singleton_method(:table_names) { tables.keys }
244
+ font.define_singleton_method(:has_table?) { |tag| tables.key?(tag) }
245
+ font.define_singleton_method(:table) do |tag|
246
+ # Return nil if table doesn't exist
247
+ return nil unless tables.key?(tag)
248
+
249
+ # Parse and return table object
250
+ # For conversion, we need to lazy-load tables
251
+ parse_table(tag, tables[tag])
252
+ end
253
+
254
+ font
255
+ end
256
+
257
+ # Parse table data into table object
258
+ #
259
+ # @param tag [String] Table tag
260
+ # @param data [String] Table binary data
261
+ # @return [Object] Parsed table object
262
+ def parse_table(tag, data)
263
+ # For OutlineConverter, we need head, maxp, loca, glyf for TTF
264
+ # and CFF for OTF
265
+ case tag
266
+ when "head"
267
+ Tables::Head.new.tap { |t| t.parse(data) }
268
+ when "maxp"
269
+ Tables::Maxp.new.tap { |t| t.parse(data) }
270
+ when "loca"
271
+ Tables::Loca.new.tap { |t| t.data = data }
272
+ when "glyf"
273
+ Tables::Glyf.new.tap { |t| t.data = data }
274
+ when "CFF "
275
+ Tables::Cff.new.tap { |t| t.parse(data) }
276
+ when "CFF2"
277
+ Tables::Cff2.new.tap { |t| t.parse(data) }
278
+ else
279
+ # For other tables, return a simple object that just holds data
280
+ Object.new.tap do |obj|
281
+ obj.define_singleton_method(:data) { data }
282
+ end
283
+ end
284
+ rescue StandardError => e
285
+ warn "Warning: Failed to parse #{tag} table: #{e.message}"
286
+ nil
287
+ end
288
+
289
+ # Write SFNT format (TTF or OTF)
290
+ #
291
+ # @param tables [Hash<String, String>] Output tables
292
+ # @param output_path [String] Output file path
293
+ # @param format [Symbol] Output format
294
+ # @return [Integer] Number of bytes written
295
+ def write_sfnt(tables, output_path, format)
296
+ # Determine SFNT version
297
+ sfnt_version = @options[:sfnt_version] || sfnt_version_for_format(
298
+ format,
299
+ )
300
+
301
+ # Write using FontWriter
302
+ FontWriter.write_to_file(tables, output_path,
303
+ sfnt_version: sfnt_version)
304
+ end
305
+
306
+ # Write WOFF format
307
+ #
308
+ # @param tables [Hash<String, String>] Output tables
309
+ # @param output_path [String] Output file path
310
+ # @param source_format [Symbol] Source format (for flavor detection)
311
+ # @return [Integer] Number of bytes written
312
+ def write_woff(tables, output_path, source_format)
313
+ # Create temporary font for WOFF writer
314
+ temp_font = create_temp_font(tables, source_format)
315
+
316
+ # Add cff? method for WoffWriter flavor detection
317
+ temp_font.define_singleton_method(:cff?) do
318
+ tables.key?("CFF ") || tables.key?("CFF2")
319
+ end
320
+
321
+ # Use WoffWriter to create WOFF
322
+ writer = Converters::WoffWriter.new
323
+ woff_data = writer.convert(temp_font)
324
+
325
+ # Write to file
326
+ File.binwrite(output_path, woff_data)
327
+ rescue StandardError => e
328
+ raise Fontisan::Error,
329
+ "Failed to write WOFF output: #{e.message}"
330
+ end
331
+
332
+ # Get SFNT version for output format
333
+ #
334
+ # @param format [Symbol] Output format
335
+ # @return [Integer] SFNT version constant
336
+ def sfnt_version_for_format(format)
337
+ format == :otf ? SFNT_VERSION_CFF : SFNT_VERSION_TRUETYPE
338
+ end
339
+ end
340
+ end
341
+ end
@@ -209,9 +209,12 @@ module Fontisan
209
209
  coords2 = r2.region_axes[i]
210
210
 
211
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)
212
+ return false unless coords_similar?(coords1.start_coord,
213
+ coords2.start_coord)
214
+ return false unless coords_similar?(coords1.peak_coord,
215
+ coords2.peak_coord)
216
+ return false unless coords_similar?(coords1.end_coord,
217
+ coords2.end_coord)
215
218
  end
216
219
 
217
220
  true
@@ -112,7 +112,10 @@ module Fontisan
112
112
  validate_input if @options[:validate]
113
113
 
114
114
  fvar = variation_table("fvar")
115
- return { tables: @font.table_data.dup, report: { error: "No fvar table" } } unless fvar
115
+ unless fvar
116
+ return { tables: @font.table_data.dup,
117
+ report: { error: "No fvar table" } }
118
+ end
116
119
 
117
120
  # Find axes to keep
118
121
  all_axes = fvar.axes
@@ -175,7 +178,8 @@ module Fontisan
175
178
  optimizer = Optimizer.new(cff2, region_threshold: threshold)
176
179
  optimizer.optimize
177
180
 
178
- @report[:regions_deduplicated] = optimizer.stats[:regions_deduplicated]
181
+ @report[:regions_deduplicated] =
182
+ optimizer.stats[:regions_deduplicated]
179
183
  @report[:cff2_optimized] = true
180
184
  end
181
185
 
@@ -316,8 +320,14 @@ module Fontisan
316
320
  # @param tables [Hash] Font tables
317
321
  # @param glyph_ids [Array<Integer>] Glyph IDs to keep
318
322
  def subset_metrics_variations(tables, glyph_ids)
319
- subset_metrics_table(tables, "HVAR", glyph_ids) if has_variation_table?("HVAR")
320
- subset_metrics_table(tables, "VVAR", glyph_ids) if has_variation_table?("VVAR")
323
+ if has_variation_table?("HVAR")
324
+ subset_metrics_table(tables, "HVAR",
325
+ glyph_ids)
326
+ end
327
+ if has_variation_table?("VVAR")
328
+ subset_metrics_table(tables, "VVAR",
329
+ glyph_ids)
330
+ end
321
331
  # MVAR is font-wide, no glyph subsetting needed
322
332
  end
323
333
 
@@ -333,7 +343,8 @@ module Fontisan
333
343
  # 3. Remove unused ItemVariationData
334
344
  # 4. Rebuild and serialize
335
345
 
336
- @report[:"#{table_tag.downcase}_note"] = "#{table_tag} subsetting not yet implemented"
346
+ @report[:"#{table_tag.downcase}_note"] =
347
+ "#{table_tag} subsetting not yet implemented"
337
348
  end
338
349
 
339
350
  # Update non-variation glyph tables
@@ -396,9 +407,18 @@ module Fontisan
396
407
  # @param tables [Hash] Font tables
397
408
  # @param keep_indices [Array<Integer>] Axis indices to keep
398
409
  def subset_metrics_axes(tables, keep_indices)
399
- subset_metrics_table_axes(tables, "HVAR", keep_indices) if has_variation_table?("HVAR")
400
- subset_metrics_table_axes(tables, "VVAR", keep_indices) if has_variation_table?("VVAR")
401
- subset_metrics_table_axes(tables, "MVAR", keep_indices) if has_variation_table?("MVAR")
410
+ if has_variation_table?("HVAR")
411
+ subset_metrics_table_axes(tables, "HVAR",
412
+ keep_indices)
413
+ end
414
+ if has_variation_table?("VVAR")
415
+ subset_metrics_table_axes(tables, "VVAR",
416
+ keep_indices)
417
+ end
418
+ if has_variation_table?("MVAR")
419
+ subset_metrics_table_axes(tables, "MVAR",
420
+ keep_indices)
421
+ end
402
422
  end
403
423
 
404
424
  # Subset a single metrics table's axes
@@ -412,7 +432,8 @@ module Fontisan
412
432
  # 2. Filter ItemVariationStore regions to keep axis indices
413
433
  # 3. Rebuild and serialize
414
434
 
415
- @report[:"#{table_tag.downcase}_axes_note"] = "#{table_tag} axis subsetting not yet implemented"
435
+ @report[:"#{table_tag.downcase}_axes_note"] =
436
+ "#{table_tag} axis subsetting not yet implemented"
416
437
  end
417
438
 
418
439
  # Simplify metrics table regions
@@ -426,7 +447,8 @@ module Fontisan
426
447
  # 3. Update delta set indices
427
448
  # 4. Serialize back to binary
428
449
 
429
- @report[:metrics_simplify_note] = "Metrics region simplification not yet implemented"
450
+ @report[:metrics_simplify_note] =
451
+ "Metrics region simplification not yet implemented"
430
452
  end
431
453
 
432
454
  # Create temporary font wrapper for validation
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../binary/base_record"
4
+
5
+ module Fontisan
6
+ module Variation
7
+ # Tuple variation header structure
8
+ #
9
+ # Used by both gvar and cvar tables to describe variation tuples.
10
+ # Each tuple header contains metadata about peak coordinates,
11
+ # intermediate regions, and point number handling.
12
+ class TupleVariationHeader < Binary::BaseRecord
13
+ uint16 :variation_data_size
14
+ uint16 :tuple_index
15
+
16
+ # Tuple index flags
17
+ EMBEDDED_PEAK_TUPLE = 0x8000
18
+ INTERMEDIATE_REGION = 0x4000
19
+ PRIVATE_POINT_NUMBERS = 0x2000
20
+ TUPLE_INDEX_MASK = 0x0FFF
21
+
22
+ # Check if tuple has embedded peak coordinates
23
+ #
24
+ # @return [Boolean] True if embedded
25
+ def embedded_peak_tuple?
26
+ (tuple_index & EMBEDDED_PEAK_TUPLE) != 0
27
+ end
28
+
29
+ # Check if tuple has intermediate region
30
+ #
31
+ # @return [Boolean] True if intermediate region
32
+ def intermediate_region?
33
+ (tuple_index & INTERMEDIATE_REGION) != 0
34
+ end
35
+
36
+ # Check if tuple has private point numbers
37
+ #
38
+ # @return [Boolean] True if private points
39
+ def private_point_numbers?
40
+ (tuple_index & PRIVATE_POINT_NUMBERS) != 0
41
+ end
42
+
43
+ # Get shared tuple index
44
+ #
45
+ # @return [Integer] Tuple index
46
+ def shared_tuple_index
47
+ tuple_index & TUPLE_INDEX_MASK
48
+ end
49
+ end
50
+ end
51
+ end