Class: Fontisan::BaseCollection Abstract

Inherits:
BinData::Record
  • Object
show all
Includes:
Collection::SharedLogic
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

Methods included from Collection::SharedLogic

#calculate_table_sharing_for_fonts

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



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

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



50
51
52
53
# File 'lib/fontisan/base_collection.rb', line 50

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



73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/fontisan/base_collection.rb', line 73

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



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/fontisan/base_collection.rb', line 224

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)



93
94
95
96
97
98
99
# File 'lib/fontisan/base_collection.rb', line 93

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:



107
108
109
110
111
112
# File 'lib/fontisan/base_collection.rb', line 107

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



117
118
119
# File 'lib/fontisan/base_collection.rb', line 117

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



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
204
205
206
# File 'lib/fontisan/base_collection.rb', line 158

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



124
125
126
127
128
# File 'lib/fontisan/base_collection.rb', line 124

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)



133
134
135
# File 'lib/fontisan/base_collection.rb', line 133

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”)



140
141
142
# File 'lib/fontisan/base_collection.rb', line 140

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