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,77 @@
1
+ # WOFF2 Encoding Configuration
2
+ #
3
+ # This file defines settings for WOFF2 font encoding, including
4
+ # Brotli compression parameters and table transformation options.
5
+ #
6
+ # Reference: https://www.w3.org/TR/WOFF2/
7
+
8
+ # Brotli compression settings
9
+ brotli:
10
+ # Compression quality level (0-11)
11
+ # Higher values give better compression but slower encoding
12
+ # Quality 11 is recommended for production web fonts
13
+ quality: 11
14
+
15
+ # Compression mode
16
+ # :font - Optimized for font data (recommended)
17
+ # :text - Optimized for text data
18
+ # :generic - General purpose compression
19
+ mode: font
20
+
21
+ # Table transformation settings
22
+ transformations:
23
+ # Enable table transformations for better compression
24
+ # For Phase 2 Milestone 2.1: disabled (architecture in place)
25
+ # Future milestones will enable these for optimization
26
+ enabled: false
27
+
28
+ # Transform glyf and loca tables
29
+ # Combines glyf and loca into single stream with delta encoding
30
+ # Typical compression improvement: 20-30%
31
+ glyf_loca: false
32
+
33
+ # Transform hmtx table
34
+ # Compact representation of horizontal metrics
35
+ # Typical compression improvement: 10-20%
36
+ hmtx: false
37
+
38
+ # Metadata settings
39
+ metadata:
40
+ # Include font metadata in WOFF2 file
41
+ include: false
42
+
43
+ # Metadata compression
44
+ compress: true
45
+
46
+ # Private data settings
47
+ private_data:
48
+ # Include private data block
49
+ include: false
50
+
51
+ # Validation settings
52
+ validation:
53
+ # Verify checksums after encoding
54
+ verify_checksums: true
55
+
56
+ # Validate WOFF2 structure
57
+ validate_structure: true
58
+
59
+ # Optimization settings
60
+ optimization:
61
+ # Reorder tables for optimal compression
62
+ # Tables are sorted by tag for consistency
63
+ reorder_tables: true
64
+
65
+ # Remove unnecessary padding
66
+ minimize_padding: true
67
+
68
+ # Logging settings
69
+ logging:
70
+ # Log compression ratios
71
+ log_compression: true
72
+
73
+ # Log table sizes
74
+ log_table_sizes: false
75
+
76
+ # Verbose output
77
+ verbose: false
@@ -30,6 +30,15 @@ module Fontisan
30
30
  # the checksum adjustment field.
31
31
  HEAD_TAG = "head"
32
32
 
33
+ # Hhea table tag identifier (Horizontal Header)
34
+ HHEA_TAG = "hhea"
35
+
36
+ # Hmtx table tag identifier (Horizontal Metrics)
37
+ HMTX_TAG = "hmtx"
38
+
39
+ # Maxp table tag identifier (Maximum Profile)
40
+ MAXP_TAG = "maxp"
41
+
33
42
  # Name table tag identifier
34
43
  NAME_TAG = "name"
35
44
 
@@ -60,6 +69,24 @@ module Fontisan
60
69
  # Fvar table tag identifier (Font Variations)
61
70
  FVAR_TAG = "fvar"
62
71
 
72
+ # Gvar table tag identifier (Glyph Variations for TrueType)
73
+ GVAR_TAG = "gvar"
74
+
75
+ # HVAR table tag identifier (Horizontal Metrics Variations)
76
+ HVAR_TAG = "HVAR"
77
+
78
+ # MVAR table tag identifier (Metrics Variations)
79
+ MVAR_TAG = "MVAR"
80
+
81
+ # VVAR table tag identifier (Vertical Metrics Variations)
82
+ VVAR_TAG = "VVAR"
83
+
84
+ # Cvar table tag identifier (CVT Variations)
85
+ CVAR_TAG = "cvar"
86
+
87
+ # CFF2 table tag identifier (CFF version 2 with variations)
88
+ CFF2_TAG = "CFF2"
89
+
63
90
  # Magic number used for font file checksum adjustment calculation.
64
91
  # This constant is used in conjunction with the file checksum to compute
65
92
  # the checksumAdjustment value stored in the 'head' table.
@@ -74,5 +101,47 @@ module Fontisan
74
101
  # All table data in TTF files must be aligned to 4-byte boundaries,
75
102
  # with padding added as necessary.
76
103
  TABLE_ALIGNMENT = 4
104
+
105
+ # Common font subfamily names for string interning
106
+ #
107
+ # These strings are frozen and reused to reduce memory allocations
108
+ # when parsing fonts with common subfamily names.
109
+ STRING_POOL = {
110
+ "Regular" => "Regular".freeze,
111
+ "Bold" => "Bold".freeze,
112
+ "Italic" => "Italic".freeze,
113
+ "Bold Italic" => "Bold Italic".freeze,
114
+ "BoldItalic" => "BoldItalic".freeze,
115
+ "Light" => "Light".freeze,
116
+ "Medium" => "Medium".freeze,
117
+ "Semibold" => "Semibold".freeze,
118
+ "SemiBold" => "SemiBold".freeze,
119
+ "Black" => "Black".freeze,
120
+ "Thin" => "Thin".freeze,
121
+ "ExtraLight" => "ExtraLight".freeze,
122
+ "Extra Light" => "Extra Light".freeze,
123
+ "ExtraBold" => "ExtraBold".freeze,
124
+ "Extra Bold" => "Extra Bold".freeze,
125
+ "Heavy" => "Heavy".freeze,
126
+ "Book" => "Book".freeze,
127
+ "Roman" => "Roman".freeze,
128
+ "Normal" => "Normal".freeze,
129
+ "Oblique" => "Oblique".freeze,
130
+ "Light Italic" => "Light Italic".freeze,
131
+ "Medium Italic" => "Medium Italic".freeze,
132
+ "Semibold Italic" => "Semibold Italic".freeze,
133
+ "Bold Oblique" => "Bold Oblique".freeze,
134
+ }.freeze
135
+
136
+ # Intern a string using the string pool
137
+ #
138
+ # If the string is in the pool, returns the pooled instance.
139
+ # Otherwise, freezes and returns the original string.
140
+ #
141
+ # @param str [String] The string to intern
142
+ # @return [String] The interned string
143
+ def self.intern_string(str)
144
+ STRING_POOL[str] || str.freeze
145
+ end
77
146
  end
78
147
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Converters
5
+ # Interface module for font format conversion strategies
6
+ #
7
+ # [`ConversionStrategy`](lib/fontisan/converters/conversion_strategy.rb)
8
+ # defines the contract that all conversion strategy classes must implement.
9
+ # This follows the Strategy pattern to enable polymorphic handling of
10
+ # different conversion types (TTF→OTF, OTF→TTF, same-format copying).
11
+ #
12
+ # Each strategy must implement:
13
+ # - convert(font, options) - Perform the actual conversion
14
+ # - supported_conversions - Return array of [source, target] format pairs
15
+ # - validate(font, target_format) - Validate conversion is possible
16
+ #
17
+ # Strategies are selected by [`FormatConverter`](lib/fontisan/converters/format_converter.rb)
18
+ # based on source and target formats.
19
+ #
20
+ # @example Implementing a strategy
21
+ # class MyStrategy
22
+ # include Fontisan::Converters::ConversionStrategy
23
+ #
24
+ # def convert(font, options = {})
25
+ # # Perform conversion
26
+ # tables = {...}
27
+ # tables
28
+ # end
29
+ #
30
+ # def supported_conversions
31
+ # [[:ttf, :otf], [:otf, :ttf]]
32
+ # end
33
+ #
34
+ # def validate(font, target_format)
35
+ # # Validate font can be converted
36
+ # raise Error unless valid
37
+ # end
38
+ # end
39
+ module ConversionStrategy
40
+ # Convert font to target format
41
+ #
42
+ # This method must return a hash of table tags to binary data,
43
+ # which will be assembled into a complete font by FontWriter.
44
+ #
45
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
46
+ # @param options [Hash] Conversion options
47
+ # @return [Hash<String, String>] Map of table tags to binary data
48
+ # @raise [NotImplementedError] If not implemented by strategy
49
+ def convert(font, options = {})
50
+ raise NotImplementedError,
51
+ "#{self.class.name} must implement convert(font, options)"
52
+ end
53
+
54
+ # Get list of supported conversions
55
+ #
56
+ # Returns an array of [source_format, target_format] pairs that
57
+ # this strategy can handle.
58
+ #
59
+ # @return [Array<Array<Symbol>>] Supported conversion pairs
60
+ # @raise [NotImplementedError] If not implemented by strategy
61
+ #
62
+ # @example
63
+ # strategy.supported_conversions
64
+ # # => [[:ttf, :otf], [:otf, :ttf]]
65
+ def supported_conversions
66
+ raise NotImplementedError,
67
+ "#{self.class.name} must implement supported_conversions"
68
+ end
69
+
70
+ # Validate that conversion is possible
71
+ #
72
+ # Checks if the given font can be converted to the target format.
73
+ # Should raise an error with a clear message if conversion is not
74
+ # possible.
75
+ #
76
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
77
+ # @param target_format [Symbol] Target format (:ttf, :otf, etc.)
78
+ # @return [Boolean] True if valid
79
+ # @raise [Error] If conversion is not possible
80
+ # @raise [NotImplementedError] If not implemented by strategy
81
+ def validate(font, target_format)
82
+ raise NotImplementedError,
83
+ "#{self.class.name} must implement validate(font, target_format)"
84
+ end
85
+
86
+ # Check if strategy supports a conversion
87
+ #
88
+ # @param source_format [Symbol] Source format
89
+ # @param target_format [Symbol] Target format
90
+ # @return [Boolean] True if supported
91
+ def supports?(source_format, target_format)
92
+ supported_conversions.include?([source_format, target_format])
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "conversion_strategy"
4
+ require_relative "table_copier"
5
+ require_relative "outline_converter"
6
+ require_relative "woff_writer"
7
+ require_relative "woff2_encoder"
8
+ require_relative "svg_generator"
9
+ require "yaml"
10
+
11
+ module Fontisan
12
+ module Converters
13
+ # Main orchestrator for font format conversions
14
+ #
15
+ # [`FormatConverter`](lib/fontisan/converters/format_converter.rb) is the
16
+ # primary entry point for all format conversion operations. It:
17
+ # - Selects appropriate conversion strategy based on source/target formats
18
+ # - Validates conversions against the conversion matrix
19
+ # - Delegates actual conversion to strategy implementations
20
+ # - Provides clean error messages for unsupported conversions
21
+ #
22
+ # The converter uses a strategy pattern with pluggable strategies for
23
+ # different conversion types:
24
+ # - OutlineConverter: TTF ↔ OTF conversions
25
+ # - TableCopier: Same-format operations
26
+ # - Woff2Encoder: TTF/OTF → WOFF2 compression
27
+ # - SvgGenerator: TTF/OTF → SVG font generation
28
+ #
29
+ # Supported conversions are defined in the conversion matrix configuration
30
+ # file, making it easy to extend without modifying code.
31
+ #
32
+ # @example Converting TTF to OTF
33
+ # converter = Fontisan::Converters::FormatConverter.new
34
+ # tables = converter.convert(font, :otf)
35
+ # FontWriter.write_to_file(tables, 'output.otf',
36
+ # sfnt_version: 0x4F54544F)
37
+ #
38
+ # @example Same-format copy
39
+ # converter = Fontisan::Converters::FormatConverter.new
40
+ # tables = converter.convert(font, :ttf) # TTF to TTF
41
+ # FontWriter.write_to_file(tables, 'copy.ttf')
42
+ class FormatConverter
43
+ # @return [Hash] Conversion matrix loaded from config
44
+ attr_reader :conversion_matrix
45
+
46
+ # @return [Array] Available conversion strategies
47
+ attr_reader :strategies
48
+
49
+ # Initialize converter with strategies
50
+ #
51
+ # @param conversion_matrix_path [String, nil] Path to conversion matrix
52
+ # config. If nil, uses default.
53
+ def initialize(conversion_matrix_path: nil)
54
+ @strategies = [
55
+ TableCopier.new,
56
+ OutlineConverter.new,
57
+ WoffWriter.new,
58
+ Woff2Encoder.new,
59
+ SvgGenerator.new,
60
+ ]
61
+
62
+ load_conversion_matrix(conversion_matrix_path)
63
+ end
64
+
65
+ # Convert font to target format
66
+ #
67
+ # This is the main entry point for format conversion. It:
68
+ # 1. Detects source format from font
69
+ # 2. Validates conversion is supported
70
+ # 3. Selects appropriate strategy
71
+ # 4. Delegates conversion to strategy
72
+ #
73
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
74
+ # @param target_format [Symbol] Target format (:ttf, :otf, :woff2, :svg)
75
+ # @param options [Hash] Additional conversion options
76
+ # @return [Hash<String, String>] Map of table tags to binary data
77
+ # @raise [ArgumentError] If parameters are invalid
78
+ # @raise [Error] If conversion is not supported
79
+ #
80
+ # @example
81
+ # tables = converter.convert(font, :otf)
82
+ def convert(font, target_format, options = {})
83
+ validate_parameters!(font, target_format)
84
+
85
+ source_format = detect_format(font)
86
+ validate_conversion_supported!(source_format, target_format)
87
+
88
+ strategy = select_strategy(source_format, target_format)
89
+ strategy.convert(font, options.merge(target_format: target_format))
90
+ end
91
+
92
+ # Check if a conversion is supported
93
+ #
94
+ # @param source_format [Symbol] Source format
95
+ # @param target_format [Symbol] Target format
96
+ # @return [Boolean] True if conversion is supported
97
+ def supported?(source_format, target_format)
98
+ return false unless conversion_matrix
99
+
100
+ conversions = conversion_matrix["conversions"]
101
+ return false unless conversions
102
+
103
+ conversions.any? do |conv|
104
+ conv["from"] == source_format.to_s &&
105
+ conv["to"] == target_format.to_s
106
+ end
107
+ end
108
+
109
+ # Get list of supported target formats for a source format
110
+ #
111
+ # @param source_format [Symbol] Source format
112
+ # @return [Array<Symbol>] Supported target formats
113
+ def supported_targets(source_format)
114
+ return [] unless conversion_matrix
115
+
116
+ conversions = conversion_matrix["conversions"]
117
+ return [] unless conversions
118
+
119
+ conversions
120
+ .select { |conv| conv["from"] == source_format.to_s }
121
+ .map { |conv| conv["to"].to_sym }
122
+ end
123
+
124
+ # Get all supported conversions
125
+ #
126
+ # @return [Array<Hash>] Array of conversion hashes with :from and :to
127
+ def all_conversions
128
+ return [] unless conversion_matrix
129
+
130
+ conversions = conversion_matrix["conversions"]
131
+ return [] unless conversions
132
+
133
+ conversions.map do |conv|
134
+ { from: conv["from"].to_sym, to: conv["to"].to_sym }
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ # Load conversion matrix from YAML config
141
+ #
142
+ # @param path [String, nil] Path to config file
143
+ def load_conversion_matrix(path)
144
+ config_path = path || default_conversion_matrix_path
145
+
146
+ @conversion_matrix = if File.exist?(config_path)
147
+ YAML.load_file(config_path)
148
+ else
149
+ # Use default inline matrix if file doesn't exist
150
+ default_conversion_matrix
151
+ end
152
+ rescue StandardError => e
153
+ warn "Failed to load conversion matrix: #{e.message}"
154
+ @conversion_matrix = default_conversion_matrix
155
+ end
156
+
157
+ # Get default conversion matrix path
158
+ #
159
+ # @return [String] Path to conversion matrix config
160
+ def default_conversion_matrix_path
161
+ File.join(
162
+ __dir__,
163
+ "..",
164
+ "config",
165
+ "conversion_matrix.yml",
166
+ )
167
+ end
168
+
169
+ # Get default conversion matrix (fallback)
170
+ #
171
+ # @return [Hash] Default conversion matrix
172
+ def default_conversion_matrix
173
+ {
174
+ "conversions" => [
175
+ { "from" => "ttf", "to" => "ttf" },
176
+ { "from" => "otf", "to" => "otf" },
177
+ { "from" => "ttf", "to" => "otf" },
178
+ { "from" => "otf", "to" => "ttf" },
179
+ ],
180
+ }
181
+ end
182
+
183
+ # Validate conversion parameters
184
+ #
185
+ # @param font [Object] Font object
186
+ # @param target_format [Symbol] Target format
187
+ # @raise [ArgumentError] If parameters are invalid
188
+ def validate_parameters!(font, target_format)
189
+ raise ArgumentError, "Font cannot be nil" if font.nil?
190
+
191
+ unless font.respond_to?(:table)
192
+ raise ArgumentError, "Font must respond to :table method"
193
+ end
194
+
195
+ unless target_format.is_a?(Symbol)
196
+ raise ArgumentError,
197
+ "target_format must be a Symbol, got: #{target_format.class}"
198
+ end
199
+ end
200
+
201
+ # Validate conversion is supported
202
+ #
203
+ # @param source_format [Symbol] Source format
204
+ # @param target_format [Symbol] Target format
205
+ # @raise [Error] If conversion is not supported
206
+ def validate_conversion_supported!(source_format, target_format)
207
+ unless supported?(source_format, target_format)
208
+ available = supported_targets(source_format)
209
+ message = "Conversion from #{source_format} to #{target_format} " \
210
+ "is not supported."
211
+ message += if available.any?
212
+ " Available targets for #{source_format}: " \
213
+ "#{available.join(', ')}"
214
+ else
215
+ " No conversions available from #{source_format}."
216
+ end
217
+ raise Fontisan::Error, message
218
+ end
219
+ end
220
+
221
+ # Select conversion strategy
222
+ #
223
+ # @param source_format [Symbol] Source format
224
+ # @param target_format [Symbol] Target format
225
+ # @return [ConversionStrategy] Selected strategy
226
+ # @raise [Error] If no strategy supports the conversion
227
+ def select_strategy(source_format, target_format)
228
+ strategy = strategies.find do |s|
229
+ s.supports?(source_format, target_format)
230
+ end
231
+
232
+ unless strategy
233
+ raise Fontisan::Error,
234
+ "No strategy available for #{source_format} → #{target_format}"
235
+ end
236
+
237
+ strategy
238
+ end
239
+
240
+ # Detect font format from tables
241
+ #
242
+ # @param font [TrueTypeFont, OpenTypeFont] Font to detect
243
+ # @return [Symbol] Format (:ttf or :otf)
244
+ # @raise [Error] If format cannot be detected
245
+ def detect_format(font)
246
+ # Check for CFF/CFF2 tables (OpenType/CFF)
247
+ if font.has_table?("CFF ") || font.has_table?("CFF2")
248
+ :otf
249
+ # Check for glyf table (TrueType)
250
+ elsif font.has_table?("glyf")
251
+ :ttf
252
+ else
253
+ raise Fontisan::Error,
254
+ "Cannot detect font format: missing both CFF and glyf tables"
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end