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 (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.



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

def compressed_table_data
  @compressed_table_data
end

#decompressed_tablesObject

Table data storage (decompressed on demand)



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

def decompressed_tables
  @decompressed_tables
end

#io_sourceObject

File IO handle for lazy table decompression



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

def io_source
  @io_source
end

#parsed_tablesObject

Parsed table instances cache



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

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



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

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



184
185
186
# File 'lib/fontisan/woff_font.rb', line 184

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)


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

def collection? = false

#find_table_entry(tag) ⇒ WoffTableDirectoryEntry?

Find a table entry by tag

Parameters:

  • tag (String)

    The table tag to find

Returns:



245
246
247
# File 'lib/fontisan/woff_font.rb', line 245

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



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

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



237
238
239
# File 'lib/fontisan/woff_font.rb', line 237

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

#initialize_storagevoid

This method returns an undefined value.

Initialize storage hashes



114
115
116
117
118
# File 'lib/fontisan/woff_font.rb', line 114

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



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

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



170
171
172
# File 'lib/fontisan/woff_font.rb', line 170

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



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

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



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

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



266
267
268
# File 'lib/fontisan/woff_font.rb', line 266

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



194
195
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
# File 'lib/fontisan/woff_font.rb', line 194

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)



255
256
257
# File 'lib/fontisan/woff_font.rb', line 255

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:



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

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:



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

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



177
178
179
# File 'lib/fontisan/woff_font.rb', line 177

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



273
274
275
276
# File 'lib/fontisan/woff_font.rb', line 273

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



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

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:



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

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



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

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

  :static
end