Class: Fontisan::Variation::BlendApplier

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/variation/blend_applier.rb

Overview

Applies CFF2 blend operators during CharString execution

The blend operator in CFF2 CharStrings provides variation support by blending base values with deltas based on design space coordinates.

Blend Format:

v1 Δv1_axis1 Δv1_axis2 ... v2 Δv2_axis1 ... K N blend

Where:

  • K = number of values to blend
  • N = number of axes
  • Each value has N deltas (one per axis)

The applier calculates blended values:

result = base + Σ(delta_i × scalar_i)

Reference: Adobe Technical Note #5177 (CFF2 specification)

Examples:

Applying blend operators

applier = Fontisan::Variation::BlendApplier.new(interpolator)
blended = applier.apply_blend(base: 100, deltas: [10, 5], scalars: [0.8, 0.5])
# => 110.5 (100 + 10*0.8 + 5*0.5)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(interpolator, coordinates = {}) ⇒ BlendApplier

Initialize blend applier

Parameters:

  • interpolator (Interpolator)

    Coordinate interpolator

  • coordinates (Hash<String, Float>) (defaults to: {})

    Design space coordinates



38
39
40
41
42
# File 'lib/fontisan/variation/blend_applier.rb', line 38

def initialize(interpolator, coordinates = {})
  @interpolator = interpolator
  @coordinates = coordinates
  @scalars = []
end

Instance Attribute Details

#interpolatorInterpolator (readonly)

Returns Coordinate interpolator.

Returns:



29
30
31
# File 'lib/fontisan/variation/blend_applier.rb', line 29

def interpolator
  @interpolator
end

#scalarsArray<Float> (readonly)

Returns Current variation scalars.

Returns:

  • (Array<Float>)

    Current variation scalars



32
33
34
# File 'lib/fontisan/variation/blend_applier.rb', line 32

def scalars
  @scalars
end

Instance Method Details

#apply_blend(base:, deltas:, num_axes: nil) ⇒ Float

Apply blend operation

Blends base value with deltas using variation scalars.

Parameters:

  • base (Numeric)

    Base value

  • deltas (Array<Numeric>)

    Delta values (one per axis)

  • num_axes (Integer) (defaults to: nil)

    Number of axes (for validation)

Returns:

  • (Float)

    Blended value

Raises:



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/fontisan/variation/blend_applier.rb', line 64

def apply_blend(base:, deltas:, num_axes: nil)
  # Validate delta count matches axes
  if num_axes && deltas.length != num_axes
    raise InvalidVariationDataError.new(
      message: "Blend delta count (#{deltas.length}) doesn't match axes (#{num_axes})",
      details: {
        delta_count: deltas.length,
        expected_axes: num_axes,
        base_value: base,
      },
    )
  end

  # Start with base value
  result = base.to_f

  # Apply each delta with its scalar
  deltas.each_with_index do |delta, index|
    scalar = @scalars[index] || 0.0
    result += delta.to_f * scalar
  end

  result
end

#apply_blend_operands(operands, num_values, num_axes) ⇒ Array<Float>

Apply blend operator from CharString stack

Processes blend operator arguments from CharString execution.

Parameters:

  • operands (Array<Numeric>)

    Blend operands from stack

  • num_values (Integer)

    K (number of values to blend)

  • num_axes (Integer)

    N (number of axes)

Returns:

  • (Array<Float>)

    Blended values

Raises:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/fontisan/variation/blend_applier.rb', line 115

def apply_blend_operands(operands, num_values, num_axes)
  # Expected operands: K * (N + 1)
  expected_count = num_values * (num_axes + 1)

  if operands.length != expected_count
    raise InvalidVariationDataError.new(
      message: "Blend operand count mismatch: expected #{expected_count}, got #{operands.length}",
      details: {
        operand_count: operands.length,
        expected_count: expected_count,
        num_values: num_values,
        num_axes: num_axes,
      },
    )
  end

  blended_values = []

  num_values.times do |i|
    offset = i * (num_axes + 1)
    base = operands[offset]
    deltas = operands[offset + 1, num_axes] || []

    blended_values << apply_blend(
      base: base,
      deltas: deltas,
      num_axes: num_axes,
    )
  end

  blended_values
end

#apply_blends(blends, num_axes) ⇒ Array<Float>

Apply multiple blend operations

Processes multiple values with their deltas.

Parameters:

  • blends (Array<Hash>)

    Array of { base:, deltas: } hashes

  • num_axes (Integer)

    Number of axes

Returns:

  • (Array<Float>)

    Blended values



96
97
98
99
100
101
102
103
104
# File 'lib/fontisan/variation/blend_applier.rb', line 96

def apply_blends(blends, num_axes)
  blends.map do |blend|
    apply_blend(
      base: blend[:base],
      deltas: blend[:deltas],
      num_axes: num_axes,
    )
  end
end

#at_default?Boolean

Check if coordinates are at default

Returns:

  • (Boolean)

    True if all scalars are zero



164
165
166
# File 'lib/fontisan/variation/blend_applier.rb', line 164

def at_default?
  @scalars.all?(&:zero?)
end

#blend_point(base_x, base_y, deltas_x, deltas_y) ⇒ Array<Float>

Get blended point coordinates

Applies blend to X and Y coordinates simultaneously.

Parameters:

  • base_x (Numeric)

    Base X coordinate

  • base_y (Numeric)

    Base Y coordinate

  • deltas_x (Array<Numeric>)

    X deltas

  • deltas_y (Array<Numeric>)

    Y deltas

Returns:

  • (Array<Float>)

    [blended_x, blended_y]



177
178
179
180
181
182
# File 'lib/fontisan/variation/blend_applier.rb', line 177

def blend_point(base_x, base_y, deltas_x, deltas_y)
  [
    apply_blend(base: base_x, deltas: deltas_x),
    apply_blend(base: base_y, deltas: deltas_y),
  ]
end

#blend_to_static(blend_data) ⇒ Array<Float>

Convert blend data to static values

For instance generation, replaces blend operators with static values.

Parameters:

  • blend_data (Array<Hash>)

    Blend operations data

Returns:

  • (Array<Float>)

    Static blended values



190
191
192
193
194
# File 'lib/fontisan/variation/blend_applier.rb', line 190

def blend_to_static(blend_data)
  blend_data.flat_map do |blend_op|
    apply_blends(blend_op[:blends], blend_op[:num_axes])
  end
end

#calculate_scalars(axes) ⇒ Array<Float>

Calculate scalars for current coordinates

Converts design space coordinates to normalized scalars [-1, 1].

Parameters:

  • axes (Array)

    Variation axes

Returns:

  • (Array<Float>)

    Scalar for each axis



154
155
156
157
158
159
# File 'lib/fontisan/variation/blend_applier.rb', line 154

def calculate_scalars(axes)
  axes.map do |axis|
    coord = @coordinates[axis.axis_tag] || axis.default_value
    @interpolator.normalize_coordinate(coord, axis.axis_tag)
  end
end

#set_coordinates(coordinates, axes) ⇒ Object

Set design space coordinates

Updates the variation scalars based on new coordinates.

Parameters:

  • coordinates (Hash<String, Float>)

    Axis tag => value

  • axes (Array)

    Variation axes from fvar



50
51
52
53
# File 'lib/fontisan/variation/blend_applier.rb', line 50

def set_coordinates(coordinates, axes)
  @coordinates = coordinates
  @scalars = calculate_scalars(axes)
end