Class: Fontisan::Woff2Font

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/woff2_font.rb

Overview

Web Open Font Format 2.0 (WOFF2) font domain object

Represents a WOFF2 font file that uses Brotli compression and table transformations. WOFF2 is significantly more complex than WOFF.

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

  • Tables can be transformed (glyf, loca, hmtx have special formats)

  • All compressed data in a single Brotli stream

  • Variable-length integer encoding (UIntBase128, 255UInt16)

  • More efficient compression than WOFF

Examples:

Reading a WOFF2 font

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

Converting to TTF/OTF

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

Constant Summary collapse

WOFF2_SIGNATURE =

WOFF2 signature constant

0x774F4632

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeWoff2Font

Returns a new instance of Woff2Font.



142
143
144
145
146
147
148
# File 'lib/fontisan/woff2_font.rb', line 142

def initialize
  @header = nil
  @table_entries = []
  @decompressed_tables = {}
  @parsed_tables = {}
  @io_source = nil
end

Instance Attribute Details

#decompressed_tablesObject

Returns the value of attribute decompressed_tables.



110
111
112
# File 'lib/fontisan/woff2_font.rb', line 110

def decompressed_tables
  @decompressed_tables
end

#headerObject

Returns the value of attribute header.



110
111
112
# File 'lib/fontisan/woff2_font.rb', line 110

def header
  @header
end

#io_sourceObject

Returns the value of attribute io_source.



110
111
112
# File 'lib/fontisan/woff2_font.rb', line 110

def io_source
  @io_source
end

#parsed_tablesObject

Returns the value of attribute parsed_tables.



110
111
112
# File 'lib/fontisan/woff2_font.rb', line 110

def parsed_tables
  @parsed_tables
end

#table_entriesObject

Returns the value of attribute table_entries.



110
111
112
# File 'lib/fontisan/woff2_font.rb', line 110

def table_entries
  @table_entries
end

Class Method Details

.from_file(path) ⇒ Woff2Font

Read WOFF2 font from a file

Parameters:

  • path (String)

    Path to the WOFF2 file

Returns:

Raises:

  • (ArgumentError)

    if path is nil or empty

  • (Errno::ENOENT)

    if file does not exist

  • (InvalidFontError)

    if file format is invalid



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/fontisan/woff2_font.rb', line 123

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") do |io|
    font = new
    font.read_from_io(io)
    font.validate_signature!
    font.initialize_storage
    font.decompress_and_parse_tables(io)
    font.io_source = io
    font
  end
rescue BinData::ValidityError, EOFError => e
  raise InvalidFontError, "Invalid WOFF2 file: #{e.message}"
end

Instance Method Details

#cff?Boolean

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

Returns:

  • (Boolean)

    true if CFF, false if TrueType



190
191
192
# File 'lib/fontisan/woff2_font.rb', line 190

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

#find_table_entry(tag) ⇒ Woff2TableDirectoryEntry?

Find a table entry by tag

Parameters:

  • tag (String)

    The table tag to find

Returns:



216
217
218
# File 'lib/fontisan/woff2_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/woff2_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



162
163
164
165
# File 'lib/fontisan/woff2_font.rb', line 162

def initialize_storage
  @decompressed_tables ||= {}
  @initialize_storage ||= {}
end

#metadataString?

Get WOFF2 metadata if present

Returns:

  • (String, nil)

    Decompressed metadata XML or nil



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

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 = Brotli.inflate(compressed_meta)

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

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

#read_from_io(io) ⇒ void

This method returns an undefined value.

Read header and table directory from IO

Parameters:

  • io (IO)

    Open file handle



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

def read_from_io(io)
  @header = Woff2Header.read(io)
  read_table_directory(io)
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/woff2_font.rb', line 234

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

#table_data(tag) ⇒ String?

Get decompressed table data

Provides unified interface compatible with WoffFont

Parameters:

  • tag (String)

    The table tag

Returns:

  • (String, nil)

    Decompressed table data or nil if not found



200
201
202
# File 'lib/fontisan/woff2_font.rb', line 200

def table_data(tag)
  @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/woff2_font.rb', line 223

def table_names
  table_entries.map(&:tag)
end

#to_otf(output_path) ⇒ Integer

Convert WOFF2 to OTF format

Decompresses and reconstructs tables, then builds a standard OTF file

Parameters:

  • output_path (String)

    Path where OTF file will be written

Returns:

  • (Integer)

    Number of bytes written

Raises:



294
295
296
297
298
299
300
301
# File 'lib/fontisan/woff2_font.rb', line 294

def to_otf(output_path)
  unless cff?
    raise 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 WOFF2 to TTF format

Decompresses and reconstructs tables, then builds a standard TTF file

Parameters:

  • output_path (String)

    Path where TTF file will be written

Returns:

  • (Integer)

    Number of bytes written

Raises:



278
279
280
281
282
283
284
285
# File 'lib/fontisan/woff2_font.rb', line 278

def to_ttf(output_path)
  unless truetype?
    raise 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



183
184
185
# File 'lib/fontisan/woff2_font.rb', line 183

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/woff2_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 WOFF2 format is valid, false otherwise



306
307
308
309
310
311
312
313
314
# File 'lib/fontisan/woff2_font.rb', line 306

def valid?
  return false unless header
  return false unless header.signature == WOFF2_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 WOFF2 signature

Raises:



171
172
173
174
175
176
177
178
# File 'lib/fontisan/woff2_font.rb', line 171

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