Class: Fontisan::DfontCollection

Inherits:
Object
  • Object
show all
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

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



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

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)


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

def num_fonts
  @num_fonts
end

#pathString (readonly)

Path to dfont file

Returns:

  • (String)


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

def path
  @path
end

Class Method Details

.from_file(path) ⇒ DfontCollection

Load dfont collection from file

Parameters:

  • path (String)

    Path to dfont file

Returns:

Raises:



40
41
42
43
44
45
46
47
48
49
# File 'lib/fontisan/dfont_collection.rb', line 40

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

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

Extract all fonts from dfont

Parameters:

  • io (IO)

    Open file handle

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/fontisan/dfont_collection.rb', line 72

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:



151
152
153
154
155
156
157
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
# File 'lib/fontisan/dfont_collection.rb', line 151

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

#list_fonts(io) ⇒ Models::CollectionListInfo

List fonts in collection (brief info)

Parameters:

  • io (IO)

    Open file handle

Returns:



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

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



64
65
66
# File 'lib/fontisan/dfont_collection.rb', line 64

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