Class: Fontisan::Variation::Converter

Inherits:
Object
  • Object
show all
Includes:
TableAccessor
Defined in:
lib/fontisan/variation/converter.rb

Overview

Converts variation data between TrueType (gvar) and CFF2 (blend) formats

This class enables format conversion while preserving variation data:

  • gvar tuples → CFF2 blend operators
  • CFF2 blend operators → gvar tuples

Process for gvar → blend:

  1. Extract tuple variations from gvar
  2. Map tuple regions to blend regions
  3. Embed blend operators in CharStrings at control points
  4. Encode delta values in blend format

Process for blend → gvar:

  1. Parse CharStrings with blend operators
  2. Extract blend deltas and regions
  3. Map to gvar tuple format
  4. Build gvar table structure

Examples:

Converting gvar to CFF2 blend

converter = Converter.new(font, axes)
blend_data = converter.gvar_to_blend(glyph_id)

Converting CFF2 blend to gvar

converter = Converter.new(font, axes)
tuple_data = converter.blend_to_gvar(glyph_id)

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from TableAccessor

#clear_variation_cache, #clear_variation_table, #has_variation_table?, #require_variation_table, #variation_table

Constructor Details

#initialize(font, axes) ⇒ Converter

Initialize converter

Parameters:



43
44
45
46
47
# File 'lib/fontisan/variation/converter.rb', line 43

def initialize(font, axes)
  @font = font
  @axes = axes || []
  @variation_tables = {}
end

Instance Attribute Details

#axesArray<VariationAxisRecord> (readonly)

Returns Variation axes.

Returns:

  • (Array<VariationAxisRecord>)

    Variation axes



37
38
39
# File 'lib/fontisan/variation/converter.rb', line 37

def axes
  @axes
end

#fontTrueTypeFont, OpenTypeFont (readonly)

Returns Font instance.

Returns:



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

def font
  @font
end

Instance Method Details

#blend_to_gvar(glyph_id) ⇒ Hash?

Convert CFF2 blend operators to gvar tuple format for a glyph

Parameters:

  • glyph_id (Integer)

    Glyph ID

Returns:

  • (Hash, nil)

    Tuple data or nil



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/fontisan/variation/converter.rb', line 72

def blend_to_gvar(glyph_id)
  return nil unless has_variation_table?("CFF2")

  cff2 = variation_table("CFF2")
  return nil unless cff2

  # Get CharString with blend operators
  charstring = cff2.charstring_for_glyph(glyph_id)
  return nil unless charstring

  # Parse CharString to extract blend data
  charstring.parse
  blend_data = charstring.blend_data
  return nil if blend_data.nil? || blend_data.empty?

  # Convert blend data to tuple format
  convert_blend_to_tuples_for_glyph(blend_data)
end

#build_region_from_tuple(tuple) ⇒ Hash

Build region from tuple peak/start/end coordinates

Parameters:

  • tuple (Hash)

    Tuple data with :peak, :start, :end

Returns:

  • (Hash)

    Region definition



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/fontisan/variation/converter.rb', line 226

def build_region_from_tuple(tuple)
  region = {}

  @axes.each_with_index do |axis, axis_index|
    # Extract coordinates for this axis
    peak = tuple[:peak] ? tuple[:peak][axis_index] : 0.0
    start_val = tuple[:start] ? tuple[:start][axis_index] : -1.0
    end_val = tuple[:end] ? tuple[:end][axis_index] : 1.0

    region[axis.axis_tag] = {
      start: start_val,
      peak: peak,
      end: end_val,
    }
  end

  region
end

#build_tuple_from_region(region, point_deltas, region_index) ⇒ Hash

Build tuple from region and deltas

Parameters:

  • region (Hash)

    Region definition

  • point_deltas (Array<Array<Hash>>)

    Deltas per point

  • region_index (Integer)

    Region index

Returns:

  • (Hash)

    Tuple data



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/fontisan/variation/converter.rb', line 314

def build_tuple_from_region(region, point_deltas, region_index)
  # Extract peak, start, end for all axes
  peak = Array.new(@axes.length, 0.0)
  start_vals = Array.new(@axes.length, -1.0)
  end_vals = Array.new(@axes.length, 1.0)

  @axes.each_with_index do |axis, axis_index|
    axis_region = region[axis.axis_tag]
    next unless axis_region

    peak[axis_index] = axis_region[:peak]
    start_vals[axis_index] = axis_region[:start]
    end_vals[axis_index] = axis_region[:end]
  end

  # Extract deltas for this region
  deltas = point_deltas.map do |point_delta_set|
    point_delta_set[region_index] || { x: 0, y: 0 }
  end

  {
    peak: peak,
    start: start_vals,
    end: end_vals,
    deltas: deltas,
  }
end

#can_convert?Boolean

Check if variation data can be converted

Returns:

  • (Boolean)

    True if conversion possible



120
121
122
123
124
125
# File 'lib/fontisan/variation/converter.rb', line 120

def can_convert?
  !@axes.empty? && (
    has_variation_table?("gvar") ||
    has_variation_table?("CFF2")
  )
end

#convert_all_blend_to_gvar(glyph_count) ⇒ Hash<Integer, Hash>

Convert all glyphs from blend to gvar format

Parameters:

  • glyph_count (Integer)

    Number of glyphs

Returns:

  • (Hash<Integer, Hash>)

    Map of glyph_id to tuple data



108
109
110
111
112
113
114
115
# File 'lib/fontisan/variation/converter.rb', line 108

def convert_all_blend_to_gvar(glyph_count)
  return {} unless can_convert?

  (0...glyph_count).each_with_object({}) do |glyph_id, result|
    tuple_data = blend_to_gvar(glyph_id)
    result[glyph_id] = tuple_data if tuple_data
  end
end

#convert_all_gvar_to_blend(glyph_count) ⇒ Hash<Integer, Hash>

Convert all glyphs from gvar to blend format

Parameters:

  • glyph_count (Integer)

    Number of glyphs

Returns:

  • (Hash<Integer, Hash>)

    Map of glyph_id to blend data



95
96
97
98
99
100
101
102
# File 'lib/fontisan/variation/converter.rb', line 95

def convert_all_gvar_to_blend(glyph_count)
  return {} unless can_convert?

  (0...glyph_count).each_with_object({}) do |glyph_id, result|
    blend_data = gvar_to_blend(glyph_id)
    result[glyph_id] = blend_data if blend_data
  end
end

#convert_blend_to_tuples(blend_data) ⇒ Hash

Convert blend data to tuple format

Parameters:

  • blend_data (Hash)

    Blend format data

Returns:

  • (Hash)

    Tuple variation data



293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/fontisan/variation/converter.rb', line 293

def convert_blend_to_tuples(blend_data)
  regions = blend_data[:regions] || []
  point_deltas = blend_data[:point_deltas] || []

  # Build tuples from regions
  tuples = regions.map.with_index do |region, region_index|
    build_tuple_from_region(region, point_deltas, region_index)
  end

  {
    tuples: tuples,
    point_count: point_deltas.length,
  }
end

#convert_blend_to_tuples_for_glyph(blend_data) ⇒ Hash

Convert blend data from a glyph to tuple format

Parameters:

  • blend_data (Array<Hash>)

    Array of blend operations

Returns:

  • (Hash)

    Tuple variation data



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
158
159
160
161
162
163
164
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
# File 'lib/fontisan/variation/converter.rb', line 133

def convert_blend_to_tuples_for_glyph(blend_data)
  # Each blend operation represents variation at different points
  # We need to aggregate these into region-based tuples

  # Extract all regions from blend operations
  regions_map = {}
  point_count = 0

  blend_data.each_with_index do |blend_op, idx|
    blend_op[:blends].each do |blend|
      # Track the maximum point index we've seen
      point_count = [point_count, idx + 1].max

      # For each delta axis, we need to create or update a region
      blend[:deltas].each_with_index do |delta, axis_index|
        next if delta.zero? # Skip zero deltas

        # Create region key based on unique delta pattern
        region_key = "region_#{axis_index}"

        regions_map[region_key] ||= {
          axis_index: axis_index,
          deltas_per_point: Array.new(point_count) { { x: 0, y: 0 } },
        }

        # Store this delta for this point
        # Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
        # This is a simplified mapping - full implementation would track
        # which coordinates are being varied
        regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
                                                                  y: 0 }
        if idx.even?
          regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
        else
          regions_map[region_key][:deltas_per_point][idx / 2][:y] = delta
        end
      end
    end
  end

  # Convert regions to tuples
  tuples = []
  regions_map.each_value do |region_data|
    axis_index = region_data[:axis_index]

    # Build peak coordinates (one per axis)
    peak = Array.new(@axes.length, 0.0)
    peak[axis_index] = 1.0 if axis_index < @axes.length

    # Build start/end (default full range)
    start_vals = Array.new(@axes.length, -1.0)
    end_vals = Array.new(@axes.length, 1.0)

    tuples << {
      peak: peak,
      start: start_vals,
      end: end_vals,
      deltas: region_data[:deltas_per_point],
    }
  end

  {
    tuples: tuples,
    point_count: point_count,
  }
end

#convert_tuples_to_blend(tuple_data) ⇒ Hash

Convert tuple variations to blend format

Parameters:

  • tuple_data (Hash)

    Tuple variation data from gvar

Returns:

  • (Hash)

    Blend format data



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/fontisan/variation/converter.rb', line 204

def convert_tuples_to_blend(tuple_data)
  tuples = tuple_data[:tuples] || []
  point_count = tuple_data[:point_count] || 0

  # Build blend regions from tuples
  regions = tuples.map { |tuple| build_region_from_tuple(tuple) }

  # Extract deltas for each point
  point_deltas = extract_point_deltas(tuples, point_count)

  {
    regions: regions,
    point_deltas: point_deltas,
    num_regions: regions.length,
    num_axes: @axes.length,
  }
end

#decode_blend_operator(args) ⇒ Hash

Decode blend operator arguments to base and deltas

Parameters:

  • args (Array<Numeric>)

    Blend operator arguments

Returns:

  • (Hash)

    Base value and deltas



357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/fontisan/variation/converter.rb', line 357

def decode_blend_operator(args)
  return { base: 0, deltas: [] } if args.length < 3

  # Last two values are K and N
  k = args[-2]
  _n = args[-1]

  # Before K and N: base + deltas
  values = args[0...-2]
  base = values[0] || 0
  deltas = values[1, k] || []

  { base: base, deltas: deltas }
end

#encode_blend_operator(base_value, deltas) ⇒ Array<Numeric>

Encode deltas in CharString blend format

Parameters:

  • base_value (Numeric)

    Base value

  • deltas (Array<Numeric>)

    Delta values

Returns:

  • (Array<Numeric>)

    Blend operator arguments



347
348
349
350
351
# File 'lib/fontisan/variation/converter.rb', line 347

def encode_blend_operator(base_value, deltas)
  # CFF2 blend format: base_value delta1 delta2 ... K N blend
  # Where K = number of deltas, N = number of blend operations
  [base_value] + deltas + [deltas.length, 1]
end

#extract_point_deltas(tuples, point_count) ⇒ Array<Array<Hash>>

Extract point deltas from all tuples

Parameters:

  • tuples (Array<Hash>)

    Tuple variations

  • point_count (Integer)

    Number of points

Returns:

  • (Array<Array<Hash>>)

    Deltas per point per tuple



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/fontisan/variation/converter.rb', line 250

def extract_point_deltas(tuples, point_count)
  return [] if point_count.zero?

  # Initialize deltas array
  point_deltas = Array.new(point_count) { [] }

  # For each tuple, extract deltas for all points
  tuples.each do |tuple|
    deltas = parse_tuple_deltas(tuple, point_count)

    deltas.each_with_index do |delta, point_index|
      point_deltas[point_index] << delta
    end
  end

  point_deltas
end

#gvar_to_blend(glyph_id) ⇒ Hash?

Convert gvar tuples to CFF2 blend format for a glyph

Parameters:

  • glyph_id (Integer)

    Glyph ID

Returns:

  • (Hash, nil)

    Blend data or nil



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/fontisan/variation/converter.rb', line 53

def gvar_to_blend(glyph_id)
  return nil unless has_variation_table?("gvar")
  return nil unless has_variation_table?("glyf")

  gvar = variation_table("gvar")
  return nil unless gvar

  # Get tuple variations for this glyph
  tuple_data = gvar.glyph_tuple_variations(glyph_id)
  return nil unless tuple_data

  # Convert tuples to blend format
  convert_tuples_to_blend(tuple_data)
end

#parse_tuple_deltas(tuple, point_count) ⇒ Array<Hash>

Parse deltas from a tuple

Parameters:

  • tuple (Hash)

    Tuple data

  • point_count (Integer)

    Number of points

Returns:

  • (Array<Hash>)

    Deltas with :x and :y



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/fontisan/variation/converter.rb', line 273

def parse_tuple_deltas(tuple, point_count)
  # If tuple has deltas array, use it
  if tuple[:deltas].is_a?(Array)
    return tuple[:deltas].map do |delta|
      { x: delta[:x] || 0, y: delta[:y] || 0 }
    end
  end

  # Otherwise return zeros (placeholder for parsing raw delta data)
  # Full implementation would:
  # 1. Parse delta data from tuple[:data]
  # 2. Decompress if needed
  # 3. Return array of { x: dx, y: dy } for each point
  Array.new(point_count) { { x: 0, y: 0 } }
end