Class: Fontisan::GlyphAccessor

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/glyph_accessor.rb

Overview

High-level utility class for unified glyph access across font formats

[‘GlyphAccessor`](lib/fontisan/glyph_accessor.rb) provides a clean, unified interface for accessing glyphs regardless of the underlying font format (TrueType with glyf table or OpenType with CFF table).

This class automatically detects the font format and delegates to the appropriate table parser, abstracting away the complexity of different glyph storage mechanisms.

Key features:

  • Unified glyph access by ID, Unicode character, or PostScript name

  • Automatic format detection (TrueType glyf vs CFF)

  • Metrics retrieval (advance width, left sidebearing)

  • Glyph closure calculation for subsetting (tracks composite dependencies)

  • Validation of glyph IDs and character mappings

Reference: [‘docs/ttfunk-feature-analysis.md:541-575`](docs/ttfunk-feature-analysis.md:541)

Examples:

Basic usage

font = Fontisan::TrueTypeFont.from_file('font.ttf')
accessor = Fontisan::GlyphAccessor.new(font)

# Access glyph by ID
glyph = accessor.glyph_for_id(42)
puts glyph.class  # => SimpleGlyph or CompoundGlyph

# Access glyph by Unicode character
glyph_a = accessor.glyph_for_char(0x0041)  # 'A'

# Get metrics
metrics = accessor.metrics_for_id(42)
puts "Width: #{metrics[:advance_width]}, LSB: #{metrics[:lsb]}"

Subsetting workflow with closure

# Calculate all glyphs needed (including composite dependencies)
base_glyphs = [0, 1, 65, 66, 67]  # .notdef, A, B, C
all_glyphs = accessor.closure_for(base_glyphs)
puts "Total glyphs needed: #{all_glyphs.size}"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(font) ⇒ GlyphAccessor

Initialize a new glyph accessor

Parameters:

Raises:

  • (ArgumentError)

    If font is nil or doesn’t respond to table method



52
53
54
55
56
57
58
59
60
61
62
# File 'lib/fontisan/glyph_accessor.rb', line 52

def initialize(font)
  raise ArgumentError, "Font cannot be nil" if font.nil?

  unless font.respond_to?(:table)
    raise ArgumentError, "Font must respond to :table method"
  end

  @font = font
  @glyph_cache = {}
  @closure_cache = {}
end

Instance Attribute Details

#fontTrueTypeFont, OpenTypeFont (readonly)

Font instance this accessor operates on



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

def font
  @font
end

Instance Method Details

#cff?Boolean

Check if font uses CFF outlines (CFF table)

Returns:

  • (Boolean)

    True if font has CFF table



289
290
291
# File 'lib/fontisan/glyph_accessor.rb', line 289

def cff?
  font.table("CFF ") != nil
end

#clear_cachevoid

This method returns an undefined value.

Clear internal caches to free memory

Useful for long-running processes that access many glyphs.



367
368
369
370
371
372
373
374
# File 'lib/fontisan/glyph_accessor.rb', line 367

def clear_cache
  @glyph_cache.clear
  @closure_cache.clear

  # Also clear glyf table cache if present
  glyf = font.table("glyf")
  glyf&.clear_cache if glyf.respond_to?(:clear_cache)
end

#closure_for(glyph_ids) ⇒ Set<Integer>

Calculate glyph closure for subsetting

This method recursively tracks all glyphs needed for a given set of glyph IDs, including component glyphs referenced by compound glyphs. This is essential for font subsetting to ensure all required glyphs are included.

The closure always includes glyph 0 (.notdef) as required by the OpenType specification.

Examples:

Calculate closure for subsetting

# Want to subset to just "ABC"
base_glyphs = [65, 66, 67]  # Assuming these are glyph IDs for A, B, C
all_needed = accessor.closure_for(base_glyphs)
# all_needed includes base glyphs + any composite dependencies + .notdef

Closure with composite glyphs

# If 'Ä' (glyph 100) is composite referencing 'A' (glyph 65) and
# dieresis (glyph 200)
closure = accessor.closure_for([100])
# Returns: [0, 100, 65, 200]  (includes .notdef, Ä, A, dieresis)

Parameters:

  • glyph_ids (Array<Integer>)

    Base set of glyph IDs

Returns:

  • (Set<Integer>)

    Complete set of glyph IDs needed (including composite dependencies)

Raises:

  • (ArgumentError)

    If glyph_ids is not an array



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/fontisan/glyph_accessor.rb', line 319

def closure_for(glyph_ids)
  unless glyph_ids.is_a?(Array)
    raise ArgumentError, "glyph_ids must be an Array"
  end

  # Start with provided glyphs plus .notdef
  result = Set.new([0])
  glyph_ids.each { |id| result.add(id) if glyph_exists?(id) }

  # CFF fonts have no composite glyphs, so return early
  return result if cff?

  # Recursively collect composite dependencies (TrueType only)
  to_process = result.to_a.dup
  processed = Set.new

  while (glyph_id = to_process.shift)
    next if processed.include?(glyph_id)

    processed.add(glyph_id)

    # Get glyph and check if it's compound
    glyph = glyph_for_id(glyph_id)
    next unless glyph
    next unless glyph.respond_to?(:compound?) && glyph.compound?

    # Add component glyph IDs
    if glyph.respond_to?(:components)
      glyph.components.each do |component|
        component_id = component[:glyph_index]
        next unless glyph_exists?(component_id)

        unless result.include?(component_id)
          result.add(component_id)
          to_process << component_id
        end
      end
    end
  end

  result
end

#glyph_exists?(glyph_id) ⇒ Boolean

Check if a glyph ID exists and is valid

Parameters:

  • glyph_id (Integer)

    Glyph ID to check

Returns:

  • (Boolean)

    True if glyph ID is valid



262
263
264
265
266
267
268
269
# File 'lib/fontisan/glyph_accessor.rb', line 262

def glyph_exists?(glyph_id)
  return false if glyph_id.nil? || glyph_id.negative?

  maxp = font.table("maxp")
  return false unless maxp

  glyph_id < maxp.num_glyphs
end

#glyph_for_char(char_code) ⇒ SimpleGlyph, ...

Get glyph object for a Unicode character code

Uses the cmap table to map the character code to a glyph ID, then retrieves the corresponding glyph.

Examples:

Get glyph for ‘A’

glyph_a = accessor.glyph_for_char(0x0041)
glyph_a = accessor.glyph_for_char('A'.ord)  # Equivalent

Parameters:

  • char_code (Integer)

    Unicode character code (e.g., 0x0041 for ‘A’)

Returns:

  • (SimpleGlyph, CompoundGlyph, nil)

    Glyph object or nil if character is not mapped

Raises:



113
114
115
116
117
118
# File 'lib/fontisan/glyph_accessor.rb', line 113

def glyph_for_char(char_code)
  glyph_id = char_to_glyph_id(char_code)
  return nil unless glyph_id

  glyph_for_id(glyph_id)
end

#glyph_for_id(glyph_id) ⇒ SimpleGlyph, ...

Get glyph object for a glyph ID

Returns the appropriate glyph object based on the font format:

  • TrueType fonts: [‘SimpleGlyph`](lib/fontisan/tables/glyf/simple_glyph.rb) or [`CompoundGlyph`](lib/fontisan/tables/glyf/compound_glyph.rb)

  • CFF fonts: [‘CFFGlyph`](lib/fontisan/tables/cff/cff_glyph.rb)

Examples:

Get a glyph

glyph = accessor.glyph_for_id(65)
if glyph
  puts "Bounding box: #{glyph.bounding_box}"
  puts "Type: #{glyph.simple? ? 'simple' : 'compound'}"
end

Parameters:

  • glyph_id (Integer)

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

Returns:

  • (SimpleGlyph, CompoundGlyph, CFFGlyph, nil)

    Glyph object or nil if glyph is empty or invalid

Raises:



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/fontisan/glyph_accessor.rb', line 83

def glyph_for_id(glyph_id)
  validate_glyph_id!(glyph_id)

  return @glyph_cache[glyph_id] if @glyph_cache.key?(glyph_id)

  glyph = if truetype?
            truetype_glyph(glyph_id)
          elsif cff?
            cff_glyph(glyph_id)
          else
            raise Fontisan::MissingTableError,
                  "Font has neither glyf nor CFF table"
          end

  @glyph_cache[glyph_id] = glyph
end

#glyph_for_name(glyph_name) ⇒ SimpleGlyph, ...

Get glyph object for a PostScript glyph name

Uses the post table (if available) to map the glyph name to a glyph ID. This method is primarily useful for fonts with post table version 2.0.

Examples:

Get glyph by name

glyph = accessor.glyph_for_name("A")
glyph = accessor.glyph_for_name("Aacute")

Parameters:

  • glyph_name (String)

    PostScript glyph name (e.g., “A”, “Aacute”)

Returns:

  • (SimpleGlyph, CompoundGlyph, nil)

    Glyph object or nil if name is not found

Raises:



134
135
136
137
138
139
# File 'lib/fontisan/glyph_accessor.rb', line 134

def glyph_for_name(glyph_name)
  glyph_id = name_to_glyph_id(glyph_name)
  return nil unless glyph_id

  glyph_for_id(glyph_id)
end

#has_glyph_for_char?(char_code) ⇒ Boolean

Check if a Unicode character is mapped in the font

Parameters:

  • char_code (Integer)

    Unicode character code

Returns:

  • (Boolean)

    True if character has a glyph mapping



275
276
277
# File 'lib/fontisan/glyph_accessor.rb', line 275

def has_glyph_for_char?(char_code)
  !char_to_glyph_id(char_code).nil?
end

#metrics_for_char(char_code) ⇒ Hash{Symbol => Integer}?

Get horizontal metrics for a Unicode character

Examples:

Get metrics for ‘A’

metrics = accessor.metrics_for_char(0x0041)

Parameters:

  • char_code (Integer)

    Unicode character code

Returns:

  • (Hash{Symbol => Integer}, nil)

    Metrics hash or nil if not mapped

Raises:



176
177
178
179
180
181
# File 'lib/fontisan/glyph_accessor.rb', line 176

def metrics_for_char(char_code)
  glyph_id = char_to_glyph_id(char_code)
  return nil unless glyph_id

  metrics_for_id(glyph_id)
end

#metrics_for_id(glyph_id) ⇒ Hash{Symbol => Integer}?

Get horizontal metrics for a glyph ID

Returns a hash with advance width and left sidebearing in font units.

Examples:

Get metrics

metrics = accessor.metrics_for_id(65)  # 'A'
puts "Advance width: #{metrics[:advance_width]} FUnits"
puts "Left sidebearing: #{metrics[:lsb]} FUnits"

Parameters:

  • glyph_id (Integer)

    Glyph ID

Returns:

  • (Hash{Symbol => Integer}, nil)

    Hash with :advance_width and :lsb keys, or nil if glyph is invalid

Raises:



154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/fontisan/glyph_accessor.rb', line 154

def metrics_for_id(glyph_id)
  validate_glyph_id!(glyph_id)

  hmtx = font.table("hmtx")
  raise_missing_table!("hmtx") unless hmtx

  unless hmtx.parsed?
    # Auto-parse if not already parsed
    parse_hmtx_with_context!(hmtx)
  end

  hmtx.metric_for(glyph_id)
end

#outline_for_char(char) ⇒ Models::GlyphOutline?

Get outline for character

Convenience method that takes a character string and extracts its outline. The character is converted to its Unicode codepoint first.

Examples:

Get outline for ‘A’

outline = accessor.outline_for_char('A')
commands = outline.to_commands if outline

Handle multi-codepoint characters

outline = accessor.outline_for_char('A')  # Works
outline = accessor.outline_for_char('AB') # ArgumentError

Parameters:

  • char (String)

    Single character (e.g., ‘A’, ‘中’, ‘😀’)

Returns:

  • (Models::GlyphOutline, nil)

    Outline object or nil if character is not mapped or glyph is empty

Raises:



249
250
251
252
253
254
255
256
# File 'lib/fontisan/glyph_accessor.rb', line 249

def outline_for_char(char)
  unless char.is_a?(String) && char.length == 1
    raise ArgumentError,
          "char must be a single character String, got: #{char.inspect}"
  end

  outline_for_codepoint(char.ord)
end

#outline_for_codepoint(codepoint) ⇒ Models::GlyphOutline?

Get outline for Unicode codepoint

Maps a Unicode codepoint to a glyph ID via the cmap table, then extracts the outline for that glyph.

Examples:

Get outline for ‘A’

outline = accessor.outline_for_codepoint(0x0041)
svg_path = outline.to_svg_path if outline

Parameters:

  • codepoint (Integer)

    Unicode codepoint (e.g., 0x0041 for ‘A’)

Returns:

  • (Models::GlyphOutline, nil)

    Outline object or nil if character is not mapped or glyph is empty

Raises:



224
225
226
227
228
229
# File 'lib/fontisan/glyph_accessor.rb', line 224

def outline_for_codepoint(codepoint)
  glyph_id = char_to_glyph_id(codepoint)
  return nil unless glyph_id

  outline_for_id(glyph_id)
end

#outline_for_id(glyph_id) ⇒ Models::GlyphOutline?

Get outline for glyph by ID

Extracts the complete outline data for a glyph, including all contours, points, and bounding box information. The outline can be converted to SVG paths or drawing commands for rendering.

This method uses [‘OutlineExtractor`](lib/fontisan/outline_extractor.rb) to handle both TrueType (glyf) and CFF outline formats transparently. For compound glyphs, it recursively resolves component dependencies.

Examples:

Get outline for a glyph

outline = accessor.outline_for_id(65)  # 'A'
if outline
  puts "Contours: #{outline.contour_count}"
  puts "Points: #{outline.point_count}"
  puts "SVG: #{outline.to_svg_path}"
end

Parameters:

  • glyph_id (Integer)

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

Returns:

Raises:



206
207
208
209
# File 'lib/fontisan/glyph_accessor.rb', line 206

def outline_for_id(glyph_id)
  extractor = OutlineExtractor.new(@font)
  extractor.extract(glyph_id)
end

#truetype?Boolean

Check if font uses TrueType outlines (glyf table)

Returns:

  • (Boolean)

    True if font has glyf table



282
283
284
# File 'lib/fontisan/glyph_accessor.rb', line 282

def truetype?
  font.table("glyf") != nil
end