Class: Fontisan::Tables::Cff::Cff2CharStringBuilder

Inherits:
CharStringBuilder show all
Defined in:
lib/fontisan/tables/cff/cff2_charstring_builder.rb

Overview

CFF2 CharStringBuilder — extends the CFF1 charstring with variable-font operators (vsindex, blend).

In CFF2, the hmoveto operator (22) is repurposed as vsindex (selects a VariationStore item), and a new blend operator (23) applies variation deltas to the current operands.

The blend protocol for a single coordinate:

push base_value, push delta_r0, ..., push delta_r(n-1), push 1, blend

After blend, the stack has one blended value.

For n coordinates batched together:

push base_0 + deltas_0, push base_1 + deltas_1, ..., push n, blend

After blend, the stack has n blended values.

Defined Under Namespace

Classes: MasterState

Constant Summary collapse

OPERATORS_CFF2 =

CFF2 replaces hmoveto(22) with vsindex(22). The :hmoveto key is omitted entirely so attempts to emit it raise naturally.

OPERATORS.except(:hmoveto).merge(
  vsindex: 22,
  blend: 23,
)

Constants inherited from CharStringBuilder

Fontisan::Tables::Cff::CharStringBuilder::OPERATORS, Fontisan::Tables::Cff::CharStringBuilder::TWO_BYTE_OPERATORS

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from CharStringBuilder

#build, #build_empty, build_from_operations, #build_from_operations

Class Method Details

.build_variable(outline, master_outlines:, num_regions:, width: nil) ⇒ String

Build a complete variable CFF2 charstring from a default outline plus variation master outlines.

Parameters:

  • outline (Models::Outline)

    default-master outline

  • master_outlines (Array<Models::Outline>)

    one per region

  • num_regions (Integer)

    number of variation regions

  • width (Integer, nil) (defaults to: nil)

    advance width

Returns:

  • (String)

    CFF2 charstring bytes with blend sequences



43
44
45
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 43

def self.build_variable(outline, master_outlines:, num_regions:, width: nil)
  new.build_variable_outline(outline, master_outlines, num_regions, width)
end

Instance Method Details

#build_variable_outline(outline, master_outlines, num_regions, width) ⇒ String

Build a variable charstring from a default outline + masters. Creates the output buffer internally (same pattern as the parent's build method).

Returns:

  • (String)

    charstring bytes with blend sequences



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 53

def build_variable_outline(outline, master_outlines, num_regions, width)
  @output = StringIO.new("".b)
  @first_move = true
  @current_x = 0.0
  @current_y = 0.0

  encode_width(width) if width
  encode_variable_outline(outline, master_outlines, num_regions)
  write_operator(:endchar)

  @output.string
end

#encode_variable_command(cmd, master_cmds) ⇒ Object



82
83
84
85
86
87
88
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 82

def encode_variable_command(cmd, master_cmds)
  case cmd[:type]
  when :move_to then encode_variable_moveto(cmd, master_cmds)
  when :line_to then encode_variable_lineto(cmd, master_cmds)
  when :curve_to then encode_variable_curveto(cmd, master_cmds)
  end
end

#encode_variable_curveto(cmd, master_cmds) ⇒ Object

---------- variable curveto ----------



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
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 116

def encode_variable_curveto(cmd, master_cmds)
  # Compute relative values for the default master
  dx1 = (cmd[:x1] - @current_x).round
  dy1 = (cmd[:y1] - @current_y).round
  dx2 = (cmd[:x2] - @current_x).round
  dy2 = (cmd[:y2] - @current_y).round
  dx = (cmd[:x] - @current_x).round
  dy = (cmd[:y] - @current_y).round

  base_values = [dx1, dy1, dx2, dy2, dx, dy]
  base_keys = %i[x1 y1 x2 y2 x y]

  # Compute deltas for each coordinate against each master
  all_deltas = base_keys.each_with_index.map do |key, vi|
    master_cmds.each_with_index.map do |mc, i|
      ms = @master_states[i]
      master_relative = if key.to_s.start_with?("x")
                          (mc[key] - ms.current_x).round
                        else
                          (mc[key] - ms.current_y).round
                        end
      master_relative - base_values[vi]
    end
  end

  base_values.each_with_index do |val, vi|
    write_variable_number(val, all_deltas[vi])
  end
  write_operator(:rrcurveto)

  advance_state(cmd, master_cmds)
end

#encode_variable_lineto(cmd, master_cmds) ⇒ Object

---------- variable lineto ----------



104
105
106
107
108
109
110
111
112
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 104

def encode_variable_lineto(cmd, master_cmds)
  dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd, master_cmds)

  write_variable_number(dx, dx_deltas)
  write_variable_number(dy, dy_deltas)
  write_operator(:rlineto)

  advance_state(cmd, master_cmds)
end

#encode_variable_moveto(cmd, master_cmds) ⇒ Object

---------- variable moveto ----------



92
93
94
95
96
97
98
99
100
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 92

def encode_variable_moveto(cmd, master_cmds)
  dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd, master_cmds)

  write_variable_number(dx, dx_deltas)
  write_variable_number(dy, dy_deltas)
  write_operator(:rmoveto)

  advance_state(cmd, master_cmds)
end

#encode_variable_outline(outline, master_outlines, num_regions) ⇒ Object

Process an outline with blend operators for each varying coordinate.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 67

def encode_variable_outline(outline, master_outlines, num_regions)
  @master_states = master_outlines.map { MasterState.new(current_x: 0, current_y: 0) }
  @num_regions = num_regions
  @has_blend = false

  outline.commands.each_with_index do |cmd, i|
    master_cmds = master_outlines.map { |mo| mo.commands[i] }
    encode_variable_command(cmd, master_cmds)
  end

  # Emit vsindex(0) at the start if any blend operators were used.
  # In a full implementation, vsindex selects the ItemVariationData
  # subtable; for single-subtable fonts, vsindex 0 is the default.
end

#write_operator(operator) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 171

def write_operator(operator)
  if OPERATORS_CFF2.key?(operator)
    @output.putc(OPERATORS_CFF2[operator])
  elsif TWO_BYTE_OPERATORS.key?(operator)
    bytes = TWO_BYTE_OPERATORS[operator]
    @output.putc(bytes[0])
    @output.putc(bytes[1])
  else
    raise ArgumentError, "Unknown CFF2 operator: #{operator}"
  end
end

#write_variable_number(base_value, deltas) ⇒ Object

Emit a variable coordinate: base value + per-region deltas + blend. If all deltas are zero, emits just the base value (no blend needed).

Parameters:

  • base_value (Integer)

    the default-master coordinate

  • deltas (Array<Integer>)

    one delta per variation region



155
156
157
158
159
160
161
162
163
164
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 155

def write_variable_number(base_value, deltas)
  return write_number(base_value) if deltas.nil? || deltas.empty?
  return write_number(base_value) if deltas.all?(&:zero?)

  @has_blend = true
  write_number(base_value)
  deltas.each { |d| write_number(d) }
  write_number(1) # n = 1 value to blend
  write_operator(:blend)
end

#write_vsindex(index) ⇒ Object



166
167
168
169
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 166

def write_vsindex(index)
  write_number(index)
  write_operator(:vsindex)
end