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
46
47
# 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



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

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



86
87
88
89
90
91
92
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 86

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 ----------



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

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 ----------



109
110
111
112
113
114
115
116
117
118
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 109

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 ----------



96
97
98
99
100
101
102
103
104
105
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 96

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.



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

def encode_variable_outline(outline, master_outlines, num_regions)
  @master_states = master_outlines.map do
    MasterState.new(current_x: 0, current_y: 0)
  end
  @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



177
178
179
180
181
182
183
184
185
186
187
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 177

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



161
162
163
164
165
166
167
168
169
170
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 161

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



172
173
174
175
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 172

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