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



40
41
42
43
44
# File 'lib/fontisan/variation/blend_applier.rb', line 40

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

Instance Attribute Details

#interpolatorInterpolator (readonly)

Returns Coordinate interpolator.

Returns:



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

def interpolator
  @interpolator
end

#scalarsArray<Float> (readonly)

Returns Current variation scalars.

Returns:

  • (Array<Float>)

    Current variation scalars



34
35
36
# File 'lib/fontisan/variation/blend_applier.rb', line 34

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:



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

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:



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
147
148
# File 'lib/fontisan/variation/blend_applier.rb', line 117

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



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

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



166
167
168
# File 'lib/fontisan/variation/blend_applier.rb', line 166

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


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

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



192
193
194
195
196
# File 'lib/fontisan/variation/blend_applier.rb', line 192

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



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

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



52
53
54
55
# File 'lib/fontisan/variation/blend_applier.rb', line 52

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