Class: Fontisan::TrueTypeFont

Inherits:
BinData::Record
  • Object
show all
Defined in:
lib/fontisan/true_type_font.rb

Overview

TrueType Font domain object using BinData

Represents a complete TrueType Font file using BinData’s declarative DSL for binary structure definition. The structure definition IS the documentation, and BinData handles all low-level reading/writing.

Extended from ExtractTTC to support table parsing for analysis.

Examples:

Reading and analyzing a font

ttf = TrueTypeFont.from_file("font.ttf")
puts ttf.header.num_tables  # => 14
name_table = ttf.table("name")  # Fontisan extension
puts name_table.english_name(Tables::Name::FAMILY)

Loading with metadata mode

ttf = TrueTypeFont.from_file("font.ttf", mode: :metadata)
puts ttf.loading_mode  # => :metadata
ttf.table_available?("GSUB")  # => false

Writing a font

ttf.to_file("output.ttf")

Constant Summary collapse

PAGE_SIZE =

Page size for lazy loading alignment (typical filesystem page size)

4096

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#io_sourceObject

IO source for lazy loading



67
68
69
# File 'lib/fontisan/true_type_font.rb', line 67

def io_source
  @io_source
end

#lazy_load_enabledObject

Whether lazy loading is enabled



70
71
72
# File 'lib/fontisan/true_type_font.rb', line 70

def lazy_load_enabled
  @lazy_load_enabled
end

#loading_modeObject

Loading mode for this font (:metadata or :full)



64
65
66
# File 'lib/fontisan/true_type_font.rb', line 64

def loading_mode
  @loading_mode
end

#page_cacheObject

Page cache for lazy loading (maps page_start_offset => page_data)



73
74
75
# File 'lib/fontisan/true_type_font.rb', line 73

def page_cache
  @page_cache
end

#parsed_tablesObject

Parsed table instances cache (Fontisan extension)



61
62
63
# File 'lib/fontisan/true_type_font.rb', line 61

def parsed_tables
  @parsed_tables
end

#table_dataObject

Table data is stored separately since it’s at variable offsets



58
59
60
# File 'lib/fontisan/true_type_font.rb', line 58

def table_data
  @table_data
end

Class Method Details

.finalize(io) ⇒ Proc

Finalizer proc for closing IO

Parameters:

  • io (IO)

    The IO object to close

Returns:

  • (Proc)

    The finalizer proc



429
430
431
# File 'lib/fontisan/true_type_font.rb', line 429

def self.finalize(io)
  proc { io&.close }
end

.from_file(path, mode: LoadingModes::FULL, lazy: false) ⇒ TrueTypeFont

Read TrueType Font from a file

Parameters:

  • path (String)

    Path to the TTF file

  • mode (Symbol) (defaults to: LoadingModes::FULL)

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

  • lazy (Boolean) (defaults to: false)

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

Returns:

Raises:

  • (ArgumentError)

    if path is nil or empty, or if mode is invalid

  • (Errno::ENOENT)

    if file does not exist

  • (RuntimeError)

    if file format is invalid



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/fontisan/true_type_font.rb', line 87

def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
  if path.nil? || path.to_s.empty?
    raise ArgumentError,
          "path cannot be nil or empty"
  end
  raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)

  # Validate mode
  LoadingModes.validate_mode!(mode)

  File.open(path, "rb") do |io|
    font = read(io)
    font.initialize_storage
    font.loading_mode = mode
    font.lazy_load_enabled = lazy

    if lazy
      # Keep file handle open for lazy loading
      font.io_source = File.open(path, "rb")
      font.setup_finalizer
    else
      # Read tables upfront
      font.read_table_data(io)
    end

    font
  end
rescue BinData::ValidityError, EOFError => e
  raise "Invalid TTF file: #{e.message}"
end

.from_ttc(io, offset, mode: LoadingModes::FULL) ⇒ TrueTypeFont

Read TrueType Font from TTC at specific offset

Parameters:

  • io (IO)

    Open file handle

  • offset (Integer)

    Byte offset to the font

  • mode (Symbol) (defaults to: LoadingModes::FULL)

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

Returns:



124
125
126
127
128
129
130
131
132
133
# File 'lib/fontisan/true_type_font.rb', line 124

def self.from_ttc(io, offset, mode: LoadingModes::FULL)
  LoadingModes.validate_mode!(mode)

  io.seek(offset)
  font = read(io)
  font.initialize_storage
  font.loading_mode = mode
  font.read_table_data(io)
  font
end

Instance Method Details

#closevoid

This method returns an undefined value.

Close the IO source (for lazy loading)



413
414
415
416
# File 'lib/fontisan/true_type_font.rb', line 413

def close
  @io_source&.close
  @io_source = nil
end

#family_nameString?

Get font family name

Returns:

  • (String, nil)

    Family name or nil if not found



365
366
367
368
# File 'lib/fontisan/true_type_font.rb', line 365

def family_name
  name_table = table(Constants::NAME_TAG)
  name_table&.english_name(Tables::Name::FAMILY)
end

#find_table_entry(tag) ⇒ TableDirectory?

Find a table entry by tag

Parameters:

  • tag (String)

    The table tag to find

Returns:



303
304
305
# File 'lib/fontisan/true_type_font.rb', line 303

def find_table_entry(tag)
  tables.find { |entry| entry.tag == tag }
end

#full_nameString?

Get full font name

Returns:

  • (String, nil)

    Full name or nil if not found



381
382
383
384
# File 'lib/fontisan/true_type_font.rb', line 381

def full_name
  name_table = table(Constants::NAME_TAG)
  name_table&.english_name(Tables::Name::FULL_NAME)
end

#has_table?(tag) ⇒ Boolean

Check if font has a specific table

Parameters:

  • tag (String)

    The table tag to check for

Returns:

  • (Boolean)

    true if table exists, false otherwise



286
287
288
# File 'lib/fontisan/true_type_font.rb', line 286

def has_table?(tag)
  tables.any? { |entry| entry.tag == tag }
end

#head_tableTableDirectory?

Get the head table entry

Returns:



310
311
312
# File 'lib/fontisan/true_type_font.rb', line 310

def head_table
  find_table_entry(Constants::HEAD_TAG)
end

#initialize_storagevoid

This method returns an undefined value.

Initialize storage hashes (Fontisan extension)



138
139
140
141
142
143
144
145
# File 'lib/fontisan/true_type_font.rb', line 138

def initialize_storage
  @table_data = {}
  @parsed_tables = {}
  @loading_mode = LoadingModes::FULL
  @lazy_load_enabled = false
  @io_source = nil
  @page_cache = {}
end

#post_script_nameString?

Get PostScript name

Returns:

  • (String, nil)

    PostScript name or nil if not found



389
390
391
392
# File 'lib/fontisan/true_type_font.rb', line 389

def post_script_name
  name_table = table(Constants::NAME_TAG)
  name_table&.english_name(Tables::Name::POSTSCRIPT_NAME)
end

#preferred_family_nameString?

Get preferred family name

Returns:

  • (String, nil)

    Preferred family name or nil if not found



397
398
399
400
# File 'lib/fontisan/true_type_font.rb', line 397

def preferred_family_name
  name_table = table(Constants::NAME_TAG)
  name_table&.english_name(Tables::Name::PREFERRED_FAMILY)
end

#preferred_subfamily_nameString?

Get preferred subfamily name

Returns:

  • (String, nil)

    Preferred subfamily name or nil if not found



405
406
407
408
# File 'lib/fontisan/true_type_font.rb', line 405

def preferred_subfamily_name
  name_table = table(Constants::NAME_TAG)
  name_table&.english_name(Tables::Name::PREFERRED_SUBFAMILY)
end

#read_metadata_tables_batched(io) ⇒ void

This method returns an undefined value.

Read metadata tables using page-aware batching

Groups adjacent tables within page boundaries and reads them together to maximize filesystem prefetching and minimize random seeks.

Parameters:

  • io (IO)

    Open file handle



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
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/fontisan/true_type_font.rb', line 185

def (io)
  # Typical filesystem page size (4KB is common, but 8KB gives better prefetch window)
  page_threshold = 8192

  # Get metadata tables sorted by offset for sequential access
   = tables.select { |entry| LoadingModes::METADATA_TABLES_SET.include?(entry.tag) }
  .sort_by!(&:offset)

  return if .empty?

  # Group adjacent tables within page threshold for batched reading
  i = 0
  while i < .size
    batch_start = [i]
    batch_end = batch_start
    batch_entries = [batch_start]

    # Extend batch while next table is within page threshold
    j = i + 1
    while j < .size
      next_entry = [j]
      gap = next_entry.offset - (batch_end.offset + batch_end.table_length)

      # If gap is small (within page threshold), include in batch
      if gap <= page_threshold
        batch_end = next_entry
        batch_entries << next_entry
        j += 1
      else
        break
      end
    end

    # Read batch
    if batch_entries.size == 1
      # Single table, read normally
      io.seek(batch_start.offset)
      tag_key = batch_start.tag.dup.force_encoding("UTF-8")
      @table_data[tag_key] = io.read(batch_start.table_length)
    else
      # Multiple tables, read contiguous segment
      batch_offset = batch_start.offset
      batch_length = (batch_end.offset + batch_end.table_length) - batch_start.offset

      io.seek(batch_offset)
      batch_data = io.read(batch_length)

      # Extract individual tables from batch
      batch_entries.each do |entry|
        relative_offset = entry.offset - batch_offset
        tag_key = entry.tag.dup.force_encoding("UTF-8")
        @table_data[tag_key] = batch_data[relative_offset, entry.table_length]
      end
    end

    i = j
  end
end

#read_table_data(io) ⇒ void

This method returns an undefined value.

Read table data for all tables

In metadata mode, only reads metadata tables. In full mode, reads all tables. In lazy load mode, doesn’t read data upfront.

Parameters:

  • io (IO)

    Open file handle



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/fontisan/true_type_font.rb', line 154

def read_table_data(io)
  @table_data = {}

  if @lazy_load_enabled
    # Don't read data, just keep IO reference
    @io_source = io
    return
  end

  if @loading_mode == LoadingModes::METADATA
    # Only read metadata tables for performance
    # Use page-aware batched reading to maximize filesystem prefetching
    (io)
  else
    # Read all tables
    tables.each do |entry|
      io.seek(entry.offset)
      # Force UTF-8 encoding on tag for hash key consistency
      tag_key = entry.tag.dup.force_encoding("UTF-8")
      @table_data[tag_key] = io.read(entry.table_length)
    end
  end
end

#setup_finalizervoid

This method returns an undefined value.

Setup finalizer for cleanup



421
422
423
# File 'lib/fontisan/true_type_font.rb', line 421

def setup_finalizer
  ObjectSpace.define_finalizer(self, self.class.finalize(@io_source))
end

#subfamily_nameString?

Get font subfamily name (e.g., Regular, Bold, Italic)

Returns:

  • (String, nil)

    Subfamily name or nil if not found



373
374
375
376
# File 'lib/fontisan/true_type_font.rb', line 373

def subfamily_name
  name_table = table(Constants::NAME_TAG)
  name_table&.english_name(Tables::Name::SUBFAMILY)
end

#table(tag) ⇒ Tables::*?

Get parsed table instance (Fontisan extension)

This method parses the raw table data into a structured table object and caches the result for subsequent calls. Enforces mode restrictions.

Parameters:

  • tag (String)

    The table tag to retrieve

Returns:

  • (Tables::*, nil)

    Parsed table object or nil if not found

Raises:

  • (ArgumentError)

    if table is not available in current loading mode



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/fontisan/true_type_font.rb', line 329

def table(tag)
  # Check mode restrictions
  unless table_available?(tag)
    if has_table?(tag)
      raise ArgumentError,
            "Table '#{tag}' is not available in #{@loading_mode} mode. " \
            "Available tables: #{LoadingModes.tables_for(@loading_mode).inspect}"
    else
      return nil
    end
  end

  return @parsed_tables[tag] if @parsed_tables.key?(tag)

  # Lazy load table data if enabled
  if @lazy_load_enabled && !@table_data.key?(tag)
    load_table_data(tag)
  end

  @parsed_tables[tag] ||= parse_table(tag)
end

#table_available?(tag) ⇒ Boolean

Check if a table is available in the current loading mode

Parameters:

  • tag (String)

    The table tag to check

Returns:

  • (Boolean)

    true if table is available in current mode



294
295
296
297
# File 'lib/fontisan/true_type_font.rb', line 294

def table_available?(tag)
  return false unless has_table?(tag)
  LoadingModes.table_allowed?(@loading_mode, tag)
end

#table_namesArray<String>

Get list of all table tags (Fontisan extension)

Returns:

  • (Array<String>)

    Array of table tag strings



317
318
319
# File 'lib/fontisan/true_type_font.rb', line 317

def table_names
  tables.map(&:tag)
end

#to_file(path) ⇒ Integer

Write TrueType Font to a file

Writes the complete TTF structure to disk, including proper checksum calculation and table alignment.

Parameters:

  • path (String)

    Path where the TTF file will be written

Returns:

  • (Integer)

    Number of bytes written

Raises:

  • (IOError)

    if writing fails



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/fontisan/true_type_font.rb', line 252

def to_file(path)
  File.open(path, "wb") do |io|
    # Write header and tables (directory)
    write_structure(io)

    # Write table data with updated offsets
    write_table_data_with_offsets(io)

    io.pos
  end

  # Update checksum adjustment in head table
  update_checksum_adjustment_in_file(path) if head_table

  File.size(path)
end

#units_per_emInteger?

Get units per em from head table (Fontisan extension)

Returns:

  • (Integer, nil)

    Units per em value



354
355
356
357
# File 'lib/fontisan/true_type_font.rb', line 354

def units_per_em
  head = table(Constants::HEAD_TAG)
  head&.units_per_em
end

#valid?Boolean

Validate format correctness

Returns:

  • (Boolean)

    true if the TTF format is valid, false otherwise



272
273
274
275
276
277
278
279
280
# File 'lib/fontisan/true_type_font.rb', line 272

def valid?
  return false unless header
  return false unless tables.respond_to?(:length)
  return false unless @table_data.is_a?(Hash)
  return false if tables.length != header.num_tables
  return false unless head_table

  true
end