This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user