Class: Fontisan::BaseCollection Abstract

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

Overview

This class is abstract.

Abstract base class for font collections (TTC/OTC)

This class implements the shared logic for TrueTypeCollection and OpenTypeCollection using the Template Method pattern. Subclasses must implement the abstract methods to specify their font class and collection format.

The BinData structure definition is shared between both collection types since both TTC and OTC files use the same “ttcf” tag and binary format. The only differences are:

  1. The type of fonts contained (TrueType vs OpenType)

  2. The format string used for display (“TTC” vs “OTC”)

Examples:

Implementing a collection subclass

class TrueTypeCollection < BaseCollection
  def self.font_class
    TrueTypeFont
  end

  def self.collection_format
    "TTC"
  end
end

Direct Known Subclasses

OpenTypeCollection, TrueTypeCollection

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.collection_formatString

Abstract method: Get the collection format string

Subclasses must override this to return “TTC” or “OTC”.

Returns:

  • (String)

    Collection format (“TTC” or “OTC”)

Raises:

  • (NotImplementedError)

    if not overridden by subclass



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

def self.collection_format
  raise NotImplementedError,
        "#{name} must implement self.collection_format"
end

.font_classClass

Abstract method: Get the font class for this collection type

Subclasses must override this to return their specific font class (TrueTypeFont or OpenTypeFont).

Returns:

  • (Class)

    The font class (TrueTypeFont or OpenTypeFont)

Raises:

  • (NotImplementedError)

    if not overridden by subclass



47
48
49
50
# File 'lib/fontisan/base_collection.rb', line 47

def self.font_class
  raise NotImplementedError,
        "#{name} must implement self.font_class"
end

.from_file(path) ⇒ BaseCollection

Read collection from a file

Parameters:

  • path (String)

    Path to the collection file

Returns:

Raises:

  • (ArgumentError)

    if path is nil or empty

  • (Errno::ENOENT)

    if file does not exist

  • (RuntimeError)

    if file format is invalid



70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/fontisan/base_collection.rb', line 70

def self.from_file(path)
  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)

  File.open(path, "rb") { |io| read(io) }
rescue BinData::ValidityError => e
  raise "Invalid #{collection_format} file: #{e.message}"
rescue EOFError => e
  raise "Invalid #{collection_format} file: unexpected end of file - #{e.message}"
end

Instance Method Details

#collection_info(io, path) ⇒ CollectionInfo

Get comprehensive collection metadata

Returns a CollectionInfo model with header information, offsets, and table sharing statistics. This is the API method used by the ‘info` command for collections.

Examples:

Get collection info

File.open("fonts.ttc", "rb") do |io|
  collection = TrueTypeCollection.read(io)
  info = collection.collection_info(io, "fonts.ttc")
  puts "Version: #{info.version_string}"
end

Parameters:

  • io (IO)

    Open file handle to read fonts from

  • path (String)

    Collection file path (for file size)

Returns:

  • (CollectionInfo)

    Collection metadata



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/base_collection.rb', line 221

def collection_info(io, path)
  require_relative "models/collection_info"
  require_relative "models/table_sharing_info"

  # Calculate table sharing statistics
  table_sharing = calculate_table_sharing(io)

  # Get file size
  file_size = path ? File.size(path) : 0

  Models::CollectionInfo.new(
    collection_path: path,
    collection_format: self.class.collection_format,
    ttc_tag: tag,
    major_version: major_version,
    minor_version: minor_version,
    num_fonts: num_fonts,
    font_offsets: font_offsets.to_a,
    file_size_bytes: file_size,
    table_sharing: table_sharing,
  )
end

#extract_fonts(io) ⇒ Array

Extract fonts from the collection

Reads each font from the collection file and returns them as font objects.

Parameters:

  • io (IO)

    Open file handle to read fonts from

Returns:

  • (Array)

    Array of font objects (TrueTypeFont or OpenTypeFont)



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

def extract_fonts(io)
  font_class = self.class.font_class

  font_offsets.map do |offset|
    font_class.from_collection(io, offset)
  end
end

#font(index, io, mode: LoadingModes::FULL) ⇒ TrueTypeFont, ...

Get a single font from the collection

Parameters:

  • index (Integer)

    Index of the font (0-based)

  • io (IO)

    Open file handle

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

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

Returns:



104
105
106
107
108
109
# File 'lib/fontisan/base_collection.rb', line 104

def font(index, io, mode: LoadingModes::FULL)
  return nil if index >= num_fonts

  font_class = self.class.font_class
  font_class.from_collection(io, font_offsets[index], mode: mode)
end

#font_countInteger

Get font count

Returns:

  • (Integer)

    Number of fonts in collection



114
115
116
# File 'lib/fontisan/base_collection.rb', line 114

def font_count
  num_fonts
end

#list_fonts(io) ⇒ CollectionListInfo

List all fonts in the collection with basic metadata

Returns a CollectionListInfo model containing summaries of all fonts. This is the API method used by the ‘ls` command for collections.

Examples:

List fonts in collection

File.open("fonts.ttc", "rb") do |io|
  collection = TrueTypeCollection.read(io)
  list = collection.list_fonts(io)
  list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
end

Parameters:

  • io (IO)

    Open file handle to read fonts from

Returns:

  • (CollectionListInfo)

    List of fonts with metadata



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
195
196
197
198
199
200
201
202
203
# File 'lib/fontisan/base_collection.rb', line 155

def list_fonts(io)
  require_relative "models/collection_list_info"
  require_relative "models/collection_font_summary"
  require_relative "tables/name"

  font_class = self.class.font_class

  fonts = font_offsets.map.with_index do |offset, index|
    font = font_class.from_collection(io, offset)

    # Extract basic font info
    name_table = font.table("name")
    post_table = font.table("post")

    family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
    subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
    postscript_name = name_table&.english_name(Tables::Name::POSTSCRIPT_NAME) || "Unknown"

    # Determine font format
    sfnt = font.header.sfnt_version
    font_format = case sfnt
                  when 0x00010000, 0x74727565 # 0x74727565 = 'true'
                    "TrueType"
                  when 0x4F54544F # 'OTTO'
                    "OpenType"
                  else
                    "Unknown"
                  end

    num_glyphs = post_table&.glyph_names&.length || 0
    num_tables = font.table_names.length

    Models::CollectionFontSummary.new(
      index: index,
      family_name: family_name,
      subfamily_name: subfamily_name,
      postscript_name: postscript_name,
      font_format: font_format,
      num_glyphs: num_glyphs,
      num_tables: num_tables,
    )
  end

  Models::CollectionListInfo.new(
    collection_path: nil, # Will be set by command
    num_fonts: num_fonts,
    fonts: fonts,
  )
end

#valid?Boolean

Validate format correctness

Returns:

  • (Boolean)

    true if the format is valid, false otherwise



121
122
123
124
125
# File 'lib/fontisan/base_collection.rb', line 121

def valid?
  tag == Constants::TTC_TAG && num_fonts.positive? && font_offsets.length == num_fonts
rescue StandardError
  false
end

#versionInteger

Get the collection version as a single integer

Returns:

  • (Integer)

    Version number (e.g., 0x00010000 for version 1.0)



130
131
132
# File 'lib/fontisan/base_collection.rb', line 130

def version
  (major_version << 16) | minor_version
end

#version_stringString

Get the collection version as a string

Returns:

  • (String)

    Version string (e.g., “1.0”)



137
138
139
# File 'lib/fontisan/base_collection.rb', line 137

def version_string
  "#{major_version}.#{minor_version}"
end