Class: Fontisan::Tables::Glyf

Inherits:
Binary::BaseRecord show all
Extended by:
Registered
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 included from Registered

register_tag

Methods inherited from Binary::BaseRecord

#valid?

Instance Attribute Details

#glyphs_cacheHash

Lazy initialization of glyphs cache

Returns:

  • (Hash)

    The glyphs cache



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

def glyphs_cache
  @glyphs_cache
end

#raw_dataObject

Store the raw data for deferred parsing



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

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



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

def self.read(io)
  instance = new

  # Initialize cache
  instance.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



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

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



139
140
141
# File 'lib/fontisan/tables/glyf.rb', line 139

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



147
148
149
# File 'lib/fontisan/tables/glyf.rb', line 147

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.



132
133
134
# File 'lib/fontisan/tables/glyf.rb', line 132

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:



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
122
123
124
# File 'lib/fontisan/tables/glyf.rb', line 94

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



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

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.nil?
      # Compound glyphs may have instructions too
      true
    else
      inst_len = glyph.instruction_length
      # If instructions present, length should be reasonable
      inst_len.nil? || inst_len >= 0
    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



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

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



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/fontisan/tables/glyf.rb', line 166

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



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

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
  case glyph
  when SimpleGlyph, CompoundGlyph
    glyph.num_contours >= -1
  else
    true
  end
rescue StandardError
  false
end