Class: Fontisan::OpenTypeCollection

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

Overview

OpenType Collection domain object using BinData

Represents a complete OpenType Collection file (OTC) using BinData’s declarative DSL for binary structure definition. Parallel to TrueTypeCollection but for OpenType fonts.

Examples:

Reading and extracting fonts

File.open("fonts.otc", "rb") do |io|
  otc = OpenTypeCollection.read(io)
  puts otc.num_fonts  # => 4
  fonts = otc.extract_fonts(io)  # => [OpenTypeFont, OpenTypeFont, ...]
end

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.from_file(path) ⇒ OpenTypeCollection

Read OpenType Collection from a file

Parameters:

  • path (String)

    Path to the OTC file

Returns:

Raises:

  • (ArgumentError)

    if path is nil or empty

  • (Errno::ENOENT)

    if file does not exist

  • (RuntimeError)

    if file format is invalid



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/fontisan/open_type_collection.rb', line 34

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 OTC file: #{e.message}"
rescue EOFError => e
  raise "Invalid OTC 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.otc", "rb") do |io|
  otc = OpenTypeCollection.read(io)
  info = otc.collection_info(io, "fonts.otc")
  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



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/fontisan/open_type_collection.rb', line 177

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: "OTC",
    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<OpenTypeFont>

Extract fonts as OpenTypeFont objects

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

Parameters:

  • io (IO)

    Open file handle to read fonts from

Returns:



54
55
56
57
58
59
60
# File 'lib/fontisan/open_type_collection.rb', line 54

def extract_fonts(io)
  require_relative "open_type_font"

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

#font(index, io, mode: LoadingModes::FULL) ⇒ OpenTypeFont?

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:

  • (OpenTypeFont, nil)

    Font object or nil if index out of range



68
69
70
71
72
73
# File 'lib/fontisan/open_type_collection.rb', line 68

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

  require_relative "open_type_font"
  OpenTypeFont.from_collection(io, font_offsets[index], mode: mode)
end

#font_countInteger

Get font count

Returns:

  • (Integer)

    Number of fonts in collection



78
79
80
# File 'lib/fontisan/open_type_collection.rb', line 78

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.otc", "rb") do |io|
  otc = OpenTypeCollection.read(io)
  list = otc.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



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/fontisan/open_type_collection.rb', line 112

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

  fonts = font_offsets.map.with_index do |offset, index|
    font = OpenTypeFont.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



85
86
87
88
89
# File 'lib/fontisan/open_type_collection.rb', line 85

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

#versionInteger

Get the OTC version as a single integer

Returns:

  • (Integer)

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



94
95
96
# File 'lib/fontisan/open_type_collection.rb', line 94

def version
  (major_version << 16) | minor_version
end