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



100
101
102
103
# File 'lib/fontisan/dfont_collection.rb', line 100

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)


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

def num_fonts
  @num_fonts
end

#pathString (readonly)

Path to dfont file

Returns:

  • (String)


56
57
58
# File 'lib/fontisan/dfont_collection.rb', line 56

def path
  @path
end

Class Method Details

.collection_formatString

Get the collection format identifier

Returns:

  • (String)

    "dfont" for dfont collection



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

def self.collection_format
  "dfont"
end

.from_file(path) ⇒ DfontCollection

Load dfont collection from file

Parameters:

  • path (String)

    Path to dfont file

Returns:

Raises:



84
85
86
87
88
89
90
91
92
93
# File 'lib/fontisan/dfont_collection.rb', line 84

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

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

Returns:

  • (Boolean)


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

def collection? = true

#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:



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/fontisan/dfont_collection.rb', line 251

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



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/dfont_collection.rb', line 125

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:



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/fontisan/dfont_collection.rb', line 201

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



68
69
70
# File 'lib/fontisan/dfont_collection.rb', line 68

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

#formatSymbol

High-level pipeline format identifier. Owned by the collection class so the conversion pipeline can dispatch without case statements (OCP).

Returns:

  • (Symbol)

    :dfont



29
# File 'lib/fontisan/dfont_collection.rb', line 29

def format = :dfont

#list_fonts(io) ⇒ Models::CollectionListInfo

List fonts in collection (brief info)

Parameters:

  • io (IO)

    Open file handle

Returns:



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

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

#outline_typeSymbol

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

Returns:

  • (Symbol)

    :unknown



47
# File 'lib/fontisan/dfont_collection.rb', line 47

def outline_type = :unknown

#table_namesArray<String>

Collections have no single SFNT table directory.

Returns:

  • (Array<String>)

    empty



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

def table_names = []

#valid?Boolean

Check if collection is valid

Returns:

  • (Boolean)

    true if valid



108
109
110
# File 'lib/fontisan/dfont_collection.rb', line 108

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

#variation_typeSymbol

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

Returns:

  • (Symbol)

    :static



41
# File 'lib/fontisan/dfont_collection.rb', line 41

def variation_type = :static

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



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

def version_string
  "N/A"
end