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) ⇒ WoffFont

Read WOFF font from a file

Parameters:

  • path (String)

    Path to the WOFF file

Returns:

Raises:

  • (ArgumentError)

    if path is nil or empty

  • (Errno::ENOENT)

    if file does not exist

  • (InvalidFontError)

    if file format is invalid



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

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 = read(io)
    font.validate_signature!
    font.initialize_storage
    font.io_source = io
    font.read_compressed_table_data(io)
    font
  end
rescue BinData::ValidityError, EOFError => e
  Kernel.raise(::Fontisan::InvalidFontError,
               "Invalid WOFF 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



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

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:



203
204
205
# File 'lib/fontisan/woff_font.rb', line 203

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



195
196
197
# File 'lib/fontisan/woff_font.rb', line 195

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

#initialize_storagevoid

This method returns an undefined value.

Initialize storage hashes



108
109
110
111
112
# File 'lib/fontisan/woff_font.rb', line 108

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



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/fontisan/woff_font.rb', line 238

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



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/fontisan/woff_font.rb', line 266

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



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

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



221
222
223
# File 'lib/fontisan/woff_font.rb', line 221

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

#table_data(tag) ⇒ String?

Get decompressed table data

Decompresses table data on first access and caches result

Parameters:

  • tag (String)

    The table tag

Returns:

  • (String, nil)

    Decompressed table data or nil if not found



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

def table_data(tag)
  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



210
211
212
# File 'lib/fontisan/woff_font.rb', line 210

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:



302
303
304
305
306
307
308
309
# File 'lib/fontisan/woff_font.rb', line 302

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:



286
287
288
289
290
291
292
293
# File 'lib/fontisan/woff_font.rb', line 286

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



146
147
148
# File 'lib/fontisan/woff_font.rb', line 146

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



228
229
230
231
# File 'lib/fontisan/woff_font.rb', line 228

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



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

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:



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

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