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 https://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



46
47
48
# File 'lib/fontisan/tables/glyf.rb', line 46

def glyphs_cache
  @glyphs_cache
end

#raw_dataObject

Store the raw data for deferred parsing



42
43
44
# File 'lib/fontisan/tables/glyf.rb', line 42

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



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/fontisan/tables/glyf.rb', line 52

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



260
261
262
263
264
265
266
267
268
269
# File 'lib/fontisan/tables/glyf.rb', line 260

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



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

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



144
145
146
# File 'lib/fontisan/tables/glyf.rb', line 144

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.



129
130
131
# File 'lib/fontisan/tables/glyf.rb', line 129

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:



91
92
93
94
95
96
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
# File 'lib/fontisan/tables/glyf.rb', line 91

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



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/fontisan/tables/glyf.rb', line 212

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



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/fontisan/tables/glyf.rb', line 183

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



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/fontisan/tables/glyf.rb', line 163

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



237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/fontisan/tables/glyf.rb', line 237

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