fontisan 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +270 -131
  3. data/README.adoc +158 -4
  4. data/Rakefile +44 -47
  5. data/lib/fontisan/cli.rb +84 -33
  6. data/lib/fontisan/collection/builder.rb +81 -0
  7. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  8. data/lib/fontisan/commands/base_command.rb +16 -0
  9. data/lib/fontisan/commands/convert_command.rb +97 -170
  10. data/lib/fontisan/commands/instance_command.rb +71 -80
  11. data/lib/fontisan/commands/validate_command.rb +25 -0
  12. data/lib/fontisan/config/validation_rules.yml +1 -1
  13. data/lib/fontisan/constants.rb +10 -0
  14. data/lib/fontisan/converters/format_converter.rb +150 -1
  15. data/lib/fontisan/converters/outline_converter.rb +80 -18
  16. data/lib/fontisan/converters/woff_writer.rb +1 -1
  17. data/lib/fontisan/font_loader.rb +3 -5
  18. data/lib/fontisan/font_writer.rb +7 -6
  19. data/lib/fontisan/hints/hint_converter.rb +133 -0
  20. data/lib/fontisan/hints/postscript_hint_applier.rb +221 -140
  21. data/lib/fontisan/hints/postscript_hint_extractor.rb +100 -0
  22. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  23. data/lib/fontisan/hints/truetype_hint_extractor.rb +127 -0
  24. data/lib/fontisan/loading_modes.rb +2 -0
  25. data/lib/fontisan/models/font_export.rb +2 -2
  26. data/lib/fontisan/models/hint.rb +173 -1
  27. data/lib/fontisan/models/validation_report.rb +1 -1
  28. data/lib/fontisan/open_type_font.rb +25 -9
  29. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  30. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  31. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  32. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  33. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  34. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  35. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  36. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  37. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  38. data/lib/fontisan/tables/cff/charstring.rb +33 -4
  39. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  40. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  41. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  42. data/lib/fontisan/tables/cff/dict_builder.rb +15 -0
  43. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  44. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  45. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  46. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  47. data/lib/fontisan/tables/cff.rb +2 -0
  48. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  49. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  50. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  51. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  52. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  53. data/lib/fontisan/tables/cff2.rb +9 -4
  54. data/lib/fontisan/tables/cvar.rb +2 -41
  55. data/lib/fontisan/tables/gvar.rb +2 -41
  56. data/lib/fontisan/true_type_font.rb +24 -9
  57. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  58. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  59. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  60. data/lib/fontisan/validation/table_validator.rb +1 -1
  61. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  62. data/lib/fontisan/variation/converter.rb +120 -13
  63. data/lib/fontisan/variation/instance_writer.rb +341 -0
  64. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  65. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  66. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  67. data/lib/fontisan/version.rb +1 -1
  68. data/lib/fontisan/version.rb.orig +9 -0
  69. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  70. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  71. data/lib/fontisan/woff2_font.rb +475 -470
  72. data/lib/fontisan/woff_font.rb +16 -11
  73. data/lib/fontisan.rb +12 -0
  74. metadata +31 -2
@@ -24,13 +24,13 @@ module Fontisan
24
24
  # 4. Build gvar table structure
25
25
  #
26
26
  # @example Converting gvar to CFF2 blend
27
- # converter = VariationConverter.new(font, axes)
27
+ # converter = Converter.new(font, axes)
28
28
  # blend_data = converter.gvar_to_blend(glyph_id)
29
29
  #
30
30
  # @example Converting CFF2 blend to gvar
31
- # converter = VariationConverter.new(font, axes)
31
+ # converter = Converter.new(font, axes)
32
32
  # tuple_data = converter.blend_to_gvar(glyph_id)
33
- class VariationConverter
33
+ class Converter
34
34
  include TableAccessor
35
35
 
36
36
  # @return [TrueTypeFont, OpenTypeFont] Font instance
@@ -72,19 +72,49 @@ module Fontisan
72
72
  #
73
73
  # @param glyph_id [Integer] Glyph ID
74
74
  # @return [Hash, nil] Tuple data or nil
75
- def blend_to_gvar(_glyph_id)
75
+ def blend_to_gvar(glyph_id)
76
76
  return nil unless has_variation_table?("CFF2")
77
77
 
78
78
  cff2 = variation_table("CFF2")
79
79
  return nil unless cff2
80
80
 
81
81
  # Get CharString with blend operators
82
- # This is a placeholder - full implementation would parse CharString
83
- # and extract blend operator data
82
+ charstring = cff2.charstring_for_glyph(glyph_id)
83
+ return nil unless charstring
84
84
 
85
- # Convert blend data to tuples
86
- # Placeholder for full implementation
87
- nil
85
+ # Parse CharString to extract blend data
86
+ charstring.parse unless charstring.instance_variable_get(:@parsed)
87
+ blend_data = charstring.blend_data
88
+ return nil if blend_data.nil? || blend_data.empty?
89
+
90
+ # Convert blend data to tuple format
91
+ convert_blend_to_tuples_for_glyph(blend_data)
92
+ end
93
+
94
+ # Convert all glyphs from gvar to blend format
95
+ #
96
+ # @param glyph_count [Integer] Number of glyphs
97
+ # @return [Hash<Integer, Hash>] Map of glyph_id to blend data
98
+ def convert_all_gvar_to_blend(glyph_count)
99
+ return {} unless can_convert?
100
+
101
+ (0...glyph_count).each_with_object({}) do |glyph_id, result|
102
+ blend_data = gvar_to_blend(glyph_id)
103
+ result[glyph_id] = blend_data if blend_data
104
+ end
105
+ end
106
+
107
+ # Convert all glyphs from blend to gvar format
108
+ #
109
+ # @param glyph_count [Integer] Number of glyphs
110
+ # @return [Hash<Integer, Hash>] Map of glyph_id to tuple data
111
+ def convert_all_blend_to_gvar(glyph_count)
112
+ return {} unless can_convert?
113
+
114
+ (0...glyph_count).each_with_object({}) do |glyph_id, result|
115
+ tuple_data = blend_to_gvar(glyph_id)
116
+ result[glyph_id] = tuple_data if tuple_data
117
+ end
88
118
  end
89
119
 
90
120
  # Check if variation data can be converted
@@ -99,6 +129,76 @@ module Fontisan
99
129
 
100
130
  private
101
131
 
132
+ # Convert blend data from a glyph to tuple format
133
+ #
134
+ # @param blend_data [Array<Hash>] Array of blend operations
135
+ # @return [Hash] Tuple variation data
136
+ def convert_blend_to_tuples_for_glyph(blend_data)
137
+ # Each blend operation represents variation at different points
138
+ # We need to aggregate these into region-based tuples
139
+
140
+ # Extract all regions from blend operations
141
+ regions_map = {}
142
+ point_count = 0
143
+
144
+ blend_data.each_with_index do |blend_op, idx|
145
+ blend_op[:blends].each do |blend|
146
+ # Track the maximum point index we've seen
147
+ point_count = [point_count, idx + 1].max
148
+
149
+ # For each delta axis, we need to create or update a region
150
+ blend[:deltas].each_with_index do |delta, axis_index|
151
+ next if delta.zero? # Skip zero deltas
152
+
153
+ # Create region key based on unique delta pattern
154
+ region_key = "region_#{axis_index}"
155
+
156
+ regions_map[region_key] ||= {
157
+ axis_index: axis_index,
158
+ deltas_per_point: Array.new(point_count) { { x: 0, y: 0 } },
159
+ }
160
+
161
+ # Store this delta for this point
162
+ # Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
163
+ # This is a simplified mapping - full implementation would track
164
+ # which coordinates are being varied
165
+ regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0, y: 0 }
166
+ if idx.even?
167
+ regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
168
+ else
169
+ regions_map[region_key][:deltas_per_point][idx / 2][:y] = delta
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ # Convert regions to tuples
176
+ tuples = []
177
+ regions_map.each_value do |region_data|
178
+ axis_index = region_data[:axis_index]
179
+
180
+ # Build peak coordinates (one per axis)
181
+ peak = Array.new(@axes.length, 0.0)
182
+ peak[axis_index] = 1.0 if axis_index < @axes.length
183
+
184
+ # Build start/end (default full range)
185
+ start_vals = Array.new(@axes.length, -1.0)
186
+ end_vals = Array.new(@axes.length, 1.0)
187
+
188
+ tuples << {
189
+ peak: peak,
190
+ start: start_vals,
191
+ end: end_vals,
192
+ deltas: region_data[:deltas_per_point],
193
+ }
194
+ end
195
+
196
+ {
197
+ tuples: tuples,
198
+ point_count: point_count,
199
+ }
200
+ end
201
+
102
202
  # Convert tuple variations to blend format
103
203
  #
104
204
  # @param tuple_data [Hash] Tuple variation data from gvar
@@ -172,12 +272,19 @@ module Fontisan
172
272
  # @param tuple [Hash] Tuple data
173
273
  # @param point_count [Integer] Number of points
174
274
  # @return [Array<Hash>] Deltas with :x and :y
175
- def parse_tuple_deltas(_tuple, point_count)
176
- # This is a placeholder - full implementation would:
177
- # 1. Parse delta data from tuple
275
+ def parse_tuple_deltas(tuple, point_count)
276
+ # If tuple has deltas array, use it
277
+ if tuple[:deltas].is_a?(Array)
278
+ return tuple[:deltas].map do |delta|
279
+ { x: delta[:x] || 0, y: delta[:y] || 0 }
280
+ end
281
+ end
282
+
283
+ # Otherwise return zeros (placeholder for parsing raw delta data)
284
+ # Full implementation would:
285
+ # 1. Parse delta data from tuple[:data]
178
286
  # 2. Decompress if needed
179
287
  # 3. Return array of { x: dx, y: dy } for each point
180
-
181
288
  Array.new(point_count) { { x: 0, y: 0 } }
182
289
  end
183
290
 
@@ -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
@@ -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