Class: Fontisan::Converters::FormatConverter

Inherits:
Object
  • Object
show all
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.

Examples:

Converting TTF to OTF

converter = Fontisan::Converters::FormatConverter.new
tables = converter.convert(font, :otf)
FontWriter.write_to_file(tables, 'output.otf',
                         sfnt_version: 0x4F54544F)

Same-format copy

converter = Fontisan::Converters::FormatConverter.new
tables = converter.convert(font, :ttf)  # TTF to TTF
FontWriter.write_to_file(tables, 'copy.ttf')

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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(conversion_matrix_path: nil) ⇒ FormatConverter

Initialize converter with strategies

Parameters:

  • conversion_matrix_path (String, nil) (defaults to: nil)

    Path to conversion matrix config. If nil, uses default.



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_matrixHash (readonly)

Returns Conversion matrix loaded from config.

Returns:

  • (Hash)

    Conversion matrix loaded from config



109
110
111
# File 'lib/fontisan/converters/format_converter.rb', line 109

def conversion_matrix
  @conversion_matrix
end

#strategiesArray (readonly)

Returns Available conversion strategies.

Returns:

  • (Array)

    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_namesArray<Symbol>

All option names declared by any strategy. Used by ConversionOptions to keep its generating-options list in sync without duplicating the schema.

Returns:

  • (Array<Symbol>)


51
52
53
# File 'lib/fontisan/converters/format_converter.rb', line 51

def all_strategy_option_names
  STRATEGY_CLASSES.flat_map { |k| k.supported_options.map(&:name) }.uniq
end

.strategies_for_target(target_format) ⇒ Array[Class]

Strategies whose supported_conversions include the given target.

Parameters:

  • target_format (Symbol)

Returns:

  • (Array[Class])


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.

Parameters:

  • source_format (Symbol)
  • target_format (Symbol)

Returns:

  • (Class, nil)


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.

Parameters:

  • target_format (Symbol)
  • options (Hash)

Raises:

  • (ArgumentError)

    if a user-supplied option is declared only by strategies that don't handle this target



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 validate_options_for_target!(target_format, options)
  handlers = strategies_for_target(target_format)
  handler_names = handlers.flat_map do |k|
    k.supported_options.map(&:name)
  end.to_set
  non_handler_names = (STRATEGY_CLASSES - handlers)
    .flat_map { |k| k.supported_options.map(&:name) }

  conflicts = options.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_conversionsArray<Hash>

Get all supported conversions

Returns:

  • (Array<Hash>)

    Array of conversion hashes with :from and :to



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

#convert(font, target_format, options = {}) ⇒ Hash<String, String>

Convert font to target format

This is the main entry point for format conversion. It:

  1. Detects source format from font
  2. Validates conversion is supported
  3. Selects appropriate strategy
  4. Delegates conversion to strategy

Examples:

tables = converter.convert(font, :otf)

Variable font to SVG at specific weight

result = converter.convert(variable_font, :svg, instance_coordinates: { "wght" => 700.0 })

Convert with hint preservation

tables = converter.convert(font, :otf, preserve_hints: true)

Parameters:

  • font (TrueTypeFont, OpenTypeFont)

    Source font

  • target_format (Symbol)

    Target format (:ttf, :otf, :woff2, :svg)

  • options (Hash) (defaults to: {})

    Additional conversion options

Options Hash (options):

  • :preserve_variation (Boolean)

    Preserve variation data (default: true)

  • :preserve_hints (Boolean)

    Preserve rendering hints (default: false)

  • :instance_coordinates (Hash)

    Coordinates for variable→SVG

  • :instance_index (Integer)

    Named instance index for variable→SVG

Returns:

  • (Hash<String, String>)

    Map of table tags to binary data

Raises:

  • (ArgumentError)

    If parameters are invalid

  • (Error)

    If conversion is not supported



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, options = {})
  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, options)
  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.
  validate_strategy_options!(strategy, source_format, target_format,
                             options)
  sliced = slice_strategy_options(options, strategy)

  tables = strategy.convert(
    font,
    sliced.merge(target_format: target_format),
  )

  # Preserve variation data if requested and font is variable
  if options.fetch(:preserve_variation, true) && variable_font?(font)
    tables = preserve_variation_data(
      font,
      tables,
      source_format,
      target_format,
      options,
    )
  end

  tables
end

#supported?(source_format, target_format) ⇒ Boolean

Check if a conversion is supported

Parameters:

  • source_format (Symbol)

    Source format

  • target_format (Symbol)

    Target format

Returns:

  • (Boolean)

    True if 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

Parameters:

  • source_format (Symbol)

    Source format

Returns:

  • (Array<Symbol>)

    Supported target formats



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