Files
zttf/zttf/objects.py
2018-07-18 09:33:06 -05:00

585 lines
19 KiB
Python

# TrueType Font Glyph operators
from struct import unpack, calcsize
from zttf.utils import PackedFormat, fixed_version, read_list_uint16, Range, read_list_int16, glyph_more_components, \
glyf_skip_format, ttf_checksum
TTF_NAMES = {
0: 'Copyright Notice',
1: 'Font Family Name',
2: 'Font Subfamily Name',
3: 'Unique Font Identifier',
4: 'Full Font Name',
5: 'Version String',
6: 'Postscript Name',
7: 'Trademark',
8: 'Manufacturer Name',
9: 'Designer',
10: 'Description',
11: 'Vendor URL',
12: 'Designer URL',
13: 'Licencee Description',
14: 'Licence URL',
15: 'Preferred Family',
16: 'Preferred Subfamily',
17: 'Compatible Full',
18: 'Sample Text',
19: 'PS CID findfont name',
20: 'WWS Family Name',
21: 'WWS Subfamily Name'
}
class TTFNameRecord(PackedFormat):
FORMAT = [
{'name': 'platform_id', 'format': 'H'},
{'name': 'encoding_id', 'format': 'H'},
{'name': 'language_id', 'format': 'H'},
{'name': 'name', 'format': 'H'},
{'name': 'length', 'format': 'H'},
{'name': 'offset', 'format': 'H'},
]
def __init__(self, fh, data):
self.pos = fh.tell()
PackedFormat.__init__(self, fh)
self.raw = data[self.offset:self.offset + self.length]
self.value = self.raw
if self.platform_id == 1:
if self.encoding_id == 0:
self.value = self.raw.decode('iso-8859-1')
elif self.platform_id == 3:
if self.encoding_id == 1:
# UCS-2
self.value = self.raw.decode('utf-16-be')
def __str__(self):
return '{:08d} @ {:08X} - {:>30s}: {}'.format(self.pos, self.offset,
TTF_NAMES.get(self.name, 'Unknown Name {:X}'.format(self.name)),
self.value)
class TTF_name(PackedFormat):
FORMAT = [
{'name': 'format', 'format': 'H'},
{'name': 'count', 'format': 'H'},
{'name': 'offset', 'format': 'H'},
]
def __init__(self, fh, length):
start_pos = fh.tell()
PackedFormat.__init__(self, fh)
pos = fh.tell()
fh.seek(start_pos + self.offset)
data = fh.read(length - self.offset)
fh.seek(pos)
self.names = []
for n in range(self.count):
self.names.append(TTFNameRecord(fh, data))
# print("{} / {} - {}".format(n + 1, self.count, self.names[-1]))
def get_name(self, name, default=None):
for n in self.names:
if n.name == name and n.platform_id == 1 and n.encoding_id == 0:
return n.value
return default
class TTFHeader(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'I', 'convert': fixed_version},
{'name': 'num_tables', 'format': 'H'},
{'name': 'search_range', 'format': 'H'},
{'name': 'entry_selector', 'format': 'H'},
{'name': 'range_shift', 'format': 'H'},
]
def __init__(self, fh=None):
self.tables = []
self.num_tables = 0
PackedFormat.__init__(self, fh)
for n in range(self.num_tables):
self.tables.append(TTFOffsetTable(fh))
def check_version(self):
return self.version == 1
def get_tag(self, tag):
for t in self.tables:
if t.tag == tag:
return t
if tag == b'os2' and t.tag == b'OS/2':
return t
return None
def dump_tables(self):
print("TTF Header Tables:")
for t in self.tables:
print(" {} @ {}".format(t.tag, t.offset))
class TTF_kern(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'H'},
{'name': 'num_tables', 'format': 'H'}
]
def __init__(self, fh=None, length=None):
self.subtables = []
PackedFormat.__init__(self, fh)
if fh is None:
return
for n in range(self.num_tables):
tbl = TTF_kern_subtable(fh)
fh.seek(tbl.length - len(tbl), 1)
self.subtables.append(tbl)
class TTF_kern_subtable(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'H'},
{'name': 'length', 'format': 'H'},
{'name': 'coverage', 'format': 'H'},
]
def __init__(self, fh=None):
if fh is not None:
self.offset = fh.tell()
PackedFormat.__init__(self, fh)
class TTFOffsetTable(PackedFormat):
FORMAT = [
{'name': 'tag', 'format': '4s'},
{'name': 'checksum', 'format': 'I'},
{'name': 'offset', 'format': 'I'},
{'name': 'length', 'format': 'I'},
]
def __str__(self):
return 'Offset Table: {} {} bytes @ {}'.format(self.tag, self.length, self.offset)
def padded_length(self):
return self.length + 3 & ~ 3
def padded_data(self, data):
extra = self.padded_length() - len(data)
if extra > 0:
return data + '\0' * extra
return data
def calculate_checksum(self, data):
self.checksum = ttf_checksum(data)
class TTF_head(PackedFormat):
FORMAT = [
{'name': 'vers', 'format': 'i'},
{'name': 'font_version', 'format': 'i'},
{'name': 'checksum_adj', 'format': 'I'},
{'name': 'magic_number', 'format': 'I'},
{'name': 'flags', 'format': 'H'},
{'name': 'units_per_em', 'format': 'H', 'convert': float},
{'name': 'created', 'format': 'q'},
{'name': 'modified', 'format': 'q'},
{'name': 'x_min', 'format': 'h'},
{'name': 'y_min', 'format': 'h'},
{'name': 'x_max', 'format': 'h'},
{'name': 'y_max', 'format': 'h'},
{'name': 'mac_style', 'format': 'H'},
{'name': 'lowest_rec_ppem', 'format': 'H'},
{'name': 'direction_hint', 'format': 'H'},
{'name': 'index_to_loc_format', 'format': 'h'},
{'name': 'glyph_data_format', 'format': 'h'},
]
@property
def bounding_box(self):
scale = 1000 / self.units_per_em
return [(self.x_min * scale),
(self.y_min * scale),
(self.x_max * scale),
(self.y_max * scale)]
def decode_mac_style(self):
return {
'bold': self.mac_style & 1 << 0,
'italic': self.mac_style & 1,
'underline': self.mac_style & 1 << 1,
'outline': self.mac_style & 1 << 2,
'shadow': self.mac_style & 1 << 3,
'condensed': self.mac_style & 1 << 4,
'extended': self.mac_style & 1 << 5
}
class TTF_hhea(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'i', 'convert': fixed_version},
{'name': 'ascender', 'format': 'h'},
{'name': 'descender', 'format': 'h'},
{'name': 'line_gap', 'format': 'h'},
{'name': 'advance_width_max', 'format': 'H'},
{'name': 'min_left_side_bearing', 'format': 'h'},
{'name': 'min_right_side_bearing', 'format': 'h'},
{'name': 'x_max_extant', 'format': 'h'},
{'name': 'caret_slope_rise', 'format': 'h'},
{'name': 'caret_slope_run', 'format': 'h'},
{'name': 'caret_offset', 'format': 'h'},
{'name': 'reserved', 'format': 'q'},
{'name': 'metric_data_format', 'format': 'h'},
{'name': 'number_of_metrics', 'format': 'H'},
]
class TTF_os2(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'H'},
{'name': 'xAvgCharWidth', 'format': 'h'},
{'name': 'weight_class', 'format': 'H'},
{'name': 'usWidthClass', 'format': 'H'},
{'name': 'fsType', 'format': 'H'},
{'name': 'ySubscriptXSize', 'format': 'h'},
{'name': 'ySubscriptYSize', 'format': 'h'},
{'name': 'ySubscriptXOffset', 'format': 'h'},
{'name': 'ySubscriptYOffset', 'format': 'h'},
{'name': 'ySuperscriptXSize', 'format': 'h'},
{'name': 'ySuperscriptYSize', 'format': 'h'},
{'name': 'ySuperscriptXOffset', 'format': 'h'},
{'name': 'ySuperscriptYOffset', 'format': 'h'},
{'name': 'yStrikeoutSize', 'format': 'h'},
{'name': 'yStrikeoutPosition', 'format': 'h'},
{'name': 'sFamilyClass', 'format': 'h'},
{'name': 'panose', 'format': '10s'},
{'name': 'ulUnicodeRange1', 'format': 'I'},
{'name': 'ulUnicodeRange2', 'format': 'I'},
{'name': 'ulUnicodeRange3', 'format': 'I'},
{'name': 'ulUnicodeRange4', 'format': 'I'},
{'name': 'achVendID', 'format': '4s'},
{'name': 'fsSelection', 'format': 'H'},
{'name': 'usFirstCharIndex', 'format': 'H'},
{'name': 'usLastCharIndex', 'format': 'H'},
{'name': 'sTypoAscender', 'format': 'h'},
{'name': 'sTypoDescender', 'format': 'h'},
{'name': 'typo_line_gap', 'format': 'h'},
{'name': 'win_ascent', 'format': 'H'},
{'name': 'win_descent', 'format': 'H'},
{'name': 'ulCodePageRange1', 'format': 'I'},
{'name': 'ulCodePageRange2', 'format': 'I'},
{'name': 'sxHeight', 'format': 'h'},
{'name': 'cap_height', 'format': 'h'},
{'name': 'usDefaultChar', 'format': 'H'},
{'name': 'usBreakChar', 'format': 'H'},
{'name': 'usMaxContext', 'format': 'H'}
]
class TTF_post(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'I', 'convert': fixed_version},
{'name': 'italic_angle', 'format': 'I'},
{'name': 'underline_position', 'format': 'h'},
{'name': 'underline_thickness', 'format': 'h'},
{'name': 'is_fixed_pitch', 'format': 'I'},
{'name': 'min_mem_type42', 'format': 'I'},
{'name': 'max_mem_type42', 'format': 'I'},
{'name': 'min_mem_type1', 'format': 'I'},
{'name': 'max_mem_type1', 'format': 'I'},
]
class TTF_maxp(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'I', 'convert': fixed_version},
{'name': 'num_glyphs', 'format': 'H'},
{'name': 'max_points', 'format': 'H'},
{'name': 'max_contours', 'format': 'H'},
{'name': 'max_component_points', 'format': 'H'},
{'name': 'max_component_contours', 'format': 'H'},
{'name': 'max_zones', 'format': 'H'},
{'name': 'max_twilight_points', 'format': 'H'},
{'name': 'max_storage', 'format': 'H'},
{'name': 'max_functiondefs', 'format': 'H'},
{'name': 'max_instructiondefs', 'format': 'H'},
{'name': 'max_stack_elements', 'format': 'H'},
{'name': 'max_size_of_instructions', 'format': 'H'},
{'name': 'max_component_elements', 'format': 'H'},
{'name': 'max_component_depth', 'format': 'H'},
]
class TTF_cmap4(PackedFormat):
FORMAT = [
{'name': 'language', 'format': 'H'},
{'name': 'seg_count', 'format': 'H', 'convert': '_halve_'},
{'name': 'src_range', 'format': 'H'},
{'name': 'entry_selector', 'format': 'H'},
{'name': 'range_shift', 'format': 'H'},
]
@staticmethod
def _halve_(n):
return int(n / 2)
class CMAPRange:
def __init__(self, start, end, delta, offset, n_segments):
self.start = start
self.end = end
self.delta = delta
self.offset = 0 if offset == 0 else int(offset / 2 - n_segments)
def contains(self, n):
return self.start <= n <= self.end
def coverage(self):
return range(self.start, self.end + 1)
def char_to_glyph(self, n, glyphs):
if self.offset == 0:
return (n + self.delta) & 0xFFFF
idx = self.offset + n - self.start
if 0 < idx < len(glyphs):
print("Invalid index for glyphs! {}".format(idx))
return 0
return (glyphs[idx] + self.delta) & 0xFFFF
def __init__(self, fh=None, length=None):
start = fh.tell() - 4
PackedFormat.__init__(self, fh)
if fh is None:
return
self.ranges = []
end_codes = read_list_uint16(fh, self.seg_count + 1)
if end_codes[self.seg_count] != 0:
print("INVALID pad byte....")
return
start_codes = read_list_uint16(fh, self.seg_count)
iddelta = read_list_int16(fh, self.seg_count)
offset_start = fh.tell()
id_offset = read_list_uint16(fh, self.seg_count)
ids_length = int((length - (fh.tell() - start)) / 2)
self.glyph_ids = read_list_uint16(fh, ids_length)
for n in range(self.seg_count):
self.ranges.append(self.CMAPRange(start_codes[n], end_codes[n], iddelta[n], id_offset[n], self.seg_count - n))
def __len__(self):
return len(self.ranges)
def char_to_glyph(self, char):
for r in self.ranges:
if not r.contains(char):
continue
return r.char_to_glyph(char, self.glyph_ids)
def as_map(self, max_char):
cm = {}
for r in self.ranges:
if r.start > max_char:
continue
for c in range(r.start, max(r.end, max_char)):
cm[c] = r.char_to_glyph(c, self.glyph_ids)
return cm
class TTF_cmap6(PackedFormat):
FORMAT = [
{'name': 'language', 'format': 'H'},
{'name': 'first_code', 'format': 'H'},
{'name': 'entry_count', 'format': 'H'},
]
def __init__(self, fh, length):
PackedFormat.__init__(self, fh)
self.char_map = {}
self.glyph_map = {}
mapping = read_list_uint16(fh, self.entry_count)
for n in range(self.entry_count):
self.char_map[n] = mapping[n]
self.glyph_map.setdefault(mapping[n], []).append(n)
def __len__(self):
return len(self.char_map)
class TTF_cmap(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'H'},
{'name': 'count', 'format': 'H'},
]
PREFS = [(0, 4), (0, 3), (3, 1)]
def __init__(self, fh=None, length=0):
self.count = 0
if fh:
start_pos = fh.tell()
PackedFormat.__init__(self, fh)
self.tables = {}
self.map_table = None
if self.count == 0:
return
for n in range(self.count):
tbl = TTFcmapTable(fh)
self.tables[(tbl.platform_id, tbl.encoding_id)] = tbl
pos = fh.tell()
fh.seek(start_pos + tbl.offset)
tbl.format, length = read_list_uint16(fh, 2)
if tbl.format == 4:
tbl.map_data = TTF_cmap4(fh, length)
elif tbl.format == 6:
tbl.map_data = TTF_cmap6(fh, length)
fh.seek(pos)
# Choose the mapping we are going to use, initially on preferences and
# then just fallback to first available map.
for p in self.PREFS:
if p in self.tables and self.tables[p].has_map_data:
self.map_table = self.tables[p].map_data
break
if self.map_table is None:
for t in self.tables.values():
if t.has_map_data:
self.map_table = t.map_data
break
def char_to_glyph(self, char, fh):
for p in self.tables: #PREFS:
if p in self.tables and self.tables[p].has_map_data:
for rng in self.tables[p].map_data.ranges:
if rng.end < char:
continue
if rng.start > char:
continue
return rng.char_to_glyph(char, fh)
return None
def char_map(self, max_char=256):
return self.map_table.as_map(max_char)
def as_table_string(self):
s = PackedFormat.as_table_string(self)
n = 0
for t in self.tables:
s += '\nTable: {}\n'.format(n)
s += t.as_table_string()
n += 1
return s
class TTFcmapTable(PackedFormat):
FORMAT = [
{'name': 'platform_id', 'format': 'H'},
{'name': 'encoding_id', 'format': 'H'},
{'name': 'offset', 'format': 'I'},
]
def __init__(self, fh=None):
PackedFormat.__init__(self, fh)
self.format = 0
self.map_data = None
self.position = 0
@property
def has_map_data(self):
return self.map_data is not None and len(self.map_data) > 0
def as_map(self, max_char):
cm = {}
for r in self.map_data.ranges:
cm.update(r.as_map(max_char))
return cm
class TTF_glyf(PackedFormat):
FORMAT = [
{'name': 'contours', 'format': 'h'},
{'name': 'x_min', 'format': 'h'},
{'name': 'y_min', 'format': 'h'},
{'name': 'x_max', 'format': 'h'},
{'name': 'y_max', 'format': 'h'},
]
def __init__(self, fh=None, num=0, data=None):
self.glyph = num
self.components = []
self.required = set()
PackedFormat.__init__(self, fh=fh, data=data)
# If the glyph is a compound glyph, ie it's made up of parts of other glyphs,
# then we need to ensure we have all the component glyphs listed.
if self.contours < 0:
while True:
(flags, next_glyph) = read_list_uint16(fh, 2)
self.required.add(next_glyph)
fh.read(calcsize(glyf_skip_format(flags)))
if not glyph_more_components(flags):
break
def is_compound(self):
return self.contours < 0
def glyph_set(self):
rqd = set(self.required)
for c in self.components:
rqd.extend(c.required)
return sorted(rqd)
class TTFCollectionHeader(PackedFormat):
FORMAT = [
{'name': 'tag', 'format': '4s'},
{'name': 'version', 'format': 'I', 'convert': fixed_version},
{'name': 'count', 'format': 'I'}
]
def __init__(self, fh):
PackedFormat.__init__(self, fh)
self.offsets = []
self.is_collection = (self.tag == b'ttcf')
if self.is_collection:
for i in range(self.count):
self.offsets.append(unpack('>I', fh.read(4))[0])
else:
self.count = 1
self.offsets = [0]
if self.version == 2:
self.dsig_tag, self.dsig_length, self.dsig_offset = unpack("III", fh.read(calcsize('III')))
class TTF_gpos(PackedFormat):
FORMAT = [
{'name': 'version', 'format': 'I', 'convert': fixed_version},
{'name': 'script_list', 'format': 'H'},
{'name': 'feature_list', 'format': 'H'},
{'name': 'lookup_list', 'format': 'H'},
]
class OFT_ScriptList(PackedFormat):
FORMAT = [
{'name': 'count', 'format': 'H'}
]
def __init__(self, fh, length=None):
self.records = []
PackedFormat.__init__(self, fh)
for n in range(self.count):
self.records.append(OFT_ScriptRecord(fh))
class OFT_ScriptRecord(PackedFormat):
FORMAT = [
{'name': 'tag', 'format': '4s'},
{'name': 'offset', 'format': 'H'}
]