Class: Fontisan::TrueTypeFont

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

Overview

Extensions to TrueTypeFont for table-based construction

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

#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



439
440
441
# File 'lib/fontisan/true_type_font.rb', line 439

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



81
82
83
84
85
86
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
# File 'lib/fontisan/true_type_font.rb', line 81

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
      # Reuse existing IO handle by duplicating it to prevent double file open
      # The dup ensures the handle stays open after this block closes
      font.io_source = io.dup
      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_tables(tables) ⇒ TrueTypeFont

Create font from hash of tables

This is used during font conversion when we have tables but not a file.

Parameters:

  • tables (Hash<String, String>)

    Map of table tag to binary data

Returns:



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/fontisan/true_type_font_extensions.rb', line 12

def self.from_tables(tables)
  # Create minimal header structure
  font = new
  font.initialize_storage
  font.loading_mode = LoadingModes::FULL

  # Store table data
  font.table_data = tables

  # Build header from tables
  num_tables = tables.size
  max_power = 0
  n = num_tables
  while n > 1
    n >>= 1
    max_power += 1
  end

  search_range = (1 << max_power) * 16
  entry_selector = max_power
  range_shift = (num_tables * 16) - search_range

  font.header.sfnt_version = 0x00010000 # TrueType
  font.header.num_tables = num_tables
  font.header.search_range = search_range
  font.header.entry_selector = entry_selector
  font.header.range_shift = range_shift

  # Build table directory
  font.tables.clear
  tables.each_key do |tag|
    entry = TableDirectory.new
    entry.tag = tag
    entry.checksum = 0 # Will be calculated on write
    entry.offset = 0 # Will be calculated on write
    entry.table_length = tables[tag].bytesize
    font.tables << entry
  end

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



119
120
121
122
123
124
125
126
127
128
# File 'lib/fontisan/true_type_font.rb', line 119

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

#cff?Boolean

Check if font is CFF flavored

Returns:

  • (Boolean)

    false for TrueType fonts



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

def cff?
  false
end

#closevoid

This method returns an undefined value.

Close the IO source (for lazy loading)



423
424
425
426
# File 'lib/fontisan/true_type_font.rb', line 423

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



375
376
377
378
# File 'lib/fontisan/true_type_font.rb', line 375

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:



313
314
315
# File 'lib/fontisan/true_type_font.rb', line 313

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



391
392
393
394
# File 'lib/fontisan/true_type_font.rb', line 391

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



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

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

#head_tableTableDirectory?

Get the head table entry

Returns:



320
321
322
# File 'lib/fontisan/true_type_font.rb', line 320

def head_table
  find_table_entry(Constants::HEAD_TAG)
end

#initialize_storagevoid

This method returns an undefined value.

Initialize storage hashes (Fontisan extension)



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

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

#post_script_nameString?

Get PostScript name

Returns:

  • (String, nil)

    PostScript name or nil if not found



399
400
401
402
# File 'lib/fontisan/true_type_font.rb', line 399

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



407
408
409
410
# File 'lib/fontisan/true_type_font.rb', line 407

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



415
416
417
418
# File 'lib/fontisan/true_type_font.rb', line 415

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



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
232
233
234
235
236
237
# File 'lib/fontisan/true_type_font.rb', line 179

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



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/fontisan/true_type_font.rb', line 148

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



431
432
433
# File 'lib/fontisan/true_type_font.rb', line 431

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



383
384
385
386
# File 'lib/fontisan/true_type_font.rb', line 383

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



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/fontisan/true_type_font.rb', line 339

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



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

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



327
328
329
# File 'lib/fontisan/true_type_font.rb', line 327

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



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/fontisan/true_type_font.rb', line 247

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

#truetype?Boolean

Check if font is TrueType flavored

Returns:

  • (Boolean)

    true for TrueType fonts



280
281
282
# File 'lib/fontisan/true_type_font.rb', line 280

def truetype?
  true
end

#units_per_emInteger?

Get units per em from head table (Fontisan extension)

Returns:

  • (Integer, nil)

    Units per em value



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

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



267
268
269
270
271
272
273
274
275
# File 'lib/fontisan/true_type_font.rb', line 267

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