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



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

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



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

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



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

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?Boolean

Whether this object represents a font collection rather than a single font. Each font class is the authority on this question.

Returns:

  • (Boolean)


123
# File 'lib/fontisan/base_collection.rb', line 123

def collection? = true

#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



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/fontisan/base_collection.rb', line 241

def collection_info(io, path)
  # 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)



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

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:



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

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



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

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



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
# File 'lib/fontisan/base_collection.rb', line 179

def list_fonts(io)
  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

#outline_typeSymbol

Outline representation. Collections may contain mixed-flavor fonts; per-font outline type requires loading an individual font.

Returns:

  • (Symbol)

    :unknown



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

def outline_type = :unknown

#table_namesArray<String>

Collections have no single SFNT table directory.

Returns:

  • (Array<String>)

    empty



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

def table_names = []

#valid?Boolean

Validate format correctness

Returns:

  • (Boolean)

    true if the format is valid, false otherwise



145
146
147
148
149
# File 'lib/fontisan/base_collection.rb', line 145

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

#variation_typeSymbol

Variation profile. Collections are containers; per-font variation is not exposed at the collection level.

Returns:

  • (Symbol)

    :static



129
# File 'lib/fontisan/base_collection.rb', line 129

def variation_type = :static

#versionInteger

Get the collection version as a single integer

Returns:

  • (Integer)

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



154
155
156
# File 'lib/fontisan/base_collection.rb', line 154

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



161
162
163
# File 'lib/fontisan/base_collection.rb', line 161

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