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



89
90
91
92
93
94
95
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 89

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



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

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



112
113
114
115
116
117
118
119
120
121
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 112

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



99
100
101
102
103
104
105
106
107
108
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 99

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
85
86
87
# 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

  commands = outline.to_cff_commands
  master_commands = master_outlines.map(&:to_cff_commands)

  commands.each_with_index do |cmd, i|
    master_cmds = master_commands.map { |mc| mc[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



180
181
182
183
184
185
186
187
188
189
190
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 180

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



164
165
166
167
168
169
170
171
172
173
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 164

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



175
176
177
178
# File 'lib/fontisan/tables/cff/cff2_charstring_builder.rb', line 175

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