Class: Fontisan::Converters::FormatConverter
- Inherits:
-
Object
- Object
- Fontisan::Converters::FormatConverter
- Defined in:
- lib/fontisan/converters/format_converter.rb
Overview
Main orchestrator for font format conversions
FormatConverter is the
primary entry point for all format conversion operations. It:
- Selects appropriate conversion strategy based on source/target formats
- Validates conversions against the conversion matrix
- Validates user-supplied options against the selected strategy's schema
- Delegates actual conversion to strategy implementations
- Provides clean error messages for unsupported conversions
The converter uses a strategy pattern with pluggable strategies for
different conversion types. Each strategy declares its own options via
the ConversionStrategy.option DSL; the converter enforces the
format ↔ option mapping at runtime.
Constant Summary collapse
- STRATEGY_CLASSES =
Registry of all strategy classes. Single source of truth for option discovery and strategy lookup. Add a new format by appending its strategy class here.
[ TableCopier, OutlineConverter, Type1Converter, WoffWriter, Woff2Encoder, SvgGenerator, ].freeze
Instance Attribute Summary collapse
-
#conversion_matrix ⇒ Hash
readonly
Conversion matrix loaded from config.
-
#strategies ⇒ Array
readonly
Available conversion strategies.
Class Method Summary collapse
-
.all_strategy_option_names ⇒ Array<Symbol>
All option names declared by any strategy.
-
.strategies_for_target(target_format) ⇒ Array[Class]
Strategies whose supported_conversions include the given target.
-
.strategy_class_for(source_format, target_format) ⇒ Class?
Look up the strategy class that handles a given conversion.
-
.validate_options_for_target!(target_format, options) ⇒ void
Cross-format option check, independent of source format.
Instance Method Summary collapse
-
#all_conversions ⇒ Array<Hash>
Get all supported conversions.
-
#compatible_variation_formats?(source, target) ⇒ Boolean
Check if formats have compatible variation (same outline format).
-
#convert(font, target_format, options = {}) ⇒ Hash<String, String>
Convert font to target format.
-
#convert_variable_to_svg(font, options = {}) ⇒ Hash
Convert variable font to SVG at specific coordinates.
-
#convert_variation_data(font, tables, source_format, target_format, _options) ⇒ Hash<String, String>
Convert variation data between outline formats.
-
#convertible_variation_formats?(source, target) ⇒ Boolean
Check if formats allow variation conversion (different outline formats).
-
#default_conversion_matrix ⇒ Hash
Get default conversion matrix (fallback).
-
#default_conversion_matrix_path ⇒ String
Get default conversion matrix path.
-
#detect_format(font) ⇒ Symbol
Detect font format from tables.
-
#initialize(conversion_matrix_path: nil) ⇒ FormatConverter
constructor
Initialize converter with strategies.
-
#load_conversion_matrix(path) ⇒ Object
Load conversion matrix from YAML config.
-
#preserve_variation_data(font, tables, source_format, target_format, options) ⇒ Hash<String, String>
Preserve variation data from source to target.
-
#select_strategy(source_format, target_format) ⇒ ConversionStrategy
Select conversion strategy.
-
#slice_strategy_options(options, strategy) ⇒ Hash
Slice an options hash to only the keys declared by the given strategy.
-
#supported?(source_format, target_format) ⇒ Boolean
Check if a conversion is supported.
-
#supported_targets(source_format) ⇒ Array<Symbol>
Get list of supported target formats for a source format.
-
#validate_conversion_supported!(source_format, target_format) ⇒ Object
Validate conversion is supported.
-
#validate_parameters!(font, target_format) ⇒ Object
Validate conversion parameters.
-
#validate_strategy_options!(strategy, source_format, target_format, options) ⇒ void
Cross-format option check.
-
#variable_font?(font) ⇒ Boolean
Check if font is a variable font.
Constructor Details
#initialize(conversion_matrix_path: nil) ⇒ FormatConverter
Initialize converter with strategies
118 119 120 121 |
# File 'lib/fontisan/converters/format_converter.rb', line 118 def initialize(conversion_matrix_path: nil) @strategies = self.class::STRATEGY_CLASSES.map(&:new) load_conversion_matrix(conversion_matrix_path) end |
Instance Attribute Details
#conversion_matrix ⇒ Hash (readonly)
Returns Conversion matrix loaded from config.
109 110 111 |
# File 'lib/fontisan/converters/format_converter.rb', line 109 def conversion_matrix @conversion_matrix end |
#strategies ⇒ Array (readonly)
Returns Available conversion strategies.
112 113 114 |
# File 'lib/fontisan/converters/format_converter.rb', line 112 def strategies @strategies end |
Class Method Details
.all_strategy_option_names ⇒ Array<Symbol>
All option names declared by any strategy. Used by ConversionOptions to keep its generating-options list in sync without duplicating the schema.
51 52 53 |
# File 'lib/fontisan/converters/format_converter.rb', line 51 def all_strategy_option_names STRATEGY_CLASSES.flat_map { |k| k..map(&:name) }.uniq end |
.strategies_for_target(target_format) ⇒ Array[Class]
Strategies whose supported_conversions include the given target.
72 73 74 75 76 77 78 |
# File 'lib/fontisan/converters/format_converter.rb', line 72 def strategies_for_target(target_format) STRATEGY_CLASSES.select do |klass| klass.new.supported_conversions.any? { |(_, t)| t == target_format } rescue NotImplementedError false end end |
.strategy_class_for(source_format, target_format) ⇒ Class?
Look up the strategy class that handles a given conversion.
60 61 62 63 64 65 66 |
# File 'lib/fontisan/converters/format_converter.rb', line 60 def strategy_class_for(source_format, target_format) STRATEGY_CLASSES.find do |klass| klass.new.supports?(source_format, target_format) rescue NotImplementedError false end end |
.validate_options_for_target!(target_format, options) ⇒ void
This method returns an undefined value.
Cross-format option check, independent of source format. Used by
OutputWriter (which doesn't know the source format at write time)
to catch e.g. --brotli-quality passed with --to woff.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/fontisan/converters/format_converter.rb', line 89 def (target_format, ) handlers = strategies_for_target(target_format) handler_names = handlers.flat_map do |k| k..map(&:name) end.to_set non_handler_names = (STRATEGY_CLASSES - handlers) .flat_map { |k| k..map(&:name) } conflicts = .keys.select do |k| non_handler_names.include?(k.to_sym) end return if conflicts.empty? accepted = handler_names.empty? ? "(none)" : handler_names.join(", ") raise ArgumentError, "Option(s) #{conflicts.map(&:inspect).join(', ')} do not apply " \ "to --to #{target_format}. Accepted for #{target_format}: #{accepted}" end |
Instance Method Details
#all_conversions ⇒ Array<Hash>
Get all supported conversions
228 229 230 231 232 233 234 235 236 237 |
# File 'lib/fontisan/converters/format_converter.rb', line 228 def all_conversions return [] unless conversion_matrix conversions = conversion_matrix["conversions"] return [] unless conversions conversions.map do |conv| { from: conv["from"].to_sym, to: conv["to"].to_sym } end end |
#compatible_variation_formats?(source, target) ⇒ Boolean
Check if formats have compatible variation (same outline format)
306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/fontisan/converters/format_converter.rb', line 306 def compatible_variation_formats?(source, target) # Same format (copy operation) return true if source == target # Same outline format (just packaging change) (source == :ttf && target == :woff) || (source == :otf && target == :woff) || (source == :woff && target == :ttf) || (source == :woff && target == :otf) || (source == :ttf && target == :woff2) || (source == :otf && target == :woff2) end |
#convert(font, target_format, options = {}) ⇒ Hash<String, String>
Convert font to target format
This is the main entry point for format conversion. It:
- Detects source format from font
- Validates conversion is supported
- Selects appropriate strategy
- Delegates conversion to strategy
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/fontisan/converters/format_converter.rb', line 152 def convert(font, target_format, = {}) validate_parameters!(font, target_format) source_format = detect_format(font) validate_conversion_supported!(source_format, target_format) # Special case: Variable font to SVG if variable_font?(font) && target_format == :svg return convert_variable_to_svg(font, ) end strategy = select_strategy(source_format, target_format) # Enforce format ↔ option mapping at the orchestrator level so each # strategy only sees options it declared. Cross-format misuse # (e.g., `--zlib-level` on a WOFF2 conversion) raises here with a # clear message; the strategy itself never has to defensively # ignore unknown keys. (strategy, source_format, target_format, ) sliced = (, strategy) tables = strategy.convert( font, sliced.merge(target_format: target_format), ) # Preserve variation data if requested and font is variable if .fetch(:preserve_variation, true) && variable_font?(font) tables = preserve_variation_data( font, tables, source_format, target_format, , ) end tables end |
#convert_variable_to_svg(font, options = {}) ⇒ Hash
Convert variable font to SVG at specific coordinates
248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/fontisan/converters/format_converter.rb', line 248 def convert_variable_to_svg(font, = {}) coordinates = [:instance_coordinates] || {} generator = Variation::VariableSvgGenerator.new(font, coordinates) # Use named instance if specified if [:instance_index] generator.generate_named_instance([:instance_index], ) else generator.generate() end end |
#convert_variation_data(font, tables, source_format, target_format, _options) ⇒ Hash<String, String>
Convert variation data between outline formats
This is a placeholder for full TTF↔OTF variation conversion. Full implementation would:
- Use Variation::Converter to convert gvar ↔ CFF2 blend
- Build appropriate variation tables for target format
- Preserve common tables (fvar, avar, STAT, metrics)
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/fontisan/converters/format_converter.rb', line 344 def convert_variation_data(font, tables, source_format, target_format, ) # For now, just preserve common tables and warn about conversion warn "WARNING: Full variation conversion (#{source_format} → " \ "#{target_format}) not yet implemented. " \ "Preserving common variation tables only." # Preserve common tables (fvar, avar, STAT) but not format-specific Variation::VariationPreserver.preserve( font, tables, preserve_format_specific: false, preserve_metrics: true, ) end |
#convertible_variation_formats?(source, target) ⇒ Boolean
Check if formats allow variation conversion (different outline formats)
324 325 326 327 328 |
# File 'lib/fontisan/converters/format_converter.rb', line 324 def convertible_variation_formats?(source, target) # Different outline formats (need variation conversion) (source == :ttf && target == :otf) || (source == :otf && target == :ttf) end |
#default_conversion_matrix ⇒ Hash
Get default conversion matrix (fallback)
392 393 394 395 396 397 398 399 400 401 |
# File 'lib/fontisan/converters/format_converter.rb', line 392 def default_conversion_matrix { "conversions" => [ { "from" => "ttf", "to" => "ttf" }, { "from" => "otf", "to" => "otf" }, { "from" => "ttf", "to" => "otf" }, { "from" => "otf", "to" => "ttf" }, ], } end |
#default_conversion_matrix_path ⇒ String
Get default conversion matrix path
380 381 382 383 384 385 386 387 |
# File 'lib/fontisan/converters/format_converter.rb', line 380 def default_conversion_matrix_path File.join( __dir__, "..", "config", "conversion_matrix.yml", ) end |
#detect_format(font) ⇒ Symbol
Detect font format from tables
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/fontisan/converters/format_converter.rb', line 470 def detect_format(font) # Check for Type1Font first (uses different interface) return :type1 if font.is_a?(Type1Font) # Check for CFF/CFF2 tables (OpenType/CFF) if font.has_table?("CFF ") || font.has_table?("CFF2") :otf # Check for glyf table (TrueType) elsif font.has_table?("glyf") :ttf else raise Fontisan::Error, "Cannot detect font format: missing both CFF and glyf tables" end end |
#load_conversion_matrix(path) ⇒ Object
Load conversion matrix from YAML config
363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/fontisan/converters/format_converter.rb', line 363 def load_conversion_matrix(path) config_path = path || default_conversion_matrix_path @conversion_matrix = if File.exist?(config_path) YAML.load_file(config_path) else # Use default inline matrix if file doesn't exist default_conversion_matrix end rescue StandardError => e warn "Failed to load conversion matrix: #{e.message}" @conversion_matrix = default_conversion_matrix end |
#preserve_variation_data(font, tables, source_format, target_format, options) ⇒ Hash<String, String>
Preserve variation data from source to target
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/fontisan/converters/format_converter.rb', line 279 def preserve_variation_data(font, tables, source_format, target_format, ) # Case 1: Compatible formats (same outline format) - just copy tables if compatible_variation_formats?(source_format, target_format) Variation::VariationPreserver.preserve(font, tables, ) # Case 2: Different outline formats - convert variation data elsif convertible_variation_formats?(source_format, target_format) convert_variation_data(font, tables, source_format, target_format, ) # Case 3: Unsupported conversion else if [:preserve_variation] raise Fontisan::Error, "Cannot preserve variation data for " \ "#{source_format} → #{target_format}" end tables end end |
#select_strategy(source_format, target_format) ⇒ ConversionStrategy
Select conversion strategy
452 453 454 455 456 457 458 459 460 461 462 463 |
# File 'lib/fontisan/converters/format_converter.rb', line 452 def select_strategy(source_format, target_format) strategy = strategies.find do |s| s.supports?(source_format, target_format) end unless strategy raise Fontisan::Error, "No strategy available for #{source_format} → #{target_format}" end strategy end |
#slice_strategy_options(options, strategy) ⇒ Hash
Slice an options hash to only the keys declared by the given strategy.
530 531 532 533 |
# File 'lib/fontisan/converters/format_converter.rb', line 530 def (, strategy) names = strategy.class..to_set(&:name) .select { |k, _| names.include?(k.to_sym) } end |
#supported?(source_format, target_format) ⇒ Boolean
Check if a conversion is supported
198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/fontisan/converters/format_converter.rb', line 198 def supported?(source_format, target_format) return false unless conversion_matrix conversions = conversion_matrix["conversions"] return false unless conversions conversions.any? do |conv| conv["from"] == source_format.to_s && conv["to"] == target_format.to_s end end |
#supported_targets(source_format) ⇒ Array<Symbol>
Get list of supported target formats for a source format
214 215 216 217 218 219 220 221 222 223 |
# File 'lib/fontisan/converters/format_converter.rb', line 214 def supported_targets(source_format) return [] unless conversion_matrix conversions = conversion_matrix["conversions"] return [] unless conversions conversions .select { |conv| conv["from"] == source_format.to_s } .map { |conv| conv["to"].to_sym } end |
#validate_conversion_supported!(source_format, target_format) ⇒ Object
Validate conversion is supported
431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
# File 'lib/fontisan/converters/format_converter.rb', line 431 def validate_conversion_supported!(source_format, target_format) unless supported?(source_format, target_format) available = supported_targets(source_format) = "Conversion from #{source_format} to #{target_format} " \ "is not supported." += if available.any? " Available targets for #{source_format}: " \ "#{available.join(', ')}" else " No conversions available from #{source_format}." end raise Fontisan::Error, end end |
#validate_parameters!(font, target_format) ⇒ Object
Validate conversion parameters
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/fontisan/converters/format_converter.rb', line 408 def validate_parameters!(font, target_format) raise ArgumentError, "Font cannot be nil" if font.nil? # Type1Font uses a different interface (font_dictionary, charstrings, etc.) # rather than the SFNT table interface is_type1 = font.is_a?(Type1Font) unless is_type1 || font.is_a?(SfntSource) raise ArgumentError, "Font must be an SfntSource instance or a Type1Font" end unless target_format.is_a?(Symbol) raise ArgumentError, "target_format must be a Symbol, got: #{target_format.class}" end end |
#validate_strategy_options!(strategy, source_format, target_format, options) ⇒ void
This method returns an undefined value.
Cross-format option check. Any user-supplied key that belongs to
another strategy (i.e., is valid for some other format) raises
immediately — this is what makes --zlib-level --to woff2 fail
predictably.
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 |
# File 'lib/fontisan/converters/format_converter.rb', line 498 def (strategy, source_format, target_format, ) this_class = strategy.class other_classes = self.class::STRATEGY_CLASSES.reject do |k| k == this_class end other_names = other_classes.flat_map do |k| k..map(&:name) end conflicts = .keys.select do |k| other_names.include?(k.to_sym) end if conflicts.any? accepted = this_class..map(&:name) accepted_list = accepted.empty? ? "(none)" : accepted.join(", ") raise ArgumentError, "Option(s) #{conflicts.map(&:inspect).join(', ')} do not apply " \ "to #{source_format}→#{target_format} " \ "(#{this_class.name.demodulize}). " \ "Accepted: #{accepted_list}" end this_class.((, strategy)) end |
#variable_font?(font) ⇒ Boolean
Check if font is a variable font
264 265 266 267 268 269 |
# File 'lib/fontisan/converters/format_converter.rb', line 264 def variable_font?(font) # Type 1 fonts are never variable fonts return false if font.is_a?(Type1Font) font.has_table?("fvar") end |