Class: Fontisan::Woff2::EncoderRules

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

Overview

Applies WOFF2 spec "encoder MUST" rules to a table collection before the table directory is built and brotli compression is applied.

Each spec-mandated transform lives here so the encoder stays a thin orchestrator and new rules are MECE/discoverable instead of sprinkled as inline conditionals across Woff2Encoder.

Reference: W3C WOFF2 spec, section 5 ("Recommended table directory").

Constant Summary collapse

EXCLUDED_TABLES =

Tables that MUST NOT appear in WOFF2 output.

Per spec section 5: "The compliant WOFF2 encoder MUST remove the DSIG table from an input font data, prior to applying transformations and entropy coding steps." Chrome's OpenType Sanitizer (OTS) rejects WOFF2 files that still contain DSIG.

%w[DSIG].freeze
TRANSFORMED_TABLES =

Tables that MUST be transformed (when present) per WOFF2 spec section 5.3: glyf and loca are paired — both are either transformed (version 0) or null-transformed (version 3) together. Browsers (Chrome OTS) only accept the transformed form.

%w[glyf loca].freeze

Class Method Summary collapse

Class Method Details

.apply!(table_data) ⇒ Hash{String => String}

Apply all WOFF2 encoder rules to table_data in place.

Parameters:

  • table_data (Hash{String => String})

    Map of tag → binary table data. Mutated.

Returns:

  • (Hash{String => String})

    The same hash, for chaining.



33
34
35
36
37
38
# File 'lib/fontisan/woff2/encoder_rules.rb', line 33

def self.apply!(table_data)
  exclude_tables!(table_data)
  mark_lossless_modifying!(table_data)
  touch_head_modified!(table_data)
  table_data
end

.exclude_tables!(table_data) ⇒ Object

Drop spec-excluded tables from the collection.



46
47
48
# File 'lib/fontisan/woff2/encoder_rules.rb', line 46

def self.exclude_tables!(table_data)
  EXCLUDED_TABLES.each { |tag| table_data.delete(tag) }
end

.excluded?(tag) ⇒ Boolean

Whether tag is dropped by WOFF2 spec rules.

Returns:

  • (Boolean)


41
42
43
# File 'lib/fontisan/woff2/encoder_rules.rb', line 41

def self.excluded?(tag)
  EXCLUDED_TABLES.include?(tag)
end

.mark_lossless_modifying!(table_data) ⇒ Object

Set head.flags bit 11 to indicate the font was losslessly modified.

Per spec section 5: "The WOFF 2.0 encoders MUST also set bit 11 of the 'flags' field of the head table ... to indicate that a recreated font file was subjected to lossless modifying transform."

Uses the model-driven Head BinData record so the bits are set via a named attribute on a typed object, not via raw byte slicing.



58
59
60
61
62
63
64
# File 'lib/fontisan/woff2/encoder_rules.rb', line 58

def self.mark_lossless_modifying!(table_data)
  return unless table_data.key?("head")

  head = Tables::Head.read(table_data["head"])
  head.flags |= Tables::Head::FLAG_LOSSLESS_MODIFYING
  table_data["head"] = head.to_binary_s
end

.set_head_index_to_loc_format!(table_data, format_code) ⇒ Object

Set head.indexToLocFormat to match the chosen output loca format.

fontTools' WOFF2 encoder re-picks the loca format (preferring short when glyf fits). The reconstructed SFNT must carry the matching indexToLocFormat in head, or Chrome's OTS rejects the file with "Failed to convert WOFF 2.0 font to SFNT".

Parameters:

  • table_data (Hash{String => String})
  • format_code (Integer)

    0 (short) or 1 (long)



75
76
77
78
79
80
81
# File 'lib/fontisan/woff2/encoder_rules.rb', line 75

def self.set_head_index_to_loc_format!(table_data, format_code)
  return unless table_data.key?("head")

  head = Tables::Head.read(table_data["head"])
  head.index_to_loc_format = format_code
  table_data["head"] = head.to_binary_s
end

.touch_head_modified!(table_data) ⇒ Object

Set head.modified to the current time.

Chrome's OTS rejects WOFF2 fonts whose head.modified is not meaningfully later than head.created. Source fonts frequently carry modified == created (or modified within seconds of it), which Chrome rejects. Setting modified to the actual current wall-clock time matches fontTools' behaviour (it sets head.modified = timestampNow() on every save) and gives Chrome a delta large enough to accept.

Must be called BEFORE checksum recompute so the modified bytes are included in the checksum.

Parameters:

  • table_data (Hash{String => String})


97
98
99
100
101
102
103
# File 'lib/fontisan/woff2/encoder_rules.rb', line 97

def self.touch_head_modified!(table_data)
  return unless table_data.key?("head")

  head = Tables::Head.read(table_data["head"])
  head.modified_raw = Tables::Head.now_longdatetime
  table_data["head"] = head.to_binary_s
end