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
-
.build(flavor:, tables:) ⇒ String
SFNT binary.
Class Method Details
.build(flavor:, tables:) ⇒ String
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 |