Class: Fontisan::Collection::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/collection/builder.rb

Overview

CollectionBuilder orchestrates TTC/OTC creation

Main responsibility: Coordinate the entire collection creation process including analysis, deduplication, offset calculation, and writing. Implements builder pattern for flexible configuration.

Examples:

Create TTC with default options

builder = CollectionBuilder.new([font1, font2, font3])
builder.build_to_file("family.ttc")

Create OTC with optimization

builder = CollectionBuilder.new([font1, font2, font3])
builder.format = :otc
builder.optimize = true
result = builder.build
puts "Saved #{result[:space_savings]} bytes"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(fonts, options = {}) ⇒ Builder

Initialize builder with fonts

Parameters:

Options Hash (options):

  • :format (Symbol)

    Format type (:ttc or :otc, default: :ttc)

  • :optimize (Boolean)

    Enable optimization (default: true)

  • :config (Hash)

    Configuration overrides

Raises:

  • (ArgumentError)

    if fonts array is invalid



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/fontisan/collection/builder.rb', line 56

def initialize(fonts, options = {})
  if fonts.nil? || fonts.empty?
    raise ArgumentError,
          "fonts cannot be nil or empty"
  end
  raise ArgumentError, "fonts must be an array" unless fonts.is_a?(Array)

  unless fonts.all? do |f|
    f.respond_to?(:table_data)
  end
    raise ArgumentError,
          "all fonts must respond to table_data"
  end

  @fonts = fonts
  @format = options[:format] || :ttc
  @optimize = options.fetch(:optimize, true)
  @config = load_config.merge(options[:config] || {})
  @result = nil

  validate_format!
end

Instance Attribute Details

#configHash

Configuration settings

Returns:

  • (Hash)


42
43
44
# File 'lib/fontisan/collection/builder.rb', line 42

def config
  @config
end

#fontsArray<TrueTypeFont, OpenTypeFont> (readonly)

Source fonts

Returns:



30
31
32
# File 'lib/fontisan/collection/builder.rb', line 30

def fonts
  @fonts
end

#formatSymbol

Collection format (:ttc or :otc)

Returns:

  • (Symbol)


34
35
36
# File 'lib/fontisan/collection/builder.rb', line 34

def format
  @format
end

#optimizeBoolean

Enable table sharing optimization

Returns:

  • (Boolean)


38
39
40
# File 'lib/fontisan/collection/builder.rb', line 38

def optimize
  @optimize
end

#resultHash? (readonly)

Build result (populated after build)

Returns:

  • (Hash, nil)


46
47
48
# File 'lib/fontisan/collection/builder.rb', line 46

def result
  @result
end

Instance Method Details

#analyzeHash

Get analysis report

Runs analysis without building the full collection. Useful for previewing space savings before committing to build.

Returns:

  • (Hash)

    Analysis report



141
142
143
144
# File 'lib/fontisan/collection/builder.rb', line 141

def analyze
  analyzer = TableAnalyzer.new(@fonts)
  analyzer.analyze
end

#buildHash

Build collection and return binary

Executes the complete collection creation process:

  1. Analyze tables across fonts

  2. Deduplicate identical tables

  3. Calculate file offsets

  4. Write binary structure

Returns:

  • (Hash)

    Build result with:

    • :binary [String] - Complete collection binary

    • :space_savings [Integer] - Bytes saved by sharing

    • :analysis [Hash] - Analysis report

    • :statistics [Hash] - Deduplication statistics



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/fontisan/collection/builder.rb', line 92

def build
  # Step 1: Analyze tables
  analyzer = TableAnalyzer.new(@fonts)
  analysis_report = analyzer.analyze

  # Step 2: Deduplicate tables
  deduplicator = TableDeduplicator.new(@fonts)
  sharing_map = deduplicator.build_sharing_map
  statistics = deduplicator.statistics

  # Step 3: Calculate offsets
  calculator = OffsetCalculator.new(sharing_map, @fonts)
  offsets = calculator.calculate

  # Step 4: Write collection
  writer = Writer.new(@fonts, sharing_map, offsets, format: @format)
  binary = writer.write_collection

  # Store result
  @result = {
    binary: binary,
    space_savings: analysis_report[:space_savings],
    analysis: analysis_report,
    statistics: statistics,
    format: @format,
    num_fonts: @fonts.size,
  }

  @result
end

#build_to_file(path) ⇒ Hash

Build collection and write to file

Parameters:

  • path (String)

    Output file path

Returns:

  • (Hash)

    Build result (same as build method)



127
128
129
130
131
132
133
# File 'lib/fontisan/collection/builder.rb', line 127

def build_to_file(path)
  result = build
  File.binwrite(path, result[:binary])
  result[:output_path] = path
  result[:output_size] = result[:binary].bytesize
  result
end

#potential_savingsInteger

Get potential space savings without building

Returns:

  • (Integer)

    Bytes that can be saved



149
150
151
# File 'lib/fontisan/collection/builder.rb', line 149

def potential_savings
  analyze[:space_savings]
end

#validate!Boolean

Validate collection can be built

Returns:

  • (Boolean)

    true if valid, raises error otherwise

Raises:

  • (Error)

    if validation fails



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/fontisan/collection/builder.rb', line 157

def validate!
  # Check minimum fonts
  raise Error, "Collection requires at least 2 fonts" if @fonts.size < 2

  # Check format compatibility
  incompatible = check_format_compatibility
  if incompatible.any?
    raise Error, "Format mismatch: #{incompatible.join(', ')}"
  end

  # Check all fonts have required tables
  @fonts.each_with_index do |font, index|
    required_tables = %w[head hhea maxp]
    missing = required_tables.reject { |tag| font.has_table?(tag) }
    unless missing.empty?
      raise Error,
            "Font #{index} missing required tables: #{missing.join(', ')}"
    end
  end

  true
end