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, Type1Font, TrueTypeCollection, or OpenTypeCollection).

Examples:

Load any font type

font = FontLoader.load("font.ttf")  # => TrueTypeFont
font = FontLoader.load("font.otf")  # => OpenTypeFont
font = FontLoader.load("font.pfb")  # => Type1Font
font = FontLoader.load("font.pfa")  # => Type1Font
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



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/fontisan/font_loader.rb', line 101

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)
    io.rewind

    # Check for TTC/OTC signature
    return true if signature == Constants::TTC_TAG

    # Check for dfont - dfont is a collection format even if it contains only one font
    if signature == Constants::DFONT_RESOURCE_HEADER
      require_relative "parsers/dfont_parser"
      return Parsers::DfontParser.dfont?(io)
    end

    false
  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:



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
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/fontisan/font_loader.rb', line 47

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)

  # Check for Type 1 format first (PFB/PFA have different signatures)
  if type1_font?(path)
    return Type1Font.from_file(path, mode: resolved_mode)
  end

  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), "true"
      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)
    when Constants::DFONT_RESOURCE_HEADER
      extract_and_load_dfont(io, path, font_index, resolved_mode,
                             resolved_lazy)
    else
      raise InvalidFontError,
            "Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, WOFF2, PFB, or PFA file."
    end
  end
end

.load_collection(path) ⇒ TrueTypeCollection, ...

Load a collection object without extracting fonts

Returns the collection object (TrueTypeCollection, OpenTypeCollection, or DfontCollection) 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)

dfont (Data Fork Font) is an Apple-specific format that contains Mac font suitcase resources. It can contain multiple SFNT fonts (TrueType or OpenType).

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

For dfont files, returns DfontCollection.

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)

  • dfont suitcases (Apple-specific)

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



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/fontisan/font_loader.rb', line 173

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)
    io.rewind

    # Check for dfont
    if signature == Constants::DFONT_RESOURCE_HEADER || dfont_signature?(io)
      require_relative "dfont_collection"
      return DfontCollection.from_file(path)
    end

    # Check for TTC/OTC
    unless signature == Constants::TTC_TAG
      raise InvalidFontError,
            "File is not a collection (TTC/OTC/dfont). 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 = Array.new(num_fonts) { 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.positive?
      OpenTypeCollection.from_file(path)
    else
      # All fonts are TrueType
      TrueTypeCollection.from_file(path)
    end
  end
end