Class: Fontisan::Variation::Converter
- Inherits:
-
Object
- Object
- Fontisan::Variation::Converter
- 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:
- Extract tuple variations from gvar
- Map tuple regions to blend regions
- Embed blend operators in CharStrings at control points
- Encode delta values in blend format
Process for blend → gvar:
- Parse CharStrings with blend operators
- Extract blend deltas and regions
- Map to gvar tuple format
- Build gvar table structure
Instance Attribute Summary collapse
-
#axes ⇒ Array<VariationAxisRecord>
readonly
Variation axes.
-
#font ⇒ TrueTypeFont, OpenTypeFont
readonly
Font instance.
Instance Method Summary collapse
-
#blend_to_gvar(glyph_id) ⇒ Hash?
Convert CFF2 blend operators to gvar tuple format for a glyph.
-
#build_region_from_tuple(tuple) ⇒ Hash
Build region from tuple peak/start/end coordinates.
-
#build_tuple_from_region(region, point_deltas, region_index) ⇒ Hash
Build tuple from region and deltas.
-
#can_convert? ⇒ Boolean
Check if variation data can be converted.
-
#convert_all_blend_to_gvar(glyph_count) ⇒ Hash<Integer, Hash>
Convert all glyphs from blend to gvar format.
-
#convert_all_gvar_to_blend(glyph_count) ⇒ Hash<Integer, Hash>
Convert all glyphs from gvar to blend format.
-
#convert_blend_to_tuples(blend_data) ⇒ Hash
Convert blend data to tuple format.
-
#convert_blend_to_tuples_for_glyph(blend_data) ⇒ Hash
Convert blend data from a glyph to tuple format.
-
#convert_tuples_to_blend(tuple_data) ⇒ Hash
Convert tuple variations to blend format.
-
#decode_blend_operator(args) ⇒ Hash
Decode blend operator arguments to base and deltas.
-
#encode_blend_operator(base_value, deltas) ⇒ Array<Numeric>
Encode deltas in CharString blend format.
-
#extract_point_deltas(tuples, point_count) ⇒ Array<Array<Hash>>
Extract point deltas from all tuples.
-
#gvar_to_blend(glyph_id) ⇒ Hash?
Convert gvar tuples to CFF2 blend format for a glyph.
-
#initialize(font, axes) ⇒ Converter
constructor
Initialize converter.
-
#parse_tuple_deltas(tuple, point_count) ⇒ Array<Hash>
Parse deltas from a tuple.
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
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
#axes ⇒ Array<VariationAxisRecord> (readonly)
Returns Variation axes.
37 38 39 |
# File 'lib/fontisan/variation/converter.rb', line 37 def axes @axes end |
#font ⇒ TrueTypeFont, OpenTypeFont (readonly)
Returns Font instance.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 |