Class: Fontisan::WoffFont

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

Overview

Web Open Font Format (WOFF) font domain object

Represents a WOFF font file that uses zlib compression for table data. WOFF is a simple wrapper format for TTF/OTF fonts with compression.

According to the WOFF specification (www.w3.org/TR/WOFF/):

  • Tables are individually compressed using zlib

  • Optional metadata block (compressed XML)

  • Optional private data block

Examples:

Reading a WOFF font

woff = WoffFont.from_file("font.woff")
puts woff.header.num_tables
name_table = woff.table("name")
puts name_table.english_name(Tables::Name::FAMILY)

Converting to TTF/OTF

woff = WoffFont.from_file("font.woff")
woff.to_ttf("output.ttf")  # if TrueType flavored
woff.to_otf("output.otf")  # if CFF flavored

Constant Summary collapse

WOFF_SIGNATURE =

WOFF signature constant

0x774F4646

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#compressed_table_dataObject

Returns the value of attribute compressed_table_data.



67
68
69
# File 'lib/fontisan/woff_font.rb', line 67

def compressed_table_data
  @compressed_table_data
end

#decompressed_tablesObject

Table data storage (decompressed on demand)



66
67
68
# File 'lib/fontisan/woff_font.rb', line 66

def decompressed_tables
  @decompressed_tables
end

#io_sourceObject

File IO handle for lazy table decompression



73
74
75
# File 'lib/fontisan/woff_font.rb', line 73

def io_source
  @io_source
end

#parsed_tablesObject

Parsed table instances cache



70
71
72
# File 'lib/fontisan/woff_font.rb', line 70

def parsed_tables
  @parsed_tables
end

Class Method Details

.from_file(path, mode: LoadingModes::FULL, lazy: false) ⇒ WoffFont

Read WOFF font from a file

Parameters:

  • path (String)

    Path to the WOFF file

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

    Loading mode (:metadata or :full) - currently ignored, loads all tables

  • lazy (Boolean) (defaults to: false)

    Lazy loading flag - currently ignored, always eager

Returns:

Raises:

  • (ArgumentError)

    if path is nil or empty

  • (Errno::ENOENT)

    if file does not exist

  • (InvalidFontError)

    if file format is invalid



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/fontisan/woff_font.rb', line 87

def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
  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") do |io|
    font = read(io)
    font.validate_signature!
    font.initialize_storage
    font.io_source = io
    font.read_compressed_table_data(io)
    font
  rescue BinData::ValidityError, EOFError => e
    Kernel.raise(::Fontisan::InvalidFontError,
                 "Invalid WOFF file: #{e.message}")
  end
end

Instance Method Details

#cff?Boolean

Check if font is CFF flavored (OpenType with CFF outlines)

Returns:

  • (Boolean)

    true if CFF, false if TrueType



155
156
157
# File 'lib/fontisan/woff_font.rb', line 155

def cff?
  [Constants::SFNT_VERSION_OTTO, 0x4F54544F].include?(header.flavor) # 'OTTO'
end

#find_table_entry(tag) ⇒ WoffTableDirectoryEntry?

Find a table entry by tag

Parameters:

  • tag (String)

    The table tag to find

Returns:



216
217
218
# File 'lib/fontisan/woff_font.rb', line 216

def find_table_entry(tag)
  table_entries.find { |entry| entry.tag == tag }
end

#has_table?(tag) ⇒ Boolean

Check if font has a specific table

Parameters:

  • tag (String)

    The table tag to check for

Returns:

  • (Boolean)

    true if table exists, false otherwise



208
209
210
# File 'lib/fontisan/woff_font.rb', line 208

def has_table?(tag)
  table_entries.any? { |entry| entry.tag == tag }
end

#initialize_storagevoid

This method returns an undefined value.

Initialize storage hashes



110
111
112
113
114
# File 'lib/fontisan/woff_font.rb', line 110

def initialize_storage
  @decompressed_tables = {}
  @compressed_table_data = {}
  @parsed_tables = {}
end

#metadataString?

Get WOFF metadata if present

WOFF metadata is optional compressed XML describing the font

Returns:

  • (String, nil)

    Decompressed metadata XML or nil



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

def 
  return nil if header.meta_length.zero?
  return @metadata if defined?(@metadata)

  File.open(io_source.path, "rb") do |io|
    io.seek(header.meta_offset)
    compressed_meta = io.read(header.meta_length)
    @metadata = Zlib::Inflate.inflate(compressed_meta)

    # Verify decompressed size
    if @metadata.bytesize != header.meta_orig_length
      Kernel.raise(::Fontisan::InvalidFontError,
                   "Metadata size mismatch: expected #{header.meta_orig_length}, " \
                   "got #{@metadata.bytesize}")
    end

    @metadata
  end
rescue StandardError => e
  warn "Failed to decompress WOFF metadata: #{e.message}"
  @metadata = nil
end

#private_dataString?

Get WOFF private data if present

WOFF private data is optional application-specific data

Returns:

  • (String, nil)

    Private data or nil



279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/fontisan/woff_font.rb', line 279

def private_data
  return nil if header.priv_length.zero?
  return @private_data if defined?(@private_data)

  File.open(io_source.path, "rb") do |io|
    io.seek(header.priv_offset)
    @private_data = io.read(header.priv_length)
  end
rescue StandardError => e
  warn "Failed to read WOFF private data: #{e.message}"
  @private_data = nil
end

#read_compressed_table_data(io) ⇒ void

This method returns an undefined value.

Read compressed table data for all tables

Tables are decompressed on-demand for efficiency

Parameters:

  • io (IO)

    Open file handle



135
136
137
138
139
140
141
142
143
# File 'lib/fontisan/woff_font.rb', line 135

def read_compressed_table_data(io)
  @compressed_table_data = {}
  table_entries.each do |entry|
    io.seek(entry.offset)
    # Force UTF-8 encoding on tag for hash key consistency
    tag_key = entry.tag.dup.force_encoding("UTF-8")
    @compressed_table_data[tag_key] = io.read(entry.comp_length)
  end
end

#table(tag) ⇒ Tables::*?

Get parsed table instance

This method decompresses and parses the raw table data into a structured table object and caches the result for subsequent calls.

Parameters:

  • tag (String)

    The table tag to retrieve

Returns:

  • (Tables::*, nil)

    Parsed table object or nil if not found



234
235
236
# File 'lib/fontisan/woff_font.rb', line 234

def table(tag)
  @parsed_tables[tag] ||= parse_table(tag)
end

#table_data(tag = nil) ⇒ String, ...

Get decompressed table data

Decompresses table data on first access and caches result

Parameters:

  • tag (String, nil) (defaults to: nil)

    The table tag (optional)

Returns:

  • (String, Hash, nil)

    Decompressed table data, hash of all tables, or nil if not found



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
193
194
195
196
197
198
199
200
201
202
# File 'lib/fontisan/woff_font.rb', line 165

def table_data(tag = nil)
  # If no tag provided, return all tables
  if tag.nil?
    # Decompress all tables and return as hash
    result = {}
    @compressed_table_data.each_key do |table_tag|
      result[table_tag] = table_data(table_tag)
    end
    return result
  end

  # Tag provided - return specific table
  return @decompressed_tables[tag] if @decompressed_tables.key?(tag)

  compressed_data = @compressed_table_data[tag]
  return nil unless compressed_data

  entry = find_table_entry(tag)
  return nil unless entry

  # Decompress if compressed (comp_length != orig_length)
  @decompressed_tables[tag] = if entry.comp_length == entry.orig_length
                                # Table is not compressed
                                compressed_data
                              else
                                # Decompress using zlib
                                Zlib::Inflate.inflate(compressed_data)
                              end

  # Verify decompressed size matches expected
  if @decompressed_tables[tag].bytesize != entry.orig_length
    Kernel.raise(::Fontisan::InvalidFontError,
                 "Decompressed table '#{tag}' size mismatch: " \
                 "expected #{entry.orig_length}, got #{@decompressed_tables[tag].bytesize}")
  end

  @decompressed_tables[tag]
end

#table_namesArray<String>

Get list of all table tags

Returns:

  • (Array<String>)

    Array of table tag strings



223
224
225
# File 'lib/fontisan/woff_font.rb', line 223

def table_names
  table_entries.map(&:tag)
end

#to_otf(output_path) ⇒ Integer

Convert WOFF to OTF format

Decompresses all tables and reconstructs a standard OTF file

Parameters:

  • output_path (String)

    Path where OTF file will be written

Returns:

  • (Integer)

    Number of bytes written

Raises:



315
316
317
318
319
320
321
322
# File 'lib/fontisan/woff_font.rb', line 315

def to_otf(output_path)
  unless cff?
    Kernel.raise(::Fontisan::InvalidFontError,
                 "Cannot convert to OTF: font is TrueType flavored (use to_ttf)")
  end

  build_sfnt_font(output_path, Constants::SFNT_VERSION_OTTO)
end

#to_ttf(output_path) ⇒ Integer

Convert WOFF to TTF format

Decompresses all tables and reconstructs a standard TTF file

Parameters:

  • output_path (String)

    Path where TTF file will be written

Returns:

  • (Integer)

    Number of bytes written

Raises:



299
300
301
302
303
304
305
306
# File 'lib/fontisan/woff_font.rb', line 299

def to_ttf(output_path)
  unless truetype?
    Kernel.raise(::Fontisan::InvalidFontError,
                 "Cannot convert to TTF: font is CFF flavored (use to_otf)")
  end

  build_sfnt_font(output_path, Constants::SFNT_VERSION_TRUETYPE)
end

#truetype?Boolean

Check if font is TrueType flavored

Returns:

  • (Boolean)

    true if TrueType, false if CFF



148
149
150
# File 'lib/fontisan/woff_font.rb', line 148

def truetype?
  [Constants::SFNT_VERSION_TRUETYPE, 0x00010000].include?(header.flavor)
end

#units_per_emInteger?

Get units per em from head table

Returns:

  • (Integer, nil)

    Units per em value



241
242
243
244
# File 'lib/fontisan/woff_font.rb', line 241

def units_per_em
  head = table(Constants::HEAD_TAG)
  head&.units_per_em
end

#valid?Boolean

Validate format correctness

Returns:

  • (Boolean)

    true if the WOFF format is valid, false otherwise



327
328
329
330
331
332
333
334
335
# File 'lib/fontisan/woff_font.rb', line 327

def valid?
  return false unless header
  return false unless header.signature == WOFF_SIGNATURE
  return false unless table_entries.respond_to?(:length)
  return false if table_entries.length != header.num_tables
  return false unless has_table?(Constants::HEAD_TAG)

  true
end

#validate_signature!void

This method returns an undefined value.

Validate WOFF signature

Raises:



120
121
122
123
124
125
126
127
# File 'lib/fontisan/woff_font.rb', line 120

def validate_signature!
  signature_value = header.signature.to_i
  unless signature_value == WOFF_SIGNATURE
    Kernel.raise(::Fontisan::InvalidFontError,
                 "Invalid WOFF signature: expected 0x#{WOFF_SIGNATURE.to_s(16)}, " \
                 "got 0x#{signature_value.to_s(16)}")
  end
end