Module: Fontisan::SfntBuilder

Defined in:
lib/fontisan/sfnt_builder.rb

Overview

Builds an SFNT (TrueType/OpenType) binary from a flavor + per-tag table data. Used whenever WOFF2-derived table data needs to be reassembled into a parseable SFNT (single-font decoding, WOFF2 collection → font).

Single source of truth for the SFNT byte layout: 12-byte offset table, 16-byte table directory entries, padded table data.

Class Method Summary collapse

Class Method Details

.build(flavor:, tables:) ⇒ String

Returns SFNT binary.

Parameters:

  • flavor (Integer)

    SFNT version (Constants::SFNT_VERSION_TRUETYPE or Constants::SFNT_VERSION_OTTO)

  • tables (Hash<String => String>)

    tag → table bytes

Returns:

  • (String)

    SFNT binary



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/fontisan/sfnt_builder.rb', line 17

def self.build(flavor:, tables:)
  num_tables = tables.length
  entry_selector = (Math.log(num_tables) / Math.log(2)).floor
  search_range = (2**entry_selector) * 16
  range_shift = (num_tables * 16) - search_range

  out = String.new(encoding: Encoding::BINARY)
  # Offset table (12 bytes)
  out << [flavor].pack("N")
  out << [num_tables].pack("n")
  out << [search_range].pack("n")
  out << [entry_selector].pack("n")
  out << [range_shift].pack("n")

  # Compute layout for table directory + table data with 4-byte padding
  data_start = 12 + (num_tables * 16)
  cursor = data_start
  records = tables.map do |tag, data|
    length = data.bytesize
    checksum = Utilities::ChecksumCalculator.calculate_table_checksum(data)
    padding = Utilities::Padding.boundary(length)
    record = { tag:, checksum:, offset: cursor, length:, data:, padding: }
    cursor += length + padding
    record
  end

  # Write all table directory entries first (16 bytes each, sorted
  # alphabetically per SFNT convention for stable output), then all
  # table data with 4-byte padding. The directory entries' offset
  # fields were precomputed assuming this layout.
  sorted = records.sort_by { |r| r[:tag] }
  # rubocop:disable Style/CombinableLoops
  # SFNT requires all directory entries before any table data
  sorted.each do |r|
    out << r[:tag].ljust(4, "\x00")
    out << [r[:checksum]].pack("N")
    out << [r[:offset]].pack("N")
    out << [r[:length]].pack("N")
  end

  sorted.each do |r|
    out << r[:data]
    out << ("\x00" * r[:padding])
  end
  # rubocop:enable Style/CombinableLoops

  out
end