Class: Fontisan::DfontCollection

Inherits:
Object
  • Object
show all
Includes:
Collection::SharedLogic
Defined in:
lib/fontisan/dfont_collection.rb

Overview

DfontCollection represents an Apple dfont suitcase containing multiple fonts

dfont (Data Fork Font) is an Apple-specific format that stores Mac font suitcase resources in the data fork. It can contain multiple SFNT fonts (TrueType or OpenType).

This class provides a collection interface similar to TrueTypeCollection and OpenTypeCollection for consistency.

Examples:

Load dfont collection

collection = DfontCollection.from_file("family.dfont")
puts "Collection has #{collection.num_fonts} fonts"

Extract fonts from dfont

File.open("family.dfont", "rb") do |io|
  fonts = collection.extract_fonts(io)
  fonts.each { |font| puts font.class.name }
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Collection::SharedLogic

#calculate_table_sharing_for_fonts

Constructor Details

#initialize(path, num_fonts) ⇒ DfontCollection

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initialize collection

Parameters:

  • path (String)

    Path to dfont file

  • num_fonts (Integer)

    Number of fonts



75
76
77
78
# File 'lib/fontisan/dfont_collection.rb', line 75

def initialize(path, num_fonts)
  @path = path
  @num_fonts = num_fonts
end

Instance Attribute Details

#num_fontsInteger (readonly) Also known as: font_count

Number of fonts in collection

Returns:

  • (Integer)


35
36
37
# File 'lib/fontisan/dfont_collection.rb', line 35

def num_fonts
  @num_fonts
end

#pathString (readonly)

Path to dfont file

Returns:

  • (String)


31
32
33
# File 'lib/fontisan/dfont_collection.rb', line 31

def path
  @path
end

Class Method Details

.collection_formatString

Get the collection format identifier

Returns:

  • (String)

    “dfont” for dfont collection



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

def self.collection_format
  "dfont"
end

.from_file(path) ⇒ DfontCollection

Load dfont collection from file

Parameters:

  • path (String)

    Path to dfont file

Returns:

Raises:



59
60
61
62
63
64
65
66
67
68
# File 'lib/fontisan/dfont_collection.rb', line 59

def self.from_file(path)
  File.open(path, "rb") do |io|
    unless Parsers::DfontParser.dfont?(io)
      raise InvalidFontError, "Not a valid dfont file: #{path}"
    end

    num_fonts = Parsers::DfontParser.sfnt_count(io)
    new(path, num_fonts)
  end
end

Instance Method Details

#collection_info(io, path) ⇒ Models::CollectionInfo

Get comprehensive collection metadata

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

Examples:

Get collection info

File.open("family.dfont", "rb") do |io|
  collection = DfontCollection.from_file("family.dfont")
  info = collection.collection_info(io, "family.dfont")
  puts "Format: #{info.collection_format}"
end

Parameters:

  • io (IO)

    Open file handle to read fonts from

  • path (String)

    Collection file path (for file size)

Returns:



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/fontisan/dfont_collection.rb', line 229

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: "dfnt", # dfont doesn't use ttcf tag
    major_version: 0, # dfont doesn't have version
    minor_version: 0,
    num_fonts: @num_fonts,
    font_offsets: font_offsets,
    file_size_bytes: file_size,
    table_sharing: table_sharing,
  )
end

#extract_fonts(io) ⇒ Array<TrueTypeFont, OpenTypeFont>

Extract all fonts from dfont

Parameters:

  • io (IO)

    Open file handle

Returns:



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/fontisan/dfont_collection.rb', line 100

def extract_fonts(io)
  require "stringio"

  fonts = []

  @num_fonts.times do |index|
    io.rewind
    sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: index)

    # Load font from SFNT binary
    sfnt_io = StringIO.new(sfnt_data)
    signature = sfnt_io.read(4)
    sfnt_io.rewind

    # Create font based on signature
    font = case signature
           when [Constants::SFNT_VERSION_TRUETYPE].pack("N"), "true"
             TrueTypeFont.read(sfnt_io)
           when "OTTO"
             OpenTypeFont.read(sfnt_io)
           else
             raise InvalidFontError,
                   "Invalid SFNT signature in dfont at index #{index}: #{signature.inspect}"
           end

    font.initialize_storage
    font.loading_mode = LoadingModes::FULL
    font.lazy_load_enabled = false
    font.read_table_data(sfnt_io)

    fonts << font
  end

  fonts
end

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

Get specific font from collection

Parameters:

  • index (Integer)

    Font index

  • io (IO)

    Open file handle

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

    Loading mode

Returns:

Raises:



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

def font(index, io, mode: LoadingModes::FULL)
  if index >= @num_fonts
    raise InvalidFontError,
          "Font index #{index} out of range (collection has #{@num_fonts} fonts)"
  end

  io.rewind
  sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: index)

  # Load font from SFNT binary
  require "stringio"
  sfnt_io = StringIO.new(sfnt_data)
  signature = sfnt_io.read(4)
  sfnt_io.rewind

  # Create font based on signature
  font = case signature
         when [Constants::SFNT_VERSION_TRUETYPE].pack("N"), "true"
           TrueTypeFont.read(sfnt_io)
         when "OTTO"
           OpenTypeFont.read(sfnt_io)
         else
           raise InvalidFontError,
                 "Invalid SFNT signature: #{signature.inspect}"
         end

  font.initialize_storage
  font.loading_mode = mode
  font.lazy_load_enabled = false
  font.read_table_data(sfnt_io)

  font
end

#font_offsetsArray<Integer>

Get font offsets (indices for dfont)

dfont doesn’t use byte offsets like TTC/OTC, so we return indices

Returns:

  • (Array<Integer>)

    Array of font indices



43
44
45
# File 'lib/fontisan/dfont_collection.rb', line 43

def font_offsets
  (0...@num_fonts).to_a
end

#list_fonts(io) ⇒ Models::CollectionListInfo

List fonts in collection (brief info)

Parameters:

  • io (IO)

    Open file handle

Returns:



140
141
142
143
144
145
146
147
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/dfont_collection.rb', line 140

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

  fonts = extract_fonts(io)

  summaries = fonts.map.with_index do |font, index|
    name_table = font.table("name")
    family = name_table.english_name(Models::Tables::Name::FAMILY) || "Unknown"
    subfamily = name_table.english_name(Models::Tables::Name::SUBFAMILY) || "Regular"

    # Detect font format
    format = if font.has_table?("CFF ") || font.has_table?("CFF2")
               "OpenType"
             else
               "TrueType"
             end

    Models::CollectionFontSummary.new(
      index: index,
      family_name: family,
      subfamily_name: subfamily,
      font_format: format,
    )
  end

  Models::CollectionListInfo.new(
    num_fonts: @num_fonts,
    fonts: summaries,
  )
end

#valid?Boolean

Check if collection is valid

Returns:

  • (Boolean)

    true if valid



83
84
85
# File 'lib/fontisan/dfont_collection.rb', line 83

def valid?
  File.exist?(@path) && @num_fonts.positive?
end

#version_stringString

Get the collection version as a string

dfont files don’t have version numbers like TTC/OTC

Returns:

  • (String)

    Version string (always “N/A” for dfont)



92
93
94
# File 'lib/fontisan/dfont_collection.rb', line 92

def version_string
  "N/A"
end