Class: Fontisan::FontLoader

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

Overview

FontLoader provides unified font loading with automatic format detection.

This class is the primary entry point for loading fonts in Fontisan. It automatically detects the font format and returns the appropriate domain object (TrueTypeFont, OpenTypeFont, TrueTypeCollection, or OpenTypeCollection).

Examples:

Load any font type

font = FontLoader.load("font.ttf")  # => TrueTypeFont
font = FontLoader.load("font.otf")  # => OpenTypeFont
font = FontLoader.load("fonts.ttc") # => TrueTypeFont (first in collection)
font = FontLoader.load("fonts.ttc", font_index: 2) # => TrueTypeFont (third in collection)

Loading modes

font = FontLoader.load("font.ttf", mode: :metadata)  # Load only metadata tables
font = FontLoader.load("font.ttf", mode: :full)      # Load all tables

Lazy loading control

font = FontLoader.load("font.ttf", lazy: true)   # Tables loaded on-demand
font = FontLoader.load("font.ttf", lazy: false)  # All tables loaded upfront

Class Method Summary collapse

Class Method Details

.collection?(path) ⇒ Boolean

Check if a file is a collection (TTC or OTC)

Examples:

Check if file is collection

FontLoader.collection?("fonts.ttc") # => true
FontLoader.collection?("font.ttf")  # => false

Parameters:

  • path (String)

    Path to the font file

Returns:

  • (Boolean)

    true if file is a TTC/OTC collection

Raises:

  • (Errno::ENOENT)

    if file does not exist



90
91
92
93
94
95
96
97
# File 'lib/fontisan/font_loader.rb', line 90

def self.collection?(path)
  raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)

  File.open(path, "rb") do |io|
    signature = io.read(4)
    signature == Constants::TTC_TAG
  end
end

.load(path, font_index: 0, mode: nil, lazy: nil) ⇒ TrueTypeFont, ...

Load a font from file with automatic format detection

Parameters:

  • path (String)

    Path to the font file

  • font_index (Integer) (defaults to: 0)

    Index of font in collection (0-based, default: 0)

  • mode (Symbol) (defaults to: nil)

    Loading mode (:metadata or :full, default: from ENV or :full)

  • lazy (Boolean) (defaults to: nil)

    If true, load tables on demand (default: false for eager loading)

Returns:

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/fontisan/font_loader.rb', line 44

def self.load(path, font_index: 0, mode: nil, lazy: nil)
  raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)

  # Resolve mode and lazy parameters with environment variables
  resolved_mode = mode || env_mode || LoadingModes::FULL
  resolved_lazy = if lazy.nil?
                    env_lazy.nil? ? false : env_lazy
                  else
                    lazy
                  end

  # Validate mode
  LoadingModes.validate_mode!(resolved_mode)

  File.open(path, "rb") do |io|
    signature = io.read(4)
    io.rewind

    case signature
    when Constants::TTC_TAG
      load_from_collection(io, path, font_index, mode: resolved_mode,
                                                 lazy: resolved_lazy)
    when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
      TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
    when "OTTO"
      OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
    when "wOFF"
      WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
    when "wOF2"
      Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
    else
      raise InvalidFontError,
            "Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
    end
  end
end

.load_collection(path) ⇒ TrueTypeCollection, OpenTypeCollection

Load a collection object without extracting fonts

Returns the collection object (TrueTypeCollection or OpenTypeCollection) without extracting individual fonts. Useful for inspecting collection metadata and structure.

Collection Format Understanding

Both TTC (TrueType Collection) and OTC (OpenType Collection) files use the same “ttcf” signature. The distinction between TTC and OTC is NOT in the collection format itself, but in the fonts contained within:

  • TTC typically contains TrueType fonts (glyf outlines)

  • OTC typically contains OpenType fonts (CFF/CFF2 outlines)

  • Mixed collections are possible (both TTF and OTF in same collection)

Each collection can contain multiple SFNT-format font files, with table deduplication to save space. Individual fonts within a collection are stored at different offsets within the file, each with their own table directory and data tables.

Detection Strategy

This method scans ALL fonts in the collection to determine the collection type accurately:

  1. Reads all font offsets from the collection header

  2. Examines the sfnt_version of each font in the collection

  3. Counts TrueType fonts (0x00010000 or 0x74727565 “true”) vs OpenType fonts (0x4F54544F “OTTO”)

  4. If ANY font is OpenType (CFF), returns OpenTypeCollection

  5. Only returns TrueTypeCollection if ALL fonts are TrueType

This approach correctly handles:

  • Homogeneous collections (all TTF or all OTF)

  • Mixed collections (both TTF and OTF fonts) - uses OpenTypeCollection

  • Large collections with many fonts (like NotoSerifCJK.ttc with 35 fonts)

Examples:

Load collection for inspection

collection = FontLoader.load_collection("fonts.ttc")
puts "Collection has #{collection.num_fonts} fonts"

Parameters:

  • path (String)

    Path to the collection file

Returns:

Raises:

  • (Errno::ENOENT)

    if file does not exist

  • (InvalidFontError)

    if file is not a collection or type cannot be determined



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/fontisan/font_loader.rb', line 144

def self.load_collection(path)
  raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)

  File.open(path, "rb") do |io|
    signature = io.read(4)

    unless signature == Constants::TTC_TAG
      raise InvalidFontError,
            "File is not a collection (TTC/OTC). Use FontLoader.load instead."
    end

    # Read version and num_fonts
    io.seek(8) # Skip tag (4) + version (4)
    num_fonts = io.read(4).unpack1("N")

    # Read all font offsets
    font_offsets = num_fonts.times.map { io.read(4).unpack1("N") }

    # Scan all fonts to determine collection type (not just first)
    truetype_count = 0
    opentype_count = 0

    font_offsets.each do |offset|
      io.rewind
      io.seek(offset)
      sfnt_version = io.read(4).unpack1("N")

      case sfnt_version
      when Constants::SFNT_VERSION_TRUETYPE, 0x74727565 # 0x74727565 = 'true'
        truetype_count += 1
      when Constants::SFNT_VERSION_OTTO
        opentype_count += 1
      else
        raise InvalidFontError,
              "Unknown font type in collection at offset #{offset} (sfnt version: 0x#{sfnt_version.to_s(16)})"
      end
    end

    io.rewind

    # Determine collection type based on what fonts are inside
    # If ANY font is OpenType, use OpenTypeCollection (more general format)
    # Only use TrueTypeCollection if ALL fonts are TrueType
    if opentype_count > 0
      OpenTypeCollection.from_file(path)
    else
      # All fonts are TrueType
      TrueTypeCollection.from_file(path)
    end
  end
end