Class: Fontisan::Tables::Gvar

Inherits:
Binary::BaseRecord show all
Defined in:
lib/fontisan/tables/gvar.rb

Overview

Parser for the ‘gvar’ (Glyph Variations) table

The gvar table provides variation data for glyph outlines in TrueType variable fonts. It contains delta values for each glyph’s control points that are applied based on the current design space coordinates.

Unlike HVAR/VVAR/MVAR which use ItemVariationStore, gvar uses a TupleVariationStore structure with packed delta values.

Reference: OpenType specification, gvar table

Examples:

Reading a gvar table

data = font.table_data("gvar")
gvar = Fontisan::Tables::Gvar.read(data)
deltas = gvar.glyph_variations(glyph_id)

Constant Summary collapse

SHARED_POINT_NUMBERS =

Flags

0x8000
LONG_OFFSETS =
0x0001

Instance Method Summary collapse

Methods inherited from Binary::BaseRecord

#raw_data, read

Instance Method Details

#glyph_tuple_variations(glyph_id) ⇒ Array<Hash>?

Parse tuple variation headers for a glyph

Parameters:

  • glyph_id (Integer)

    Glyph ID

Returns:

  • (Array<Hash>, nil)

    Array of tuple info or nil



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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/fontisan/tables/gvar.rb', line 138

def glyph_tuple_variations(glyph_id)
  var_data = glyph_variation_data(glyph_id)
  return nil if var_data.nil? || var_data.empty?

  io = StringIO.new(var_data)
  io.set_encoding(Encoding::BINARY)

  # Read header
  tuple_count_and_offset = io.read(4).unpack1("N")
  tuple_count = tuple_count_and_offset >> 16
  data_offset = tuple_count_and_offset & 0xFFFF

  # Check for shared point numbers
  has_shared_points = (tuple_count & 0x8000) != 0
  tuple_count &= 0x0FFF

  return [] if tuple_count.zero?

  # Parse each tuple
  tuples = []
  tuple_count.times do
    header_data = io.read(4)
    break if header_data.nil? || header_data.bytesize < 4

    header = Variation::TupleVariationHeader.read(header_data)

    tuple_info = {
      data_size: header.variation_data_size,
      embedded_peak: header.embedded_peak_tuple?,
      intermediate: header.intermediate_region?,
      private_points: header.private_point_numbers?,
      shared_index: header.shared_tuple_index,
    }

    # Read peak tuple if embedded
    if header.embedded_peak_tuple?
      peak = Array.new(axis_count) do
        coord_data = io.read(2)
        break nil if coord_data.nil?

        value = coord_data.unpack1("n")
        signed = value > 0x7FFF ? value - 0x10000 : value
        signed / 16384.0
      end
      tuple_info[:peak] = peak.compact
    end

    # Read intermediate region if present
    if header.intermediate_region?
      start_tuple = Array.new(axis_count) do
        coord_data = io.read(2)
        break nil if coord_data.nil?

        value = coord_data.unpack1("n")
        signed = value > 0x7FFF ? value - 0x10000 : value
        signed / 16384.0
      end

      end_tuple = Array.new(axis_count) do
        coord_data = io.read(2)
        break nil if coord_data.nil?

        value = coord_data.unpack1("n")
        signed = value > 0x7FFF ? value - 0x10000 : value
        signed / 16384.0
      end

      tuple_info[:start] = start_tuple.compact
      tuple_info[:end] = end_tuple.compact
    end

    tuples << tuple_info
  end

  {
    tuple_count: tuple_count,
    has_shared_points: has_shared_points,
    data_offset: data_offset,
    tuples: tuples,
  }
rescue StandardError => e
  warn "Failed to parse glyph tuple variations: #{e.message}"
  nil
end

#glyph_variation_data(glyph_id) ⇒ String?

Get variation data for a specific glyph

Parameters:

  • glyph_id (Integer)

    Glyph ID

Returns:

  • (String, nil)

    Raw variation data or nil



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/fontisan/tables/gvar.rb', line 117

def glyph_variation_data(glyph_id)
  return nil if glyph_id >= glyph_count

  offsets = glyph_variation_data_offsets
  return nil if glyph_id >= offsets.length - 1

  start_offset = offsets[glyph_id]
  end_offset = offsets[glyph_id + 1]

  return nil if start_offset == end_offset # No data

  data = raw_data
  return nil if end_offset > data.bytesize

  data.byteslice(start_offset, end_offset - start_offset)
end

#glyph_variation_data_offsetsArray<Integer>

Parse glyph variation data offsets

Returns:

  • (Array<Integer>)

    Array of offsets



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/fontisan/tables/gvar.rb', line 89

def glyph_variation_data_offsets
  return @glyph_offsets if @glyph_offsets

  data = raw_data
  # Offsets start after the header (20 bytes)
  offset = 20

  offset_size = long_offsets? ? 4 : 2
  offset_count = glyph_count + 1 # One extra for the end

  @glyph_offsets = Array.new(offset_count) do |i|
    offset_pos = offset + (i * offset_size)
    next nil if offset_pos + offset_size > data.bytesize

    raw_offset = if long_offsets?
                   data.byteslice(offset_pos, 4).unpack1("N")
                 else
                   data.byteslice(offset_pos, 2).unpack1("n") * 2
                 end

    glyph_variation_data_array_offset + raw_offset
  end.compact
end

#long_offsets?Boolean

Check if using long offsets

Returns:

  • (Boolean)

    True if long offsets



48
49
50
# File 'lib/fontisan/tables/gvar.rb', line 48

def long_offsets?
  (flags & LONG_OFFSETS) != 0
end

#shared_point_numbers?Boolean

Check if using shared point numbers

Returns:

  • (Boolean)

    True if shared points



55
56
57
# File 'lib/fontisan/tables/gvar.rb', line 55

def shared_point_numbers?
  (flags & SHARED_POINT_NUMBERS) != 0
end

#shared_tuplesArray<Array<Integer>>

Parse shared tuples

Returns:

  • (Array<Array<Integer>>)

    Shared peak tuples



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/fontisan/tables/gvar.rb', line 62

def shared_tuples
  return @shared_tuples if @shared_tuples
  return @shared_tuples = [] if shared_tuple_count.zero?

  data = raw_data
  offset = shared_tuples_offset

  return @shared_tuples = [] if offset >= data.bytesize

  @shared_tuples = Array.new(shared_tuple_count) do |i|
    tuple_offset = offset + (i * axis_count * 2)

    Array.new(axis_count) do |j|
      coord_offset = tuple_offset + (j * 2)
      next nil if coord_offset + 2 > data.bytesize

      # F2DOT14 format
      value = data.byteslice(coord_offset, 2).unpack1("n")
      signed = value > 0x7FFF ? value - 0x10000 : value
      signed / 16384.0
    end.compact
  end.compact
end

#valid?Boolean

Check if table is valid

Returns:

  • (Boolean)

    True if valid



226
227
228
# File 'lib/fontisan/tables/gvar.rb', line 226

def valid?
  major_version == 1 && minor_version.zero?
end

#versionFloat

Get version as a float

Returns:

  • (Float)

    Version number (e.g., 1.0)



41
42
43
# File 'lib/fontisan/tables/gvar.rb', line 41

def version
  major_version + (minor_version / 10.0)
end