# frozen_string_literal: true
module TTFunk
# Encodes a TrueType font subset to its binary representation.
class TTFEncoder
# Optimal table order according to TrueType specification.
OPTIMAL_TABLE_ORDER = [
'head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'LTSH', 'VDMX',
'hdmx', 'cmap', 'fpgm', 'prep', 'cvt ', 'loca', 'glyf',
'kern', 'name', 'post', 'gasp', 'PCLT',
].freeze
# Original font.
# @return [TTFunk::File]
attr_reader :original
# Subset to encode.
# @return [TTFunk::Subset]
attr_reader :subset
# Encoding options.
# @return [Hash]
attr_reader :options
# @param original [TTFunk::File]
# @param subset [TTFunk::Subset]
# @param options [Hash]
# @option options :kerning [Boolean] whether to encode Kerning (`kern`)
# table.
def initialize(original, subset, options = {})
@original = original
@subset = subset
@options = options
end
# Encode the font subset.
#
# @return [String]
def encode
# https://www.microsoft.com/typography/otspec/otff.htm#offsetTable
search_range = (2**Math.log2(tables.length).floor) * 16
entry_selector = Integer(Math.log2(2**Math.log2(tables.length).floor))
range_shift = (tables.length * 16) - search_range
range_shift = 0 if range_shift.negative?
newfont = EncodedString.new
newfont << [
original.directory.scaler_type,
tables.length,
search_range,
entry_selector,
range_shift,
].pack('Nn*')
# Tables are supposed to be listed in ascending order whereas there is a
# known optimal order for table data.
tables.keys.sort.each do |tag|
newfont << [tag, checksum(tables[tag])].pack('A4N')
newfont << Placeholder.new(tag, length: 4)
newfont << [tables[tag].length].pack('N')
end
optimal_table_order.each do |optimal_tag|
next unless tables.include?(optimal_tag)
newfont.resolve_placeholder(optimal_tag, [newfont.length].pack('N'))
newfont << tables[optimal_tag]
newfont.align!(4)
end
sum = checksum(newfont)
adjustment = 0xB1B0AFBA - sum
newfont.resolve_placeholder(:checksum, [adjustment].pack('N'))
newfont.string
end
private
def optimal_table_order
OPTIMAL_TABLE_ORDER +
(tables.keys - ['DSIG'] - OPTIMAL_TABLE_ORDER) +
['DSIG']
end
# "mandatory" tables. Every font should ("should") have these
def cmap_table
@cmap_table ||= subset.new_cmap_table
end
def glyf_table
@glyf_table ||= TTFunk::Table::Glyf.encode(glyphs, new_to_old_glyph, old_to_new_glyph)
end
def loca_table
@loca_table ||= TTFunk::Table::Loca.encode(glyf_table[:offsets])
end
def hmtx_table
@hmtx_table ||= TTFunk::Table::Hmtx.encode(original.horizontal_metrics, new_to_old_glyph)
end
def hhea_table
@hhea_table = TTFunk::Table::Hhea.encode(original.horizontal_header, hmtx_table, original, new_to_old_glyph)
end
def maxp_table
@maxp_table ||= TTFunk::Table::Maxp.encode(original.maximum_profile, old_to_new_glyph)
end
def post_table
@post_table ||= TTFunk::Table::Post.encode(original.postscript, new_to_old_glyph)
end
def name_table
@name_table ||= TTFunk::Table::Name.encode(original.name, glyf_table.fetch(:table, ''))
end
def head_table
@head_table ||= TTFunk::Table::Head.encode(original.header, loca_table, new_to_old_glyph)
end
# "optional" tables. Fonts may omit these if they do not need them.
# Because they apply globally, we can simply copy them over, without
# modification, if they exist.
def os2_table
@os2_table ||= TTFunk::Table::OS2.encode(original.os2, subset)
end
def cvt_table
@cvt_table ||= TTFunk::Table::Simple.new(original, 'cvt ').raw
end
def fpgm_table
@fpgm_table ||= TTFunk::Table::Simple.new(original, 'fpgm').raw
end
def prep_table
@prep_table ||= TTFunk::Table::Simple.new(original, 'prep').raw
end
def gasp_table
@gasp_table ||= TTFunk::Table::Simple.new(original, 'gasp').raw
end
def kern_table
# for PDFs, the kerning info is all included in the PDF as the text is
# drawn. Thus, the PDF readers do not actually use the kerning info in
# embedded fonts. If the library is used for something else, the
# generated subfont may need a kerning table... in that case, you need
# to opt into it.
if options[:kerning]
@kern_table ||= TTFunk::Table::Kern.encode(original.kerning, old_to_new_glyph)
end
end
def vorg_table
@vorg_table ||= TTFunk::Table::Vorg.encode(original.vertical_origins)
end
def dsig_table
@dsig_table ||= TTFunk::Table::Dsig.encode(original.digital_signature)
end
def tables
@tables ||= {
'cmap' => cmap_table[:table],
'glyf' => glyf_table[:table],
'loca' => loca_table[:table],
'kern' => kern_table,
'hmtx' => hmtx_table[:table],
'hhea' => hhea_table,
'maxp' => maxp_table,
'OS/2' => os2_table,
'post' => post_table,
'name' => name_table,
'head' => head_table,
'prep' => prep_table,
'fpgm' => fpgm_table,
'cvt ' => cvt_table,
'VORG' => vorg_table,
'DSIG' => dsig_table,
'gasp' => gasp_table,
}.compact
end
def glyphs
subset.glyphs
end
def new_to_old_glyph
subset.new_to_old_glyph
end
def old_to_new_glyph
subset.old_to_new_glyph
end
def checksum(data)
align(raw(data), 4).unpack('N*').sum & 0xFFFF_FFFF
end
def raw(data)
data.respond_to?(:unresolved_string) ? data.unresolved_string : data
end
def align(data, width)
if (data.length % width).positive?
data + ("\0" * (width - (data.length % width)))
else
data
end
end
end
end