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
@@ -211,7 +211,8 @@ module Fontisan
211
211
  batch_entries.each do |entry|
212
212
  relative_offset = entry.offset - batch_offset
213
213
  tag_key = entry.tag.dup.force_encoding("UTF-8")
214
- @table_data[tag_key] = batch_data[relative_offset, entry.table_length]
214
+ @table_data[tag_key] =
215
+ batch_data[relative_offset, entry.table_length]
215
216
  end
216
217
  end
217
218
 
@@ -258,6 +259,20 @@ module Fontisan
258
259
  true
259
260
  end
260
261
 
262
+ # Check if font is TrueType flavored
263
+ #
264
+ # @return [Boolean] false for OpenType fonts
265
+ def truetype?
266
+ false
267
+ end
268
+
269
+ # Check if font is CFF flavored
270
+ #
271
+ # @return [Boolean] true for OpenType fonts
272
+ def cff?
273
+ true
274
+ end
275
+
261
276
  # Check if font has a specific table
262
277
  #
263
278
  # @param tag [String] The table tag to check for
@@ -272,6 +287,7 @@ module Fontisan
272
287
  # @return [Boolean] true if table is available in current mode
273
288
  def table_available?(tag)
274
289
  return false unless has_table?(tag)
290
+
275
291
  LoadingModes.table_allowed?(@loading_mode, tag)
276
292
  end
277
293
 
@@ -490,6 +506,7 @@ module Fontisan
490
506
  Constants::OS2_TAG => Tables::Os2,
491
507
  Constants::POST_TAG => Tables::Post,
492
508
  Constants::CMAP_TAG => Tables::Cmap,
509
+ Constants::CFF_TAG => Tables::Cff,
493
510
  Constants::FVAR_TAG => Tables::Fvar,
494
511
  Constants::GSUB_TAG => Tables::Gsub,
495
512
  Constants::GPOS_TAG => Tables::Gpos,
@@ -558,18 +575,19 @@ module Fontisan
558
575
  # @param path [String] Path to the OTF file
559
576
  # @return [void]
560
577
  def update_checksum_adjustment_in_file(path)
561
- # Calculate file checksum
562
- checksum = Utilities::ChecksumCalculator.calculate_file_checksum(path)
578
+ # Use tempfile-based checksum calculation for Windows compatibility
579
+ # This keeps the tempfile alive until we're done with the checksum
580
+ File.open(path, "r+b") do |io|
581
+ checksum, _tmpfile = Utilities::ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
563
582
 
564
- # Calculate adjustment
565
- adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
583
+ # Calculate adjustment
584
+ adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
566
585
 
567
- # Find head table position
568
- head_entry = head_table
569
- return unless head_entry
586
+ # Find head table position
587
+ head_entry = head_table
588
+ return unless head_entry
570
589
 
571
- # Write adjustment to head table (offset 8 within head table)
572
- File.open(path, "r+b") do |io|
590
+ # Write adjustment to head table (offset 8 within head table)
573
591
  io.seek(head_entry.offset + 8)
574
592
  io.write([adjustment].pack("N"))
575
593
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ # Extensions to OpenTypeFont for table-based construction
5
+ class OpenTypeFont
6
+ # Create font from hash of tables
7
+ #
8
+ # This is used during font conversion when we have tables but not a file.
9
+ #
10
+ # @param tables [Hash<String, String>] Map of table tag to binary data
11
+ # @return [OpenTypeFont] New font instance
12
+ def self.from_tables(tables)
13
+ # Create minimal header structure
14
+ font = new
15
+ font.initialize_storage
16
+ font.loading_mode = LoadingModes::FULL
17
+
18
+ # Store table data
19
+ font.table_data = tables
20
+
21
+ # Build header from tables
22
+ num_tables = tables.size
23
+ max_power = 0
24
+ n = num_tables
25
+ while n > 1
26
+ n >>= 1
27
+ max_power += 1
28
+ end
29
+
30
+ search_range = (1 << max_power) * 16
31
+ entry_selector = max_power
32
+ range_shift = (num_tables * 16) - search_range
33
+
34
+ font.header.sfnt_version = 0x4F54544F # 'OTTO' for OpenType/CFF
35
+ font.header.num_tables = num_tables
36
+ font.header.search_range = search_range
37
+ font.header.entry_selector = entry_selector
38
+ font.header.range_shift = range_shift
39
+
40
+ # Build table directory
41
+ font.tables.clear
42
+ tables.each_key do |tag|
43
+ entry = TableDirectory.new
44
+ entry.tag = tag
45
+ entry.checksum = 0 # Will be calculated on write
46
+ entry.offset = 0 # Will be calculated on write
47
+ entry.table_length = tables[tag].bytesize
48
+ font.tables << entry
49
+ end
50
+
51
+ font
52
+ end
53
+ end
54
+ end
@@ -222,7 +222,8 @@ module Fontisan
222
222
  if @stack_aware
223
223
  tracker = @stack_trackers[glyph_id]
224
224
  next unless tracker
225
- next unless tracker.stack_neutral?(start_pos, start_pos + length)
225
+ next unless tracker.stack_neutral?(start_pos,
226
+ start_pos + length)
226
227
  end
227
228
 
228
229
  pattern_bytes = charstring[start_pos, length]
@@ -74,7 +74,7 @@ module Fontisan
74
74
  # 2. Analyze patterns
75
75
  analyzer = PatternAnalyzer.new(
76
76
  min_length: @min_pattern_length,
77
- stack_aware: true
77
+ stack_aware: true,
78
78
  )
79
79
  patterns = analyzer.analyze(charstrings)
80
80
 
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_loader"
4
+
5
+ module Fontisan
6
+ module Pipeline
7
+ # Detects font format and capabilities
8
+ #
9
+ # This class analyzes font files to determine:
10
+ # - Format: TTF, OTF, TTC, OTC, WOFF, WOFF2, SVG
11
+ # - Variation type: static, gvar (TrueType variable), CFF2 (OpenType variable)
12
+ # - Capabilities: outline type, variation support, collection support
13
+ #
14
+ # Used by the universal transformation pipeline to determine conversion
15
+ # strategies and validate compatibility.
16
+ #
17
+ # @example Detecting a font's format
18
+ # detector = FormatDetector.new("font.ttf")
19
+ # info = detector.detect
20
+ # puts info[:format] # => :ttf
21
+ # puts info[:variation_type] # => :gvar
22
+ # puts info[:capabilities][:outline] # => :truetype
23
+ class FormatDetector
24
+ # @return [String] Path to font file
25
+ attr_reader :file_path
26
+
27
+ # @return [TrueTypeFont, OpenTypeFont, TrueTypeCollection, OpenTypeCollection, nil] Loaded font
28
+ attr_reader :font
29
+
30
+ # Initialize detector
31
+ #
32
+ # @param file_path [String] Path to font file
33
+ def initialize(file_path)
34
+ @file_path = file_path
35
+ @font = nil
36
+ end
37
+
38
+ # Detect format and capabilities
39
+ #
40
+ # @return [Hash] Detection results with :format, :variation_type, :capabilities
41
+ def detect
42
+ load_font
43
+
44
+ {
45
+ format: detect_format,
46
+ variation_type: detect_variation,
47
+ capabilities: detect_capabilities,
48
+ }
49
+ end
50
+
51
+ # Detect font format
52
+ #
53
+ # @return [Symbol] One of :ttf, :otf, :ttc, :otc, :woff, :woff2, :svg
54
+ def detect_format
55
+ # Check for SVG first (from file extension even if font failed to load)
56
+ return :svg if @file_path.end_with?(".svg")
57
+
58
+ return :unknown unless @font
59
+
60
+ # Use is_a? for proper class checking
61
+ case @font
62
+ when Fontisan::TrueTypeCollection
63
+ :ttc
64
+ when Fontisan::OpenTypeCollection
65
+ :otc
66
+ when Fontisan::TrueTypeFont
67
+ if @file_path.end_with?(".woff")
68
+ :woff
69
+ elsif @file_path.end_with?(".woff2")
70
+ :woff2
71
+ else
72
+ :ttf
73
+ end
74
+ when Fontisan::OpenTypeFont
75
+ if @file_path.end_with?(".woff")
76
+ :woff
77
+ elsif @file_path.end_with?(".woff2")
78
+ :woff2
79
+ else
80
+ :otf
81
+ end
82
+ else
83
+ :unknown
84
+ end
85
+ end
86
+
87
+ # Detect variation type
88
+ #
89
+ # @return [Symbol] One of :static, :gvar, :cff2
90
+ def detect_variation
91
+ return :static unless @font
92
+
93
+ # Collections don't have has_table? method
94
+ # Return :static for collections (variation detection would need to load first font)
95
+ return :static if collection?
96
+
97
+ # Check for variable font tables
98
+ if @font.has_table?("fvar")
99
+ # Variable font detected - check variation type
100
+ if @font.has_table?("gvar")
101
+ :gvar # TrueType variable font
102
+ elsif @font.has_table?("CFF2")
103
+ :cff2 # OpenType variable font (CFF2)
104
+ else
105
+ :static # Has fvar but no variation data (shouldn't happen)
106
+ end
107
+ else
108
+ :static
109
+ end
110
+ end
111
+
112
+ # Detect font capabilities
113
+ #
114
+ # @return [Hash] Capabilities hash
115
+ def detect_capabilities
116
+ return default_capabilities unless @font
117
+
118
+ # Check if this is a collection
119
+ is_collection = collection?
120
+
121
+ font_to_check = if is_collection
122
+ # Collections don't have fonts method, need to load first font
123
+ nil # Will handle in API usage
124
+ else
125
+ @font
126
+ end
127
+
128
+ # For collections, return basic capabilities
129
+ if is_collection
130
+ return {
131
+ outline: :unknown, # Would need to load first font to know
132
+ variation: false, # Would need to load first font to know
133
+ collection: true,
134
+ tables: [],
135
+ }
136
+ end
137
+
138
+ return default_capabilities unless font_to_check
139
+
140
+ {
141
+ outline: detect_outline_type(font_to_check),
142
+ variation: detect_variation != :static,
143
+ collection: false,
144
+ tables: available_tables(font_to_check),
145
+ }
146
+ end
147
+
148
+ # Check if font is a collection
149
+ #
150
+ # @return [Boolean] True if collection (TTC/OTC)
151
+ def collection?
152
+ @font.is_a?(Fontisan::TrueTypeCollection) ||
153
+ @font.is_a?(Fontisan::OpenTypeCollection)
154
+ end
155
+
156
+ # Check if font is variable
157
+ #
158
+ # @return [Boolean] True if variable font
159
+ def variable?
160
+ detect_variation != :static
161
+ end
162
+
163
+ # Check if format is compatible with target
164
+ #
165
+ # @param target_format [Symbol] Target format (:ttf, :otf, etc.)
166
+ # @return [Boolean] True if conversion is possible
167
+ def compatible_with?(target_format)
168
+ current_format = detect_format
169
+ variation_type = detect_variation
170
+
171
+ # Same format is always compatible
172
+ return true if current_format == target_format
173
+
174
+ # Collection formats
175
+ if %i[ttc otc].include?(current_format)
176
+ return %i[ttc otc].include?(target_format)
177
+ end
178
+
179
+ # Variable font constraints
180
+ if variation_type == :static
181
+ # Static fonts can convert to any format
182
+ true
183
+ else
184
+ case variation_type
185
+ when :gvar
186
+ # TrueType variable can convert to TrueType formats
187
+ %i[ttf ttc woff woff2].include?(target_format)
188
+ when :cff2
189
+ # OpenType variable can convert to OpenType formats
190
+ %i[otf otc woff woff2].include?(target_format)
191
+ end
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ # Load font from file
198
+ def load_font
199
+ # Check if it's a collection first
200
+ @font = if FontLoader.collection?(@file_path)
201
+ FontLoader.load_collection(@file_path)
202
+ else
203
+ FontLoader.load(@file_path, mode: :full)
204
+ end
205
+ rescue StandardError => e
206
+ warn "Failed to load font: #{e.message}"
207
+ @font = nil
208
+ end
209
+
210
+ # Detect outline type
211
+ #
212
+ # @param font [Font] Font object
213
+ # @return [Symbol] :truetype or :cff
214
+ def detect_outline_type(font)
215
+ if font.has_table?("glyf") || font.has_table?("gvar")
216
+ :truetype
217
+ elsif font.has_table?("CFF ") || font.has_table?("CFF2")
218
+ :cff
219
+ else
220
+ :unknown
221
+ end
222
+ end
223
+
224
+ # Get available tables
225
+ #
226
+ # @param font [Font] Font object
227
+ # @return [Array<String>] List of table tags
228
+ def available_tables(font)
229
+ return [] unless font.respond_to?(:table_names)
230
+
231
+ font.table_names
232
+ rescue StandardError
233
+ []
234
+ end
235
+
236
+ # Default capabilities when font cannot be loaded
237
+ #
238
+ # @return [Hash] Default capabilities
239
+ def default_capabilities
240
+ {
241
+ outline: :unknown,
242
+ variation: false,
243
+ collection: false,
244
+ tables: [],
245
+ }
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_writer"
4
+
5
+ module Fontisan
6
+ module Pipeline
7
+ # Handles writing font tables to various output formats
8
+ #
9
+ # This class abstracts the complexity of writing different font formats:
10
+ # - SFNT formats (TTF, OTF) via FontWriter
11
+ # - WOFF via WoffWriter
12
+ # - WOFF2 via Woff2Encoder
13
+ #
14
+ # Single Responsibility: Coordinate output writing for different formats
15
+ #
16
+ # @example Write TTF font
17
+ # writer = OutputWriter.new("output.ttf", :ttf)
18
+ # writer.write(tables)
19
+ #
20
+ # @example Write OTF font
21
+ # writer = OutputWriter.new("output.otf", :otf)
22
+ # writer.write(tables)
23
+ class OutputWriter
24
+ # @return [String] Output file path
25
+ attr_reader :output_path
26
+
27
+ # @return [Symbol] Target format
28
+ attr_reader :format
29
+
30
+ # @return [Hash] Writing options
31
+ attr_reader :options
32
+
33
+ # Initialize output writer
34
+ #
35
+ # @param output_path [String] Path to write output
36
+ # @param format [Symbol] Target format (:ttf, :otf, :woff, :woff2)
37
+ # @param options [Hash] Writing options
38
+ def initialize(output_path, format, options = {})
39
+ @output_path = output_path
40
+ @format = format
41
+ @options = options
42
+ end
43
+
44
+ # Write font tables to output file
45
+ #
46
+ # @param tables [Hash<String, String>, Hash] Font tables (tag => binary data) or special format result
47
+ # @return [Integer] Number of bytes written
48
+ # @raise [ArgumentError] If format is unsupported
49
+ def write(tables)
50
+ case @format
51
+ when :ttf, :otf
52
+ write_sfnt(tables)
53
+ when :woff
54
+ write_woff(tables)
55
+ when :woff2
56
+ write_woff2(tables)
57
+ when :svg
58
+ write_svg(tables)
59
+ else
60
+ raise ArgumentError, "Unsupported output format: #{@format}"
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Write SVG format
67
+ #
68
+ # @param result [Hash] Result with :svg_xml key
69
+ # @return [Integer] Number of bytes written
70
+ def write_svg(result)
71
+ svg_xml = result[:svg_xml] || result["svg_xml"]
72
+ unless svg_xml
73
+ raise ArgumentError,
74
+ "SVG result must contain :svg_xml key"
75
+ end
76
+
77
+ File.write(@output_path, svg_xml)
78
+ end
79
+
80
+ # Write SFNT format (TTF or OTF)
81
+ #
82
+ # @param tables [Hash<String, String>] Font tables
83
+ # @return [Integer] Number of bytes written
84
+ def write_sfnt(tables)
85
+ sfnt_version = determine_sfnt_version
86
+ FontWriter.write_to_file(tables, @output_path,
87
+ sfnt_version: sfnt_version)
88
+ end
89
+
90
+ # Write WOFF format
91
+ #
92
+ # @param tables [Hash<String, String>] Font tables
93
+ # @return [Integer] Number of bytes written
94
+ def write_woff(tables)
95
+ require_relative "../converters/woff_writer"
96
+
97
+ writer = Converters::WoffWriter.new
98
+ font = build_font_from_tables(tables)
99
+ result = writer.convert(font, @options)
100
+
101
+ File.binwrite(@output_path, result[:woff_data])
102
+ end
103
+
104
+ # Write WOFF2 format
105
+ #
106
+ # @param tables [Hash<String, String>] Font tables
107
+ # @return [Integer] Number of bytes written
108
+ def write_woff2(tables)
109
+ require_relative "../converters/woff2_encoder"
110
+
111
+ encoder = Converters::Woff2Encoder.new
112
+ font = build_font_from_tables(tables)
113
+ result = encoder.convert(font, @options)
114
+
115
+ File.binwrite(@output_path, result[:woff2_binary])
116
+ end
117
+
118
+ # Determine SFNT version based on format and tables
119
+ #
120
+ # @return [Integer] SFNT version (0x00010000 for TTF, 0x4F54544F for OTF)
121
+ def determine_sfnt_version
122
+ case @format
123
+ when :ttf, :woff, :woff2 then 0x00010000
124
+ when :otf then 0x4F54544F # 'OTTO'
125
+ else raise ArgumentError, "Unsupported format: #{@format}"
126
+ end
127
+ end
128
+
129
+ # Build font object from tables
130
+ #
131
+ # Helper to create font object from tables for converters that need it.
132
+ #
133
+ # @param tables [Hash<String, String>] Font tables
134
+ # @return [Font] Font object
135
+ def build_font_from_tables(tables)
136
+ # Detect font type from tables
137
+ has_cff = tables.key?("CFF ") || tables.key?("CFF2")
138
+ has_glyf = tables.key?("glyf")
139
+
140
+ if has_cff
141
+ OpenTypeFont.from_tables(tables)
142
+ elsif has_glyf
143
+ TrueTypeFont.from_tables(tables)
144
+ else
145
+ # Default based on format
146
+ case @format
147
+ when :ttf, :woff, :woff2
148
+ TrueTypeFont.from_tables(tables)
149
+ when :otf
150
+ OpenTypeFont.from_tables(tables)
151
+ else
152
+ raise ArgumentError,
153
+ "Cannot determine font type for format: #{@format}"
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Pipeline
5
+ module Strategies
6
+ # Base class for variation resolution strategies
7
+ #
8
+ # This abstract class defines the interface that all variation resolution
9
+ # strategies must implement. It follows the Strategy pattern to allow
10
+ # different approaches to handling variable font data during conversion.
11
+ #
12
+ # Subclasses must implement:
13
+ # - resolve(font): Process the font and return tables
14
+ # - preserves_variation?: Indicate if variation data is preserved
15
+ # - strategy_name: Return the strategy identifier
16
+ #
17
+ # @example Implementing a strategy
18
+ # class MyStrategy < BaseStrategy
19
+ # def resolve(font)
20
+ # # Implementation
21
+ # end
22
+ #
23
+ # def preserves_variation?
24
+ # false
25
+ # end
26
+ #
27
+ # def strategy_name
28
+ # :my_strategy
29
+ # end
30
+ # end
31
+ class BaseStrategy
32
+ # @return [Hash] Strategy options
33
+ attr_reader :options
34
+
35
+ # Initialize strategy with options
36
+ #
37
+ # @param options [Hash] Strategy-specific options
38
+ def initialize(options = {})
39
+ @options = options
40
+ end
41
+
42
+ # Resolve variation data
43
+ #
44
+ # This method must be implemented by subclasses to process the font
45
+ # and return the appropriate tables based on the strategy.
46
+ #
47
+ # @param font [TrueTypeFont, OpenTypeFont] Font to process
48
+ # @return [Hash<String, String>] Map of table tags to binary data
49
+ # @raise [NotImplementedError] If not implemented by subclass
50
+ def resolve(font)
51
+ raise NotImplementedError,
52
+ "#{self.class.name} must implement #resolve"
53
+ end
54
+
55
+ # Check if strategy preserves variation data
56
+ #
57
+ # @return [Boolean] True if variation data is preserved
58
+ # @raise [NotImplementedError] If not implemented by subclass
59
+ def preserves_variation?
60
+ raise NotImplementedError,
61
+ "#{self.class.name} must implement #preserves_variation?"
62
+ end
63
+
64
+ # Get strategy name
65
+ #
66
+ # @return [Symbol] Strategy identifier
67
+ # @raise [NotImplementedError] If not implemented by subclass
68
+ def strategy_name
69
+ raise NotImplementedError,
70
+ "#{self.class.name} must implement #strategy_name"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end