Class: Fontisan::FontLoader
- Inherits:
-
Object
- Object
- Fontisan::FontLoader
- 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).
Class Method Summary collapse
-
.collection?(path) ⇒ Boolean
Check if a file is a collection (TTC or OTC).
-
.load(path, font_index: 0, mode: nil, lazy: nil) ⇒ TrueTypeFont, ...
Load a font from file with automatic format detection.
-
.load_collection(path) ⇒ TrueTypeCollection, ...
Load a collection object without extracting fonts.
Class Method Details
.collection?(path) ⇒ Boolean
Check if a file is a collection (TTC or OTC)
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
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:
-
Reads all font offsets from the collection header
-
Examines the sfnt_version of each font in the collection
-
Counts TrueType fonts (0x00010000 or 0x74727565 “true”) vs OpenType fonts (0x4F54544F “OTTO”)
-
If ANY font is OpenType (CFF), returns OpenTypeCollection
-
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)
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 |