Class: Fontisan::Tables::Glyf

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

Overview

Parser for the ‘glyf’ (Glyph Data) table

The glyf table contains TrueType glyph outline data. Each glyph is described by either a simple glyph (with contours and points) or a compound glyph (composed of other glyphs with transformations).

The glyf table is accessed via offsets from the loca table, which provides the byte offset and size for each glyph. Empty glyphs (e.g., space characters) have zero size in loca.

Glyph types are determined by numberOfContours:

  • numberOfContours >= 0: Simple glyph with that many contours

  • numberOfContours == -1: Compound glyph composed of other glyphs

The glyf table is context-dependent and requires:

  • loca table (for glyph offsets and sizes)

  • head table (for coordinate interpretation and flags)

Reference: OpenType specification, glyf table docs.microsoft.com/en-us/typography/opentype/spec/glyf

Examples:

Accessing a glyph

# Get required tables first
head = font.table('head')
loca = font.table('loca')
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)

# Parse glyf table
data = font.read_table_data('glyf')
glyf = Fontisan::Tables::Glyf.read(data)

# Get a specific glyph
glyph = glyf.glyph_for(42, loca, head)
puts glyph.simple? ? "Simple glyph" : "Compound glyph"
puts glyph.bounding_box  # => [xMin, yMin, xMax, yMax]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Binary::BaseRecord

#valid?

Instance Attribute Details

#glyphs_cacheHash (readonly)

Lazy initialization of glyphs cache

Returns:

  • (Hash)

    The glyphs cache



52
53
54
# File 'lib/fontisan/tables/glyf.rb', line 52

def glyphs_cache
  @glyphs_cache
end

#raw_dataObject

Store the raw data for deferred parsing



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

def raw_data
  @raw_data
end

Class Method Details

.read(io) ⇒ Glyf

Override read to capture raw data

Parameters:

  • io (IO, String)

    Input data

Returns:

  • (Glyf)

    Parsed table instance



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/fontisan/tables/glyf.rb', line 58

def self.read(io)
  instance = new

  # Initialize cache
  instance.instance_variable_set(:@glyphs_cache, {})

  # Handle nil or empty data gracefully
  instance.raw_data = if io.nil?
                        "".b
                      elsif io.is_a?(String)
                        io
                      else
                        io.read || "".b
                      end

  instance
end

Instance Method Details

#all_glyphs_accessible?(loca, head, num_glyphs) ⇒ Boolean

Validation helper: Check if all glyphs are accessible

Attempts to access each glyph to ensure no corruption

Parameters:

  • loca (Loca)

    Loca table for glyph access

  • head (Head)

    Head table for context

  • num_glyphs (Integer)

    Total number of glyphs

Returns:

  • (Boolean)

    True if all glyphs can be accessed



266
267
268
269
270
271
272
273
274
275
# File 'lib/fontisan/tables/glyf.rb', line 266

def all_glyphs_accessible?(loca, head, num_glyphs)
  (0...num_glyphs).all? do |glyph_id|
    glyph_for(glyph_id, loca, head)
    true
  rescue Fontisan::CorruptedTableError
    false
  end
rescue StandardError
  false
end

#cache_sizeInteger

Get the number of cached glyphs

Returns:

  • (Integer)

    Number of glyphs in cache



142
143
144
# File 'lib/fontisan/tables/glyf.rb', line 142

def cache_size
  glyphs_cache.size
end

#cached?(glyph_id) ⇒ Boolean

Check if a glyph is cached

Parameters:

  • glyph_id (Integer)

    Glyph ID to check

Returns:

  • (Boolean)

    True if glyph is cached



150
151
152
# File 'lib/fontisan/tables/glyf.rb', line 150

def cached?(glyph_id)
  glyphs_cache.key?(glyph_id)
end

#clear_cachevoid

This method returns an undefined value.

Clear the glyph cache to free memory

This is useful for long-running processes that parse many glyphs but don’t need to keep them all in memory.



135
136
137
# File 'lib/fontisan/tables/glyf.rb', line 135

def clear_cache
  glyphs_cache.clear
end

#glyph_for(glyph_id, loca, head) ⇒ SimpleGlyph, ...

Get glyph data for a specific glyph ID

This method retrieves and parses the glyph at the specified ID. It uses the loca table to determine the offset and size, then parses the glyph data to create either a SimpleGlyph or CompoundGlyph.

Results are cached to avoid re-parsing the same glyph multiple times.

Examples:

Getting a simple glyph

glyph = glyf.glyph_for(65, loca, head)  # 'A' character
if glyph.simple?
  puts "Contours: #{glyph.num_contours}"
  puts "Points: #{glyph.x_coordinates.length}"
end

Parameters:

  • glyph_id (Integer)

    Glyph ID (0-based, 0 is .notdef)

  • loca (Loca)

    Parsed loca table with offsets

  • head (Head)

    Parsed head table for coordinate interpretation

Returns:

Raises:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/fontisan/tables/glyf.rb', line 97

def glyph_for(glyph_id, loca, head)
  # Return cached glyph if available
  return glyphs_cache[glyph_id] if glyphs_cache.key?(glyph_id)

  # Validate inputs
  validate_context!(loca, head, glyph_id)

  # Get offset and size from loca table
  offset = loca.offset_for(glyph_id)
  size = loca.size_of(glyph_id)

  # Empty glyph (e.g., space character)
  if size.nil? || size.zero?
    glyphs_cache[glyph_id] = nil
    return nil
  end

  # Validate offset and size
  if offset + size > raw_data.length
    raise Fontisan::CorruptedTableError,
          "Glyph #{glyph_id} extends beyond glyf table: " \
          "offset=#{offset}, size=#{size}, table_size=#{raw_data.length}"
  end

  # Extract glyph data
  glyph_data = raw_data[offset, size]

  # Parse glyph
  glyph = parse_glyph_data(glyph_data, glyph_id)
  glyphs_cache[glyph_id] = glyph
end

#instructions_sound?(loca, head, num_glyphs) ⇒ Boolean

Validation helper: Check if TrueType instructions are sound

Validates that glyph instructions (if present) are parseable This is a basic check that ensures instructions exist and have valid length

Parameters:

  • loca (Loca)

    Loca table for glyph access

  • head (Head)

    Head table for context

  • num_glyphs (Integer)

    Total number of glyphs to check

Returns:

  • (Boolean)

    True if all instructions are valid or absent



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/fontisan/tables/glyf.rb', line 218

def instructions_sound?(loca, head, num_glyphs)
  (0...num_glyphs).all? do |glyph_id|
    glyph = glyph_for(glyph_id, loca, head)
    next true if glyph.nil? # Empty glyphs are OK

    # Simple glyphs have instructions
    if glyph.respond_to?(:instruction_length)
      inst_len = glyph.instruction_length
      # If instructions present, length should be reasonable
      inst_len.nil? || inst_len >= 0
    else
      # Compound glyphs may have instructions too
      true
    end
  end
rescue StandardError
  false
end

#no_clipped_glyphs?(loca, head, num_glyphs) ⇒ Boolean

Validation helper: Check if any glyphs are clipped (exceed bounds)

Validates that glyph coordinates don’t exceed head table’s bounding box

Parameters:

  • loca (Loca)

    Loca table for glyph access

  • head (Head)

    Head table for bounds reference

  • num_glyphs (Integer)

    Total number of glyphs to check

Returns:

  • (Boolean)

    True if no glyphs exceed the font’s bounding box



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/fontisan/tables/glyf.rb', line 189

def no_clipped_glyphs?(loca, head, num_glyphs)
  font_x_min = head.x_min
  font_y_min = head.y_min
  font_x_max = head.x_max
  font_y_max = head.y_max

  (0...num_glyphs).all? do |glyph_id|
    glyph = glyph_for(glyph_id, loca, head)
    next true if glyph.nil? # Empty glyphs are OK

    # Check if glyph bounds are within font bounds
    glyph.x_min >= font_x_min &&
      glyph.y_min >= font_y_min &&
      glyph.x_max <= font_x_max &&
      glyph.y_max <= font_y_max
  end
rescue StandardError
  false
end

#no_empty_glyphs_except_special?(loca, _head, num_glyphs) ⇒ Boolean

Validation helper: Check if all non-special glyphs have contours

The .notdef glyph (ID 0) can be empty, but other glyphs should have geometry

Parameters:

  • loca (Loca)

    Loca table for glyph access

  • head (Head)

    Head table for context

  • num_glyphs (Integer)

    Total number of glyphs to check

Returns:

  • (Boolean)

    True if all non-special glyphs have contours



169
170
171
172
173
174
175
176
177
178
179
# File 'lib/fontisan/tables/glyf.rb', line 169

def no_empty_glyphs_except_special?(loca, _head, num_glyphs)
  # Check glyphs 1 through num_glyphs-1 (.notdef at 0 can be empty)
  (1...num_glyphs).all? do |glyph_id|
    size = loca.size_of(glyph_id)
    # Empty glyphs (like space) are allowed, but check if they should be empty
    # This is a basic check - we just ensure non-control glyphs have data
    size.nil? || size.positive?
  end
rescue StandardError
  false
end

#valid_contour_count?(glyph_id, loca, head) ⇒ Boolean

Validation helper: Check if glyph has valid number of contours

Parameters:

  • glyph_id (Integer)

    Glyph ID to check

  • loca (Loca)

    Loca table for glyph access

  • head (Head)

    Head table for context

Returns:

  • (Boolean)

    True if contour count is valid



243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/fontisan/tables/glyf.rb', line 243

def valid_contour_count?(glyph_id, loca, head)
  glyph = glyph_for(glyph_id, loca, head)
  return true if glyph.nil? # Empty glyphs are OK

  # Simple glyphs: contours should be >= 0
  # Compound glyphs: numberOfContours = -1
  if glyph.respond_to?(:num_contours)
    glyph.num_contours >= -1
  else
    true
  end
rescue StandardError
  false
end