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
Returns 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 |