Class: Fontisan::Pipeline::TransformationPipeline

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/pipeline/transformation_pipeline.rb

Overview

Orchestrates universal font transformation pipeline

This is the main entry point for font conversion operations. It coordinates:

  1. Format detection (via FormatDetector)
  2. Font loading (via FontLoader)
  3. Variation resolution (via VariationResolver)
  4. Format conversion (via FormatConverter)
  5. Output writing (via OutputWriter)
  6. Validation (optional, via Validation::Validator)

The pipeline follows a clear MECE architecture where each phase has a single responsibility and produces well-defined outputs.

Examples:

Basic TTF to OTF conversion

pipeline = TransformationPipeline.new("input.ttf", "output.otf")
result = pipeline.transform
puts result[:success] # => true

Variable font instance generation

pipeline = TransformationPipeline.new(
  "variable.ttf",
  "bold.ttf",
  coordinates: { "wght" => 700.0 }
)
result = pipeline.transform

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input_path, output_path, options = {}) ⇒ TransformationPipeline

Initialize transformation pipeline

Parameters:

  • input_path (String)

    Path to input font

  • output_path (String)

    Path to output font

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

    Transformation options

Options Hash (options):

  • :target_format (Symbol)

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

  • :coordinates (Hash)

    Instance coordinates (for variable fonts)

  • :instance_index (Integer)

    Named instance index

  • :preserve_variation (Boolean)

    Preserve variation data (default: auto)

  • :validate (Boolean)

    Validate output (default: true)

  • :verbose (Boolean)

    Verbose output (default: false)



51
52
53
54
55
56
57
58
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 51

def initialize(input_path, output_path, options = {})
  @input_path = input_path
  @output_path = output_path
  @options = default_options.merge(options)
  @variation_strategy = nil

  validate_paths!
end

Instance Attribute Details

#input_pathString (readonly)

Returns Input file path.

Returns:

  • (String)

    Input file path



32
33
34
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 32

def input_path
  @input_path
end

#optionsHash (readonly)

Returns Transformation options.

Returns:

  • (Hash)

    Transformation options



38
39
40
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 38

def options
  @options
end

#output_pathString (readonly)

Returns Output file path.

Returns:

  • (String)

    Output file path



35
36
37
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 35

def output_path
  @output_path
end

Instance Method Details

#build_details(detection) ⇒ Hash

Build transformation details

Parameters:

  • detection (Hash)

    Detection results

Returns:

  • (Hash)

    Transformation details



350
351
352
353
354
355
356
357
358
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 350

def build_details(detection)
  {
    source_format: detection[:format],
    source_variation: detection[:variation_type],
    target_format: target_format,
    variation_strategy: @variation_strategy,
    variation_preserved: @variation_strategy == :preserve,
  }
end

#build_font_from_tables(tables, format) ⇒ Font

Build font object from tables

Parameters:

  • tables (Hash)

    Font tables

  • format (Symbol)

    Font format

Returns:

  • (Font)

    Font object



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 323

def build_font_from_tables(tables, format)
  # Detect outline type from tables
  has_cff = tables.key?("CFF ") || tables.key?("CFF2")
  has_glyf = tables.key?("glyf")

  if has_cff
    OpenTypeFont.from_tables(tables)
  elsif has_glyf
    TrueTypeFont.from_tables(tables)
  else
    # Default based on format
    case format
    when :ttf, :woff, :woff2
      TrueTypeFont.from_tables(tables)
    when :otf
      OpenTypeFont.from_tables(tables)
    else
      raise ArgumentError,
            "Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
    end
  end
end

#can_preserve_variation?(detection) ⇒ Boolean

Check if variation can be preserved for target format

Parameters:

  • detection (Hash)

    Detection results

Returns:

  • (Boolean)

    True if variation preservable



182
183
184
185
186
187
188
189
190
191
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 182

def can_preserve_variation?(detection)
  source_format = detection[:format]
  target = target_format

  # Same format
  return true if source_format == target

  # Same outline family (packaging change only)
  same_outline_family?(source_format, target)
end

#convert_format(tables, detection) ⇒ Hash

Convert format if needed

Parameters:

  • tables (Hash)

    Font tables

  • detection (Hash)

    Detection results

Returns:

  • (Hash)

    Converted tables



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 211

def convert_format(tables, detection)
  source_format = detection[:format]
  target = target_format

  # No conversion needed for same format
  return tables if source_format == target

  # Use FormatConverter for outline conversion
  if needs_outline_conversion?(source_format, target) || target == :svg
    converter = Converters::FormatConverter.new
    # Create temporary font object from tables
    font = build_font_from_tables(tables, source_format)
    converter.convert(font, target, @options)
  else
    # Just packaging change - tables can be used as-is
    tables
  end
end

#default_optionsHash

Default options

Returns:

  • (Hash)

    Default options



381
382
383
384
385
386
387
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 381

def default_options
  {
    validate: true,
    verbose: false,
    preserve_variation: nil, # Auto-determine
  }
end

#detect_input_formatHash

Detect input format and capabilities

Returns:

  • (Hash)

    Detection results from FormatDetector



113
114
115
116
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 113

def detect_input_format
  detector = FormatDetector.new(@input_path)
  detector.detect
end

#detect_target_from_extensionSymbol

Detect target format from output path extension

Returns:

  • (Symbol)

    Detected format



278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 278

def detect_target_from_extension
  ext = File.extname(@output_path).downcase
  case ext
  when ".ttf" then :ttf
  when ".otf" then :otf
  when ".woff" then :woff
  when ".woff2" then :woff2
  else
    raise ArgumentError,
          "Cannot determine target format from extension: #{ext}"
  end
end

#determine_variation_strategy(detection) ⇒ Symbol

Determine variation strategy based on options and compatibility

Parameters:

  • detection (Hash)

    Detection results

Returns:

  • (Symbol)

    Strategy type (:preserve, :instance, :named)



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 163

def determine_variation_strategy(detection)
  # User explicitly requested instance generation
  if @options[:coordinates] || @options[:instance_index]
    return @options[:instance_index] ? :named : :instance
  end

  # Check if preservation is possible
  if can_preserve_variation?(detection)
    @options.fetch(:preserve_variation, true) ? :preserve : :instance
  else
    # Cannot preserve - must generate instance
    :instance
  end
end

#export_only_format?Boolean

Check if target format is export-only (cannot be validated)

Returns:

  • (Boolean)

    True if format is export-only



400
401
402
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 400

def export_only_format?
  %i[svg woff woff2].include?(target_format)
end

#handle_error(error) ⇒ Object

Handle transformation error

Parameters:

  • error (StandardError)

    Error that occurred

Raises:

  • (Error)

    Re-raises with context



364
365
366
367
368
369
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 364

def handle_error(error)
  log "ERROR: #{error.message}"
  log error.backtrace.first(5).join("\n") if @options[:verbose]

  raise Error, "Transformation failed: #{error.message}"
end

#load_font(_detection) ⇒ Font

Load font with appropriate mode

Parameters:

  • detection (Hash)

    Detection results

Returns:

  • (Font)

    Loaded font object



122
123
124
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 122

def load_font(_detection)
  FontLoader.load(@input_path, mode: :full)
end

#log(message) ⇒ Object

Log message if verbose

Parameters:

  • message (String)

    Message to log



374
375
376
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 374

def log(message)
  puts "[TransformationPipeline] #{message}" if @options[:verbose]
end

#needs_outline_conversion?(source, target) ⇒ Boolean

Check if outline conversion is needed

Parameters:

  • source (Symbol)

    Source format

  • target (Symbol)

    Target format

Returns:

  • (Boolean)

    True if outline conversion needed



235
236
237
238
239
240
241
242
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 235

def needs_outline_conversion?(source, target)
  # TTF ↔ OTF requires outline conversion
  ttf_formats = %i[ttf ttc woff woff2]
  otf_formats = %i[otf otc]

  (ttf_formats.include?(source) && otf_formats.include?(target)) ||
    (otf_formats.include?(source) && ttf_formats.include?(target))
end

#resolve_static_font(font) ⇒ Hash

Resolve static font (just copy tables)

Parameters:

  • font (Font)

    Static font

Returns:

  • (Hash)

    Font tables



152
153
154
155
156
157
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 152

def resolve_static_font(font)
  @variation_strategy = :preserve

  # Get all tables from font - use table_data directly
  font.table_data.dup
end

#resolve_variation(font, detection) ⇒ Hash

Resolve variation data

Parameters:

  • font (Font)

    Loaded font

  • detection (Hash)

    Detection results

Returns:

  • (Hash)

    Processed font tables



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 131

def resolve_variation(font, detection)
  # Static fonts - use preserve strategy (just copy tables)
  return resolve_static_font(font) if detection[:variation_type] == :static

  # Variable fonts - determine strategy
  strategy = determine_variation_strategy(detection)
  @variation_strategy = strategy

  resolver = VariationResolver.new(
    font,
    strategy: strategy,
    **variation_options,
  )

  resolver.resolve
end

#same_format_conversion?Boolean

Check if this is a same-format conversion

Returns:

  • (Boolean)

    True if source and target formats are the same



392
393
394
395
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 392

def same_format_conversion?
  detection = detect_input_format
  detection[:format] == target_format
end

#same_outline_family?(source, target) ⇒ Boolean

Check if formats are in same outline family

Parameters:

  • source (Symbol)

    Source format

  • target (Symbol)

    Target format

Returns:

  • (Boolean)

    True if same family



198
199
200
201
202
203
204
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 198

def same_outline_family?(source, target)
  truetype_formats = %i[ttf ttc woff woff2]
  opentype_formats = %i[otf otc woff woff2]

  (truetype_formats.include?(source) && truetype_formats.include?(target)) ||
    (opentype_formats.include?(source) && opentype_formats.include?(target))
end

#target_formatSymbol

Get target format

Returns:

  • (Symbol)

    Target format



271
272
273
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 271

def target_format
  @options[:target_format] || detect_target_from_extension
end

#transformHash

Execute transformation pipeline

This is the main entry point. It orchestrates:

  1. Format detection
  2. Font loading
  3. Variation resolution
  4. Format conversion
  5. Output writing
  6. Validation (optional)

Returns:

  • (Hash)

    Transformation result with :success, :output_path, :details

Raises:

  • (Error)

    If transformation fails



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 72

def transform
  log "Starting transformation: #{@input_path}#{@output_path}"

  # Phase 1: Detect input format
  detection = detect_input_format
  log "Detected: #{detection[:format]} (#{detection[:variation_type]})"

  # Phase 2: Load font
  font = load_font(detection)
  log "Loaded: #{font.class.name}"

  # Phase 3: Resolve variation
  tables = resolve_variation(font, detection)
  log "Resolved variation using #{@variation_strategy} strategy"

  # Phase 4: Convert format
  tables = convert_format(tables, detection)
  log "Converted to #{target_format}"

  # Phase 5: Write output
  write_output(tables, detection)
  log "Written to #{@output_path}"

  # Phase 6: Validate (optional)
  validate_output if @options[:validate] && !same_format_conversion? && !export_only_format?
  log "Validation passed" if @options[:validate] && !export_only_format?

  {
    success: true,
    output_path: @output_path,
    details: build_details(detection),
  }
rescue StandardError => e
  handle_error(e)
end

#validate_outputObject

Validate output file

Raises:

  • (ValidationError)

    If validation fails



256
257
258
259
260
261
262
263
264
265
266
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 256

def validate_output
  return unless File.exist?(@output_path)

  # Use new validation framework with production profile
  report = Fontisan.validate(@output_path, profile: :production)

  return if report.valid?

  error_messages = report.errors.map(&:message).join(", ")
  raise Error, "Output validation failed: #{error_messages}"
end

#validate_paths!Object

Validate input and output paths

Raises:

  • (ArgumentError)

    If paths invalid



307
308
309
310
311
312
313
314
315
316
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 307

def validate_paths!
  unless File.exist?(@input_path)
    raise ArgumentError, "Input file not found: #{@input_path}"
  end

  output_dir = File.dirname(@output_path)
  unless File.directory?(output_dir)
    raise ArgumentError, "Output directory not found: #{output_dir}"
  end
end

#variation_optionsHash

Get variation options for VariationResolver

Returns:

  • (Hash)

    Variation options



294
295
296
297
298
299
300
301
302
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 294

def variation_options
  opts = {}
  opts[:coordinates] = @options[:coordinates] if @options[:coordinates]
  if @options[:instance_index]
    opts[:instance_index] =
      @options[:instance_index]
  end
  opts
end

#write_output(tables, _detection) ⇒ Object

Write output font file

Parameters:

  • tables (Hash)

    Font tables

  • detection (Hash)

    Detection results



248
249
250
251
# File 'lib/fontisan/pipeline/transformation_pipeline.rb', line 248

def write_output(tables, _detection)
  writer = OutputWriter.new(@output_path, target_format, @options)
  writer.write(tables)
end