Class: Fontisan::Tables::Cff2::OperandStack

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/tables/cff2/operand_stack.rb

Overview

Operand stack manager for CFF2 CharStrings

This class manages the operand stack for CFF2 CharStrings, with special handling for blend operations that mix base values and deltas.

In CFF2, the blend operator takes operands in the format:

[base1, delta1_axis1, delta1_axis2, ..., base2, delta2_axis1, ..., K, N]

Where:

- K = number of values to blend
- N = number of variation axes

The stack manager separates base values from deltas and applies blend operations to produce final values based on variation coordinates.

Examples:

Managing a blend operation

stack = OperandStack.new(num_axes: 2)
stack.push(100, 10, 5)  # base=100, deltas=[10, 5]
stack.push(200, 20, 10) # base=200, deltas=[20, 10]
blended = stack.apply_blend(k: 2, coordinates: { "wght" => 0.5, "wdth" => 0.3 })
# => [105.0, 206.0]  # base + (delta * scalar)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(num_axes: 0) ⇒ OperandStack

Initialize operand stack

Parameters:

  • num_axes (Integer) (defaults to: 0)

    Number of variation axes (default 0)



40
41
42
43
44
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 40

def initialize(num_axes: 0)
  @stack = []
  @num_axes = num_axes
  @blend_values = []
end

Instance Attribute Details

#blend_valuesArray<Hash> (readonly)

Returns Blend values (base + deltas).

Returns:

  • (Array<Hash>)

    Blend values (base + deltas)



35
36
37
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 35

def blend_values
  @blend_values
end

#num_axesInteger (readonly)

Returns Number of variation axes.

Returns:

  • (Integer)

    Number of variation axes



32
33
34
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 32

def num_axes
  @num_axes
end

#stackArray<Numeric> (readonly)

Returns The operand stack.

Returns:

  • (Array<Numeric>)

    The operand stack



29
30
31
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 29

def stack
  @stack
end

Instance Method Details

#apply_blend(scalars = []) ⇒ Array<Float>

Apply blend operation

This pops K * (N + 1) + 2 operands from the stack, where:

- K = number of values to blend
- N = number of axes
- Last 2 values are K and N themselves

Parameters:

  • scalars (Array<Float>) (defaults to: [])

    Variation scalars for each axis

Returns:

  • (Array<Float>)

    Blended values



113
114
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
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 113

def apply_blend(scalars = [])
  # Pop N and K
  n = pop.to_i
  k = pop.to_i

  # Validate
  required_operands = k * (n + 1)
  if size < required_operands
    warn "Blend requires #{required_operands} operands, got #{size}"
    clear
    return []
  end

  # Extract operands (base + deltas for each value)
  blend_operands = pop_many(required_operands).reverse

  # Process each value to blend
  blended_values = []
  k.times do |i|
    offset = i * (n + 1)
    base = blend_operands[offset]
    deltas = blend_operands[offset + 1, n] || []

    # Apply blend: result = base + sum(delta[i] * scalar[i])
    blended = base.to_f
    deltas.each_with_index do |delta, axis_index|
      scalar = scalars[axis_index] || 0.0
      blended += delta.to_f * scalar
    end

    # Store blend info for debugging/inspection
    @blend_values << {
      base: base,
      deltas: deltas,
      blended: blended,
    }

    blended_values << blended
  end

  # Push blended values back onto stack
  push(*blended_values)

  blended_values
end

#blend_historyArray<Hash>

Get blend value history

Returns:

  • (Array<Hash>)

    Blend values that have been calculated



221
222
223
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 221

def blend_history
  @blend_values.dup
end

#clearObject

Clear the stack



99
100
101
102
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 99

def clear
  @stack.clear
  @blend_values.clear
end

#empty?Boolean

Check if stack is empty

Returns:

  • (Boolean)

    True if empty



94
95
96
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 94

def empty?
  @stack.empty?
end

#extract_blend_dataHash

Extract blend data without applying

This is used when we need to store blend operations for later application with specific coordinates.

Returns:

  • (Hash)

    Blend operation data



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
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 165

def extract_blend_data
  # Pop N and K
  n = pop.to_i
  k = pop.to_i

  # Validate
  required_operands = k * (n + 1)
  if size < required_operands
    warn "Blend requires #{required_operands} operands, got #{size}"
    clear
    return nil
  end

  # Extract operands
  blend_operands = pop_many(required_operands).reverse

  # Parse into base + deltas structure
  blends = []
  k.times do |i|
    offset = i * (n + 1)
    base = blend_operands[offset]
    deltas = blend_operands[offset + 1, n] || []

    blends << {
      base: base,
      deltas: deltas,
    }

    # Push base value back (will be blended later)
    push(base)
  end

  {
    num_values: k,
    num_axes: n,
    blends: blends,
  }
end

#inspectString

Get string representation for debugging

Returns:

  • (String)

    Stack contents as string



214
215
216
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 214

def inspect
  "#<OperandStack size=#{size} values=#{@stack.inspect}>"
end

#peekNumeric?

Get the top value without popping

Returns:

  • (Numeric, nil)

    Top value or nil if empty



80
81
82
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 80

def peek
  @stack.last
end

#popNumeric?

Pop a value from the stack

Returns:

  • (Numeric, nil)

    Popped value or nil if empty



56
57
58
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 56

def pop
  @stack.pop
end

#pop_many(count) ⇒ Array<Numeric>

Pop multiple values from the stack

Parameters:

  • count (Integer)

    Number of values to pop

Returns:

  • (Array<Numeric>)

    Popped values



64
65
66
67
68
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 64

def pop_many(count)
  return [] if count <= 0 || @stack.empty?

  @stack.pop(count)
end

#push(*values) ⇒ Object

Push a value onto the stack

Parameters:

  • values (Numeric)

    Values to push



49
50
51
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 49

def push(*values)
  @stack.concat(values)
end

#reset_blend_historyObject

Reset blend history



226
227
228
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 226

def reset_blend_history
  @blend_values.clear
end

#shiftNumeric?

Shift a value from the front of the stack

Returns:

  • (Numeric, nil)

    Shifted value or nil if empty



73
74
75
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 73

def shift
  @stack.shift
end

#sizeInteger

Get stack size

Returns:

  • (Integer)

    Number of values on stack



87
88
89
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 87

def size
  @stack.size
end

#to_aArray<Numeric>

Get all values on the stack

Returns:

  • (Array<Numeric>)

    Stack contents



207
208
209
# File 'lib/fontisan/tables/cff2/operand_stack.rb', line 207

def to_a
  @stack.dup
end