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)

Defined Under Namespace

Classes: TupleVariationHeader

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



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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/fontisan/tables/gvar.rb', line 177

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



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/fontisan/tables/gvar.rb', line 156

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



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/fontisan/tables/gvar.rb', line 128

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



87
88
89
# File 'lib/fontisan/tables/gvar.rb', line 87

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

#shared_point_numbers?Boolean

Check if using shared point numbers

Returns:

  • (Boolean)

    True if shared points



94
95
96
# File 'lib/fontisan/tables/gvar.rb', line 94

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

#shared_tuplesArray<Array<Integer>>

Parse shared tuples

Returns:

  • (Array<Array<Integer>>)

    Shared peak tuples



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/fontisan/tables/gvar.rb', line 101

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



265
266
267
# File 'lib/fontisan/tables/gvar.rb', line 265

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

#versionFloat

Get version as a float

Returns:

  • (Float)

    Version number (e.g., 1.0)



80
81
82
# File 'lib/fontisan/tables/gvar.rb', line 80

def version
  major_version + (minor_version / 10.0)
end