Class: Fontisan::WoffFont

Inherits:
BinData::Record
  • Object
show all
Includes:
SfntSource
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 (https://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.



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

def compressed_table_data
  @compressed_table_data
end

#decompressed_tablesObject

Table data storage (decompressed on demand)



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

def decompressed_tables
  @decompressed_tables
end

#io_sourceObject

File IO handle for lazy table decompression



79
80
81
# File 'lib/fontisan/woff_font.rb', line 79

def io_source
  @io_source
end

#parsed_tablesObject

Parsed table instances cache



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

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



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/fontisan/woff_font.rb', line 93

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



186
187
188
# File 'lib/fontisan/woff_font.rb', line 186

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

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


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

def collection? = false

#find_table_entry(tag) ⇒ WoffTableDirectoryEntry?

Find a table entry by tag

Parameters:

  • tag (String)

    The table tag to find

Returns:



247
248
249
# File 'lib/fontisan/woff_font.rb', line 247

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

#formatSymbol

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

Returns:

  • (Symbol)

    :woff



62
# File 'lib/fontisan/woff_font.rb', line 62

def format = :woff

#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



239
240
241
# File 'lib/fontisan/woff_font.rb', line 239

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

#initialize_storagevoid

This method returns an undefined value.

Initialize storage hashes



116
117
118
119
120
# File 'lib/fontisan/woff_font.rb', line 116

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



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/fontisan/woff_font.rb', line 285

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

#outline_typeSymbol

Outline representation, derived from the wrapped SFNT flavor.

Returns:

  • (Symbol)

    :truetype or :cff



172
173
174
# File 'lib/fontisan/woff_font.rb', line 172

def outline_type
  truetype? ? :truetype : :cff
end

#private_dataString?

Get WOFF private data if present

WOFF private data is optional application-specific data

Returns:

  • (String, nil)

    Private data or nil



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

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



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

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



268
269
270
# File 'lib/fontisan/woff_font.rb', line 268

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



196
197
198
199
200
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/woff_font.rb', line 196

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 plain UTF-8 Ruby Strings so callers can compare with literals without BinData::String encoding-sensitive eql? failures.

Returns:

  • (Array<String>)

    Array of table tag strings (UTF-8)



257
258
259
# File 'lib/fontisan/woff_font.rb', line 257

def table_names
  table_entries.map { |entry| entry.tag.to_s.force_encoding("UTF-8") }
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:



349
350
351
352
353
354
355
356
# File 'lib/fontisan/woff_font.rb', line 349

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:



333
334
335
336
337
338
339
340
# File 'lib/fontisan/woff_font.rb', line 333

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



179
180
181
# File 'lib/fontisan/woff_font.rb', line 179

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



275
276
277
278
# File 'lib/fontisan/woff_font.rb', line 275

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



361
362
363
364
365
366
367
368
# File 'lib/fontisan/woff_font.rb', line 361

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



126
127
128
129
130
131
132
133
# File 'lib/fontisan/woff_font.rb', line 126

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

#variation_typeSymbol

Variation profile. WOFF wraps a single SFNT font; variation tables, if present on the wrapped font, are reported here.

Returns:

  • (Symbol)

    :static, :gvar, or :cff2



161
162
163
164
165
166
167
# File 'lib/fontisan/woff_font.rb', line 161

def variation_type
  return :static unless has_table?("fvar")
  return :gvar if has_table?("gvar")
  return :cff2 if has_table?("CFF2")

  :static
end