1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 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
|