Class: Fontisan::Converters::Woff2Encoder

Inherits:
Object
  • Object
show all
Includes:
ConversionStrategy
Defined in:
lib/fontisan/converters/woff2_encoder.rb

Overview

WOFF2 encoder conversion strategy

Woff2Encoder implements the ConversionStrategy interface to convert TTF or OTF fonts to WOFF2 format with Brotli compression.

WOFF2 encoding process:

  1. Load configuration settings
  2. Determine font flavor (TTF or CFF)
  3. Collect and order tables
  4. Apply WOFF2 spec encoder rules (drop DSIG, mark head.flags)
  5. Transform tables (placeholder for glyf/loca/hmtx optimization)
  6. Compress all tables with single Brotli stream
  7. Build WOFF2 header and table directory
  8. Assemble complete WOFF2 binary

Examples:

Convert TTF to WOFF2

encoder = Woff2Encoder.new
result = encoder.convert(font)
File.binwrite('font.woff2', result[:woff2_binary])

Constant Summary collapse

SHORT_LOCA_GLYF_MAX =

Highest glyf offset addressable by short loca (uint16 max × 2). Above this, loca MUST use the long format. Public so specs can reference the same threshold the encoder uses.

0x1FFFE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ConversionStrategy

included, #supports?

Constructor Details

#initialize(config_path: nil) ⇒ Woff2Encoder

Initialize encoder with configuration.

The encoder is stateless per call; all conversion knobs come through the per-convert options hash. The config file supplies default values only for fields not expressed via the DSL.

Parameters:

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

    Path to config file



46
47
48
# File 'lib/fontisan/converters/woff2_encoder.rb', line 46

def initialize(config_path: nil)
  @config = load_configuration(config_path)
end

Instance Attribute Details

#configHash (readonly)

Returns Configuration settings.

Returns:

  • (Hash)

    Configuration settings



37
38
39
# File 'lib/fontisan/converters/woff2_encoder.rb', line 37

def config
  @config
end

Instance Method Details

#convert(font, options = {}) ⇒ Hash{Symbol => String}

Convert font to WOFF2 format.

Parameters:

  • font (TrueTypeFont, OpenTypeFont)

    Source font

  • options (Hash{Symbol => Object}) (defaults to: {})

    Per-call options:

    • :brotli_quality (0–11, default from config or 11)
    • :transform_tables (bool, default false; glyf/loca are always transformed per WOFF2 spec section 5.3 regardless of this flag)
    • :quality — legacy alias for :brotli_quality (backward compat)

Returns:

  • (Hash{Symbol => String})

    { woff2_binary: <bytes> }

Raises:

  • (ArgumentError)

    if any option fails validation

  • (Fontisan::Error)

    if encoding fails



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/fontisan/converters/woff2_encoder.rb', line 68

def convert(font, options = {})
  validate(font, :woff2)

  resolved = normalize_legacy_quality(options)
  self.class.validate_options!(strategy_options(resolved))

  quality = resolved.fetch(:brotli_quality) do
    config["brotli"]["quality"]
  end

  flavor = detect_flavor(font)
  table_data = collect_tables(font, options)
  Woff2::EncoderRules.apply!(table_data)

  glyf_transform = apply_glyf_loca_transform!(table_data, font)

  transformer = Woff2::TableTransformer.new(font)
  transform_enabled = resolved.fetch(:transform_tables, false)
  entries, transformed_data = build_table_entries(table_data,
                                                  transformer,
                                                  transform_enabled,
                                                  glyf_transform:)

  # Recompute head.checksumAdjustment for the SFNT the decoder
  # reconstructs. Chrome's OTS validates this strictly; a stale
  # source value fails once glyf/loca/head have been modified.
  # Must run AFTER touch_head_modified! so the modified timestamp
  # change is included in the checksum.
  update_checksum_adjustment!(table_data, flavor:, glyf_transform:)

  compressed_data = compress_tables(entries, table_data,
                                    transformed_data, quality)

  total_sfnt_size = calculate_sfnt_size(table_data,
                                        glyf_transform:)
  header = build_header(
    flavor: flavor,
    num_tables: entries.size,
    total_sfnt_size: total_sfnt_size,
    total_compressed_size: compressed_data.bytesize,
  )

  { woff2_binary: assemble_woff2(header, entries, compressed_data) }
end

#supported_conversionsArray<Array<Symbol>>

Get list of supported conversions

Returns:

  • (Array<Array<Symbol>>)

    Supported conversion pairs



116
117
118
119
120
121
# File 'lib/fontisan/converters/woff2_encoder.rb', line 116

def supported_conversions
  [
    %i[ttf woff2],
    %i[otf woff2],
  ]
end

#validate(font, target_format) ⇒ Boolean

Validate that conversion is possible

Parameters:

Returns:

  • (Boolean)

    True if valid

Raises:

  • (Error)

    If conversion is not possible



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/fontisan/converters/woff2_encoder.rb', line 129

def validate(font, target_format)
  unless target_format == :woff2
    raise Fontisan::Error,
          "Woff2Encoder only supports conversion to woff2, " \
          "got: #{target_format}"
  end

  required_tables = %w[head hhea maxp]
  required_tables.each do |tag|
    unless font.table(tag)
      raise Fontisan::Error,
            "Font is missing required table: #{tag}"
    end
  end

  unless font.has_table?("glyf") || font.has_table?("CFF ") || font.has_table?("CFF2")
    raise Fontisan::Error,
          "Font must have either glyf or CFF/CFF2 table"
  end

  true
end