258 lines
9.5 KiB
Python
258 lines
9.5 KiB
Python
from io import BytesIO
|
|
from struct import pack, unpack, calcsize, error as struct_error
|
|
from zttf.objects import TTF_post, TTFHeader, TTFOffsetTable, TTF_kern, TTF_kern_subtable
|
|
from zttf.utils import Range, glyph_more_components, glyf_skip_format, ttf_checksum, binary_search_parameters
|
|
|
|
|
|
class TTFSubset:
|
|
def __init__(self, parent, subset):
|
|
self.parent = parent
|
|
self.subset = subset
|
|
|
|
self.tables = {}
|
|
# We need to build 2 maps, one for character -> glyph and one
|
|
# for glyph -> character
|
|
self.orig_char_to_glyph = {}
|
|
self.orig_glyph_to_char = {}
|
|
self.glyph_map = {}
|
|
|
|
self.char_to_glyph = {}
|
|
self.glyph_to_char = {}
|
|
self.cmap_ranges = []
|
|
|
|
self.required_glyphs = [0]
|
|
self.metrics = []
|
|
|
|
self.fh = None
|
|
|
|
def start_table(self, tag, data=None):
|
|
b = BytesIO()
|
|
if data is not None:
|
|
b.write(data)
|
|
self.tables[tag] = b
|
|
return b
|
|
|
|
def find_glyph_subset(self):
|
|
for s in self.subset:
|
|
self.parent.char_to_glyph(s)
|
|
|
|
char_to_glyphs = self.parent.get_table(b'cmap').char_map()
|
|
rqd = []
|
|
for code in self.subset:
|
|
glyph = char_to_glyphs.get(code)
|
|
if glyph is None:
|
|
print("Unknown character in parent mapping: {}".format(code))
|
|
continue
|
|
# print("character {} is glyph {}".format(code, glyph))
|
|
self.orig_char_to_glyph[code] = glyph
|
|
self.orig_glyph_to_char.setdefault(glyph, []).append(code)
|
|
if glyph not in rqd:
|
|
rqd.append(glyph)
|
|
|
|
for glyph in rqd:
|
|
self.required_glyphs.append(glyph)
|
|
self.required_glyphs.extend(self.parent.get_glyph_components(glyph))
|
|
|
|
self.required_glyphs.sort()
|
|
|
|
self.glyph_map = {}
|
|
for rg in self.required_glyphs:
|
|
glyph = len(self.glyph_map) + 1
|
|
self.glyph_map[rg] = glyph
|
|
if rg in self.orig_glyph_to_char:
|
|
for cc in self.orig_glyph_to_char[rg]:
|
|
self.char_to_glyph[cc] = glyph
|
|
self.glyph_to_char[glyph] = self.orig_glyph_to_char[rg]
|
|
|
|
def copy_tables(self):
|
|
for tag in [b'name', b'cvt', b'fpgm', b'prep', b'gasp']:
|
|
if tag in self.parent.tables:
|
|
buff = self.start_table(tag)
|
|
tbl = self.parent.header.get_tag(tag)
|
|
self.fh.seek(tbl.offset)
|
|
buff.write(self.fh.read(tbl.length))
|
|
|
|
new_post = TTF_post()
|
|
for f in ['italic_angle', 'underline_position', 'Underline_thickness', 'is_fixed_pitch']:
|
|
setattr(new_post, f, self.parent.get_table_attr(b'post', f))
|
|
self.start_table(b'post', new_post.as_bytes())
|
|
|
|
head = self.parent.copy_table(b'head')
|
|
head.checksum_adj = 0
|
|
head.index_to_loc_format = 0
|
|
self.start_table(b'head', head.as_bytes())
|
|
|
|
hhea = self.parent.copy_table(b'hhea')
|
|
hhea.number_of_metrics = len(self.metrics)
|
|
self.start_table(b'hhea', hhea.as_bytes())
|
|
|
|
maxp = self.parent.copy_table(b'maxp')
|
|
maxp.b_glyphs = len(self.required_glyphs)
|
|
self.start_table(b'maxp', maxp.as_bytes())
|
|
|
|
self.start_table(b'os2', self.parent.copy_table(b'os2').as_bytes())
|
|
# todo - is it worth finding a way to subset the GPOS and LTSH tables?
|
|
|
|
def build_cmap_ranges(self):
|
|
# As we will likely have a scattered map we will use CMAP Format 4.
|
|
# We take the character mappings we have and build 4 lists...
|
|
# start code
|
|
# end code
|
|
# id delta
|
|
# range offset
|
|
self.cmap_ranges = []
|
|
for cc, glyph in sorted(self.char_to_glyph.items()):
|
|
try:
|
|
current = self.cmap_ranges[-1]
|
|
if current is None or not current.is_consecutive(cc, glyph):
|
|
self.cmap_ranges.append(Range(cc, glyph))
|
|
else:
|
|
current.expand(cc)
|
|
except IndexError:
|
|
self.cmap_ranges.append(Range(cc, glyph))
|
|
|
|
def add_cmap_table(self):
|
|
if self.cmap_ranges == []:
|
|
self.build_cmap_ranges()
|
|
self.cmap_ranges.append(Range(0xffff, 0))
|
|
self.cmap_ranges[-1].iddelta = 0
|
|
|
|
seg_count = len(self.cmap_ranges)
|
|
src_range, entry_selector, range_shift = binary_search_parameters(seg_count * 2)
|
|
length = 16 + 8 * seg_count + len(self.glyph_to_char) + 1
|
|
|
|
data = [
|
|
0, # version
|
|
1, # number of subtables
|
|
3, # platform id (MS)
|
|
1, # endocing id (Unicode)
|
|
0, 12, # subtable location
|
|
# subtable
|
|
4, # format
|
|
length, # length
|
|
0, # language
|
|
seg_count * 2, # seg count * 2
|
|
src_range, # search range (2 ** floor(log2(seg_count)))
|
|
entry_selector, # entry selector log2(src_range / 2)
|
|
seg_count * 2 - src_range, # range shift ( 2 * seg_count - search_range)
|
|
]
|
|
data.extend([r.end for r in self.cmap_ranges])
|
|
data.append(0)
|
|
data.extend([r.start for r in self.cmap_ranges])
|
|
|
|
buff = self.start_table(b'cmap')
|
|
buff.write(pack(">{}H".format(len(data)), *data))
|
|
buff.write(pack(">{}h".format(len(self.cmap_ranges)), *[r.iddelta for r in self.cmap_ranges]))
|
|
buff.write(pack(">{}H".format(len(self.cmap_ranges)), *[r.offset for r in self.cmap_ranges]))
|
|
buff.write(pack(">{}H".format(len(self.cmap_ranges)), *[r.start_glyph for r in self.cmap_ranges]))
|
|
|
|
def get_glyphs(self):
|
|
locations = []
|
|
self.metrics = []
|
|
buff = self.start_table(b'glyf')
|
|
for g in self.required_glyphs:
|
|
locations.append(int(buff.tell() / 2))
|
|
data = self.parent.get_glyph_data(g)
|
|
if data == b'':
|
|
continue
|
|
if unpack(">h", data[:2])[0] == -1:
|
|
# need to adjust glyph index...
|
|
pos = 10
|
|
while True:
|
|
flags, next_glyph = unpack(">HH", data[pos: pos + 4])
|
|
data = data[:pos + 2] + pack(">H", self.glyph_map[next_glyph]) + data[pos+4:]
|
|
pos += 4 + calcsize(glyf_skip_format(flags))
|
|
if not glyph_more_components(flags):
|
|
break
|
|
buff.write(data)
|
|
self.metrics.append(self.parent.glyph_metrics[g])
|
|
loca = self.start_table(b'loca')
|
|
loca.write(pack(">{}H".format(len(locations)), *locations))
|
|
|
|
hmtx = self.start_table(b'hmtx')
|
|
for m in self.metrics:
|
|
hmtx.write(pack(">Hh", *m))
|
|
|
|
def add_kern_data(self):
|
|
entries = {}
|
|
|
|
for k, diff in self.parent.glyph_kern.items():
|
|
if k[0] not in self.required_glyphs or k[1] not in self.required_glyphs:
|
|
continue
|
|
# print("mapping {} to ({}, {})".format(k, self.glyph_map[k[0]], self.glyph_map[k[1]]))
|
|
entries[(self.glyph_map[k[0]], self.glyph_map[k[1]])] = diff
|
|
if len(entries) == 0:
|
|
return
|
|
|
|
kern = self.start_table(b'kern')
|
|
kh = TTF_kern()
|
|
kh.version = 0
|
|
kh.num_tables = 1
|
|
kern.write(kh.as_bytes())
|
|
st = TTF_kern_subtable()
|
|
st.length = len(st) + 6 * len(entries) + 8
|
|
st.version = 0
|
|
st.coverage = 1
|
|
kern.write(st.as_bytes())
|
|
kern.write(pack(">H", len(entries)))
|
|
kern.write(pack(">HHH", *binary_search_parameters(len(entries))))
|
|
for key, diff in entries.items():
|
|
kern.write(pack(">HHh", key[0], key[1], diff))
|
|
|
|
# Put the TTF file together
|
|
def output(self):
|
|
""" Generate a binary based on the subset we have been given. """
|
|
|
|
self.fh = open(self.parent.filename, 'rb')
|
|
self.fh.seek(self.parent.start_pos)
|
|
|
|
self.find_glyph_subset()
|
|
self.add_kern_data()
|
|
self.copy_tables()
|
|
self.add_cmap_table()
|
|
self.get_glyphs()
|
|
# self.dump_tables()
|
|
|
|
self.fh.close()
|
|
|
|
header = TTFHeader()
|
|
header.num_tables = len(self.tables)
|
|
header.version_raw = 0x00010000
|
|
|
|
output = BytesIO()
|
|
header.entry_selector, header.search_range, header.range_shift = binary_search_parameters(len(self.tables))
|
|
output.write(header.as_bytes())
|
|
|
|
head_offset = 0
|
|
offset = output.tell() + 16 * len(self.tables)
|
|
sorted_tables = sorted(self.tables.keys())
|
|
for tag in sorted_tables:
|
|
if tag == b'head':
|
|
head_offset = offset
|
|
tbl = TTFOffsetTable()
|
|
tbl.tag = tag
|
|
tbl.offset = offset
|
|
data = self.tables[tag].getvalue()
|
|
tbl.length = len(data)
|
|
tbl.calculate_checksum(data)
|
|
offset += tbl.padded_length()
|
|
output.write(tbl.as_bytes())
|
|
|
|
for tag in sorted_tables:
|
|
data = self.tables[tag].getvalue()
|
|
data += b'\0' * (len(data) % 4)
|
|
output.write(data)
|
|
|
|
checksum = 0xB1B0AFBA - ttf_checksum(output.getvalue())
|
|
data = output.getvalue()
|
|
try:
|
|
data = data[:head_offset + 8] + pack(">I", checksum) + data[head_offset + 12:]
|
|
except struct_error:
|
|
data = data[:head_offset + 8] + pack(">i", checksum) + data[head_offset + 12:]
|
|
return data
|
|
|
|
def dump_tables(self):
|
|
for n in sorted(self.tables):
|
|
print("{} {} bytes".format(n, self.tables[n].tell()))
|
|
|