First drop of code :-)
This commit is contained in:
		
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -52,3 +52,10 @@ docs/_build/ | |||||||
|  |  | ||||||
| # PyBuilder | # PyBuilder | ||||||
| target/ | target/ | ||||||
|  |  | ||||||
|  | # Ignore PyCharm stuff | ||||||
|  | .idea | ||||||
|  |  | ||||||
|  | # Ignore any fonts copied while testing! | ||||||
|  | *.ttf | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,2 +1,59 @@ | |||||||
| # zttf | # zttf | ||||||
| Python TTF file parser | Python TTF file parser | ||||||
|  |  | ||||||
|  | This was written to allow fonts to be parsed and then subsets generated for use in a PDF documents. | ||||||
|  |  | ||||||
|  | It was developed using Python 3.4 and will work to a degree with Python 2 it needs additional testing and development there. | ||||||
|  |  | ||||||
|  | ## Simple Usage | ||||||
|  |  | ||||||
|  | '''python | ||||||
|  | >>> from zttf.ttfile import TTFile | ||||||
|  | >>> font_file = TTFile('DroidSans.ttf') | ||||||
|  | >>> font_file.is_valid | ||||||
|  | True | ||||||
|  | >>> font_file.faces | ||||||
|  | [<zttf.ttf.TTFont object at 0x7f3569b73b50>] | ||||||
|  | >>> face = font_file.faces[0] | ||||||
|  | >>> face.family_name | ||||||
|  | Droid Sans | ||||||
|  | >>> face.name | ||||||
|  | DroidSans | ||||||
|  | >>> face.italic_angle | ||||||
|  | 0 | ||||||
|  | ''' | ||||||
|  |  | ||||||
|  | When used with a font collection, there will be multiple faces available. | ||||||
|  |  | ||||||
|  | '''python | ||||||
|  | >>> from zttf.ttfile import TTFile | ||||||
|  | >>> font_file = TTFile('Futura.ttc') | ||||||
|  | >>> font_file.is_valid | ||||||
|  | True | ||||||
|  | >>> font_file.faces | ||||||
|  | [<zttf.ttf.TTFont object at 0x7fc97520bc50>, <zttf.ttf.TTFont object at 0x7fc97520bc90>, <zttf.ttf.TTFont object at 0x7fc97520bd90>, <zttf.ttf.TTFont object at 0x7fc973b4c190>] | ||||||
|  | >>> font_file.faces[0].font_family | ||||||
|  | Futura | ||||||
|  | >>> font_file.faces[0].name | ||||||
|  | Futura-Medium | ||||||
|  | >>> font_file.faces[1].name | ||||||
|  | Futura-MediumItalic | ||||||
|  | >>> font_file.faces[2].name | ||||||
|  | Futura-CondensedMedium | ||||||
|  | >>> font_file.faces[3].name | ||||||
|  | Futura-CondensedExtraBold | ||||||
|  | ''' | ||||||
|  |  | ||||||
|  | Subsetting is done by passing in a subset of the characters desired. All required glyphs will be found and copied into the new file. | ||||||
|  |  | ||||||
|  | '''python | ||||||
|  | >>> from zttf.ttfile import TTFile | ||||||
|  | >>> font_file = TTFile('Futura.ttc') | ||||||
|  | >>> subset = [ord('H'), ord('e'), ord('l'), ord('o')] | ||||||
|  | >>> sub_font = font_file.faces[0].make_subset(subset) | ||||||
|  | >>> sub_font.output() | ||||||
|  | ... | ||||||
|  | >>> with open('new_font.ttf', 'wb') as fh: | ||||||
|  |         fh.write(sub_font.output()) | ||||||
|  | ''' | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import sys | ||||||
|  | from zttf.ttfile import TTFile | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     if len(sys.argv) < 2: | ||||||
|  |         print("Usage: {} <font filename>".format(sys.argv[0])) | ||||||
|  |         sys.exit(0) | ||||||
|  |  | ||||||
|  |     t = TTFile(sys.argv[1]) | ||||||
|  |     print("Is valid? {}".format(t.is_valid)) | ||||||
|  |     if not t.is_valid: | ||||||
|  |         sys.exit(0) | ||||||
|  |  | ||||||
|  |     print(t.faces) | ||||||
|  |     print(t.faces[0].font_family) | ||||||
|  |     print(t.faces[0].name) | ||||||
|  |     print(t.faces[0].italic_angle) | ||||||
|  |  | ||||||
|  |     subset = [ord('H'), ord('e'), ord('l'), ord('o')] | ||||||
|  |     font_subset = t.faces[0].make_subset(subset) | ||||||
|  |     with open('font_subset.ttf', 'wb') as fh: | ||||||
|  |         fh.write(font_subset.output()) | ||||||
							
								
								
									
										0
									
								
								zttf/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								zttf/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										571
									
								
								zttf/objects.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										571
									
								
								zttf/objects.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,571 @@ | |||||||
|  | # 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_dide_brearing', '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'}, | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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.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'} | ||||||
|  |     ] | ||||||
|  |  | ||||||
							
								
								
									
										257
									
								
								zttf/subset.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								zttf/subset.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,257 @@ | |||||||
|  | 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())) | ||||||
|  |  | ||||||
							
								
								
									
										272
									
								
								zttf/ttf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								zttf/ttf.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,272 @@ | |||||||
|  | from copy import copy | ||||||
|  | from struct import calcsize, unpack | ||||||
|  |  | ||||||
|  | from zttf.objects import TTFHeader, TTF_head, TTF_name, TTF_hhea, TTF_os2, TTF_post, TTF_maxp, TTF_cmap, TTF_glyf, \ | ||||||
|  |     TTF_kern | ||||||
|  | from zttf.subset import TTFSubset | ||||||
|  | from zttf.utils import read_list_uint16, read_list_uint32 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TTFont(object): | ||||||
|  |     def __init__(self, filename, offset): | ||||||
|  |         self.header = None | ||||||
|  |         self.tables = {} | ||||||
|  |         self.filename = filename | ||||||
|  |         self.start_pos = offset | ||||||
|  |  | ||||||
|  |         self.idx_format = 0 | ||||||
|  |         self.n_glyphs = 0 | ||||||
|  |         self.glyph_metrics = [] | ||||||
|  |         self.glyph_kern = {} | ||||||
|  |  | ||||||
|  |         self.file_handle = None | ||||||
|  |         self.parse() | ||||||
|  |  | ||||||
|  |     def parse(self): | ||||||
|  |         self._open() | ||||||
|  |         self.header = self._read_class(TTFHeader) | ||||||
|  |         if not self.header.check_version(): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         self.get_table(b'head', TTF_head) | ||||||
|  |         self.get_table(b'name', TTF_name) | ||||||
|  |         self.get_table(b'hhea', TTF_hhea) | ||||||
|  |         self.get_table(b'os2', TTF_os2) | ||||||
|  |         self.get_table(b'post', TTF_post) | ||||||
|  |         self.get_table(b'maxp', TTF_maxp) | ||||||
|  |         self.get_table(b'cmap', TTF_cmap) | ||||||
|  |         self.get_table(b'kern', TTF_kern) | ||||||
|  |  | ||||||
|  |         self.idx_format = self.get_table_attr(b'head', 'index_to_loc_format') | ||||||
|  |         self.n_glyphs = self.get_table_attr(b'maxp', 'num_glyphs', 0) | ||||||
|  |  | ||||||
|  |         self.get_hmtx() | ||||||
|  |         self.get_loca() | ||||||
|  |         if b'kern' in self.tables: | ||||||
|  |             self.get_kern_data() | ||||||
|  |  | ||||||
|  |         self._close() | ||||||
|  |  | ||||||
|  |     COMMON_DATA = { | ||||||
|  |         'font_family': (b'name', 1), | ||||||
|  |         'name': (b'name', 6), | ||||||
|  |         'ascender': (b'hhea', 'ascender'), | ||||||
|  |         'descender': (b'hhea', 'descender'), | ||||||
|  |         'units_per_em': (b'head', 'units_per_em', 1000), | ||||||
|  |         'cap_height': (b'os2', 'cap_height', 0), | ||||||
|  |         'bounding_box': (b'head', 'bounding_box'), | ||||||
|  |         'italic_angle': (b'post', 'italic_angle'), | ||||||
|  |         'underline_position': (b'post', 'underline_position'), | ||||||
|  |         'underline_thickness': (b'post', 'underline_thickness'), | ||||||
|  |         'weight_class': (b'os2', 'weight_class'), | ||||||
|  |         'line_gap': (b'hhea', 'line_gap'), | ||||||
|  |         'typo_line_gap': (b'os2', 'typo_line_gap'), | ||||||
|  |         'win_ascent': (b'os2', 'win_ascent'), | ||||||
|  |         'win_descent': (b'os2', 'win_descent') | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def __getattr__(self, item): | ||||||
|  |         if item in self.COMMON_DATA: | ||||||
|  |             how = self.COMMON_DATA[item] | ||||||
|  |             if how[0] == b'name': | ||||||
|  |                 return self.get_name_table(*how[1:]) | ||||||
|  |             if len(how) > 2: | ||||||
|  |                 return self.get_table_attr(*how[:3]) | ||||||
|  |             return self.get_table_attr(*how) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def stemv(self): | ||||||
|  |         return 50 + int(pow((self.weight_class / 65.0), 2)) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def italic(self): | ||||||
|  |         return self.italic_angle != 0 | ||||||
|  |  | ||||||
|  |     def get_string_width(self, string): | ||||||
|  |         width = 0 | ||||||
|  |         for n in range(len(string)): | ||||||
|  |             glyph = self.char_to_glyph(ord(string[n])) | ||||||
|  |             (aw, lsb) = self.glyph_metrics[glyph] | ||||||
|  |             width += aw | ||||||
|  |             if n == 0: | ||||||
|  |                 width -= lsb | ||||||
|  |             elif n < len(string) - 1: | ||||||
|  |                 glyf2 = self.char_to_glyph(ord(string[n + 1])) | ||||||
|  |                 width += self.glyph_kern.get((glyph, glyf2), 0) | ||||||
|  |         return width | ||||||
|  |  | ||||||
|  |     def get_char_width(self, char): | ||||||
|  |         if isinstance(char, str): | ||||||
|  |             char = ord(char) | ||||||
|  |         idx = self.char_to_glyph(char) | ||||||
|  |         if 0 < idx < len(self.glyph_metrics): | ||||||
|  |             idx = 0 | ||||||
|  |         return self.glyph_metrics[idx][0] | ||||||
|  |  | ||||||
|  |     # Internal Table Functions | ||||||
|  |     def get_table(self, tag, obj_class=None): | ||||||
|  |         tbl_obj = self.tables.get(tag) | ||||||
|  |         if tbl_obj is None and obj_class is not None: | ||||||
|  |             tbl = self.header.get_tag(tag) | ||||||
|  |             if tbl is None: | ||||||
|  |                 return None | ||||||
|  |             orig_pos = self._seek(tbl.offset) | ||||||
|  |             tbl_obj = self._read_class(obj_class, tbl.length) | ||||||
|  |             self.tables[tag] = tbl_obj | ||||||
|  |             self._seek(orig_pos) | ||||||
|  |         return tbl_obj | ||||||
|  |  | ||||||
|  |     def get_table_attr(self, tbl, attr, default=None): | ||||||
|  |         if tbl not in self.tables: | ||||||
|  |             return default | ||||||
|  |         return getattr(self.tables[tbl], attr, default) | ||||||
|  |  | ||||||
|  |     def get_name_table(self, n_attr, default=None): | ||||||
|  |         if b'name' not in self.tables: | ||||||
|  |             return default | ||||||
|  |         return self.tables[b'name'].get_name(n_attr, default) | ||||||
|  |  | ||||||
|  |     def copy_table(self, tag): | ||||||
|  |         tbl = self.get_table(tag) | ||||||
|  |         return copy(tbl) | ||||||
|  |  | ||||||
|  |     def _get_table_offset(self, tag): | ||||||
|  |         tbl = self.header.get_tag(tag) | ||||||
|  |         return tbl.offset if tbl is not None else 0 | ||||||
|  |  | ||||||
|  |     def get_hmtx(self): | ||||||
|  |         """ Read the glyph metrics. """ | ||||||
|  |         n_metrics = self.get_table_attr(b'hhea', 'number_of_metrics') | ||||||
|  |  | ||||||
|  |         offset = self._get_table_offset(b'hmtx') | ||||||
|  |         if offset == 0: | ||||||
|  |             return False | ||||||
|  |         self._seek(offset) | ||||||
|  |         aw = 0 | ||||||
|  |         for n in range(n_metrics): | ||||||
|  |             aw, lsb = unpack(">Hh", self.file_handle.read(4)) | ||||||
|  |             self.glyph_metrics.append((aw, lsb)) | ||||||
|  |         # Now we have read the aw and lsb for specific glyphs, we need to read additional | ||||||
|  |         # lsb data. | ||||||
|  |         extra = self.n_glyphs - n_metrics | ||||||
|  |         if extra > 0: | ||||||
|  |             lsbs = self._read_list_int16(extra) | ||||||
|  |             for n in range(extra): | ||||||
|  |                 self.glyph_metrics.append((aw, lsbs[n])) | ||||||
|  |  | ||||||
|  |     def get_loca(self,): | ||||||
|  |         start = self._get_table_offset(b'loca') | ||||||
|  |         self._seek(start) | ||||||
|  |         if self.idx_format == 0: | ||||||
|  |             self.tables[b'loca'] = [n * 2 for n in self._read_list_uint16(self.n_glyphs + 1)] | ||||||
|  |         elif self.idx_format == 1: | ||||||
|  |             self.tables[b'loca'] = self._read_list_uint32(self.n_glyphs + 1) | ||||||
|  |  | ||||||
|  |     def get_kern_data(self): | ||||||
|  |         kern = self.get_table(b'kern') | ||||||
|  |         for st in kern.subtables: | ||||||
|  |             if st.coverage != 1 or st.version != 0: | ||||||
|  |                 print("coverage = {}, version = {}  - skipping".format(st.coverage, st.version)) | ||||||
|  |                 continue | ||||||
|  |             self._seek(st.offset + len(st)) | ||||||
|  |             (npairs, a, b, c) = self._read_list_uint16(4) | ||||||
|  |             for n in range(npairs): | ||||||
|  |                 (l, r) = self._read_list_uint16(2) | ||||||
|  |                 diff = self._read_int16() | ||||||
|  |                 self.glyph_kern[(l, r)] = diff | ||||||
|  |  | ||||||
|  |     def char_to_glyph(self, char): | ||||||
|  |         self._open() | ||||||
|  |         cmap = self.get_table(b'cmap') | ||||||
|  |         glyph = cmap.char_to_glyph(char, self.file_handle) | ||||||
|  |         return glyph or 0 | ||||||
|  |  | ||||||
|  |     def get_glyph_position(self, glyph): | ||||||
|  |         loca = self.get_table(b'loca') | ||||||
|  |         return loca[glyph] | ||||||
|  |  | ||||||
|  |     def get_glyph_components(self, glyph): | ||||||
|  |         """ Return a list of any component glyphs required. """ | ||||||
|  |         if glyph < 0 or glyph > self.n_glyphs: | ||||||
|  |             print("Missing glyph!!! {}".format(glyph)) | ||||||
|  |             return [] | ||||||
|  |         pos = self._get_table_offset(b'glyf') + self.get_glyph_position(glyph) | ||||||
|  |         glyf = self._read_class(TTF_glyf, offset=pos, length=glyph) | ||||||
|  |         for g in glyf.required: | ||||||
|  |             for extra_glyph in self.get_glyph_components(g): | ||||||
|  |                 if extra_glyph not in glyf.required: | ||||||
|  |                     glyf.required.append(extra_glyph) | ||||||
|  |         return sorted(glyf.required) | ||||||
|  |  | ||||||
|  |     def get_glyph_data(self, glyph): | ||||||
|  |         data_start = self._get_table_offset(b'glyf') | ||||||
|  |         glyph_start = self.get_glyph_position(glyph) | ||||||
|  |         glyph_length = self.get_glyph_position(glyph + 1) - glyph_start | ||||||
|  |         if glyph_length == 0: | ||||||
|  |             print("Zero length glyph @ {}".format(glyph)) | ||||||
|  |             return b'' | ||||||
|  |         self._open() | ||||||
|  |         self.file_handle.seek(data_start + glyph_start) | ||||||
|  |         return self.file_handle.read(glyph_length) | ||||||
|  |  | ||||||
|  |     def get_binary_table(self, tag): | ||||||
|  |         tbl = self.header.get_tag(tag) | ||||||
|  |         print(tbl) | ||||||
|  |         if tbl is None: | ||||||
|  |             return b'' | ||||||
|  |         self._open() | ||||||
|  |         self._seek(tbl.offset) | ||||||
|  |         return self.file_handle.read(tbl.length) | ||||||
|  |  | ||||||
|  |     def make_subset(self, subset): | ||||||
|  |         """ Given a subset of characters, create a subset of the full TTF file suitable for | ||||||
|  |             inclusion in a PDF. | ||||||
|  |         :param subset: List of characters to include. | ||||||
|  |         :return: TTFSubset object | ||||||
|  |         """ | ||||||
|  |         return TTFSubset(self, subset) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # File functions. | ||||||
|  |     def _open(self): | ||||||
|  |         if self.file_handle is None: | ||||||
|  |             self.file_handle = open(self.filename, 'rb') | ||||||
|  |         self.file_handle.seek(self.start_pos) | ||||||
|  |  | ||||||
|  |     def _close(self): | ||||||
|  |         if self.file_handle is not None: | ||||||
|  |             self.file_handle.close() | ||||||
|  |             self.file_handle = None | ||||||
|  |  | ||||||
|  |     def _seek(self, offset, whence=0): | ||||||
|  |         self._open() | ||||||
|  |         pos = self.file_handle.tell() | ||||||
|  |         self.file_handle.seek(offset, whence) | ||||||
|  |         return pos | ||||||
|  |  | ||||||
|  |     def _read_class(self, cls, length=None, offset=None): | ||||||
|  |         if offset is not None: | ||||||
|  |             self._seek(offset) | ||||||
|  |         if length is not None: | ||||||
|  |             return cls(self.file_handle, length) | ||||||
|  |         return cls(self.file_handle) | ||||||
|  |  | ||||||
|  |     def _skip(self, offset): | ||||||
|  |         if self.file_handle is not None: | ||||||
|  |             self.file_handle.seek(offset, 1) | ||||||
|  |  | ||||||
|  |     def _read_list_int16(self, n): | ||||||
|  |         _fmt = ">{}h".format(n) | ||||||
|  |         return unpack(_fmt, self.file_handle.read(calcsize(_fmt))) | ||||||
|  |  | ||||||
|  |     def _read_list_uint16(self, n): | ||||||
|  |         return read_list_uint16(self.file_handle, n) | ||||||
|  |  | ||||||
|  |     def _read_uint16(self): | ||||||
|  |         return unpack(">H", self.file_handle.read(2))[0] | ||||||
|  |  | ||||||
|  |     def _read_int16(self): | ||||||
|  |         return unpack(">h", self.file_handle.read(2))[0] | ||||||
|  |  | ||||||
|  |     def _read_list_uint32(self, n): | ||||||
|  |         return read_list_uint32(self.file_handle, n) | ||||||
							
								
								
									
										22
									
								
								zttf/ttfile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								zttf/ttfile.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | from os.path import exists, getsize | ||||||
|  |  | ||||||
|  | from zttf.objects import TTFCollectionHeader | ||||||
|  | from zttf.ttf import TTFont | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TTFile(object): | ||||||
|  |     def __init__(self, filename): | ||||||
|  |         self.filename = filename | ||||||
|  |         self.faces = [] | ||||||
|  |  | ||||||
|  |         if not exists(filename) or getsize(filename) == 0: | ||||||
|  |             raise IOError("The file '{}' does not exist or is empty".format(filename)) | ||||||
|  |  | ||||||
|  |         with open(self.filename, 'rb') as fh: | ||||||
|  |             hdr = TTFCollectionHeader(fh) | ||||||
|  |             for off in hdr.offsets: | ||||||
|  |                 self.faces.append(TTFont(filename, off)) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def is_valid(self): | ||||||
|  |         return len(self.faces) > 0 | ||||||
							
								
								
									
										247
									
								
								zttf/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								zttf/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,247 @@ | |||||||
|  | from struct import calcsize, pack, unpack | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PackedFormat: | ||||||
|  |     """ Class to allow simpler extraction of data from a stream into an object with | ||||||
|  |         named attributes. | ||||||
|  |         All child classes need a FORMAT list of dicts describing the data to be extracted. | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     FORMAT = [] | ||||||
|  |  | ||||||
|  |     def __init__(self, fh=None, data=None, endian='>'): | ||||||
|  |         self.endian = endian | ||||||
|  |         self.parsed = False | ||||||
|  |         if fh is not None: | ||||||
|  |             self.from_file(fh) | ||||||
|  |         elif data is not None: | ||||||
|  |             self.from_data(data) | ||||||
|  |  | ||||||
|  |     def from_file(self, fh): | ||||||
|  |         for _f in self.FORMAT: | ||||||
|  |             if 'format' not in _f: | ||||||
|  |                 continue | ||||||
|  |             _fmt = '{}{}'.format(self.endian, _f['format']) | ||||||
|  |             _data = unpack(_fmt, fh.read(calcsize(_fmt)))[0] | ||||||
|  |             if 'name' not in _f: | ||||||
|  |                 continue | ||||||
|  |             if 'convert' in _f: | ||||||
|  |                 setattr(self, _f['name'] + '_raw', _data) | ||||||
|  |                 _fn = _f['convert'] if callable(_f['convert']) else getattr(self, _f['convert']) | ||||||
|  |                 if _fn is not None and callable(_fn): | ||||||
|  |                     _data = _fn(_data) | ||||||
|  |             setattr(self, _f['name'], _data) | ||||||
|  |         self.parsed = True | ||||||
|  |  | ||||||
|  |     def from_data(self, data): | ||||||
|  |         offset = 0 | ||||||
|  |         for _f in self.FORMAT: | ||||||
|  |             if 'format' not in _f: | ||||||
|  |                 continue | ||||||
|  |             _fmt = '{}{}'.format(self.endian, _f['format']) | ||||||
|  |             _data = unpack(_fmt, data[offset: offset + calcsize(_fmt)])[0] | ||||||
|  |             setattr(self, _f['name'], _data) | ||||||
|  |             offset += calcsize(_fmt) | ||||||
|  |         self.parsed = True | ||||||
|  |  | ||||||
|  |     def as_bytes(self): | ||||||
|  |         output = b'' | ||||||
|  |         for _f in self.FORMAT: | ||||||
|  |             _fmt = '{}{}'.format(self.endian, _f['format']) | ||||||
|  |             if 'convert' in _f: | ||||||
|  |                 _val = getattr(self, _f['name'] + '_raw', '' if 's' in _f['format'] else 0) | ||||||
|  |             else: | ||||||
|  |                 _val = getattr(self, _f['name'], '' if 's' in _f['format'] else 0) | ||||||
|  |             output += pack(_fmt, _val) | ||||||
|  |         return output | ||||||
|  |  | ||||||
|  |     def as_string(self): | ||||||
|  |         def _name_to_string(n): | ||||||
|  |             return n.replace('_', ' ').capitalize() | ||||||
|  |         ss = '' | ||||||
|  |         for _f in self.FORMAT: | ||||||
|  |             if 'name' not in _f: | ||||||
|  |                 continue | ||||||
|  |             ss += '  {}: {}\n'.format(_name_to_string(_f['name']), getattr(self, _f['name'])) | ||||||
|  |         return ss | ||||||
|  |  | ||||||
|  |     def as_table_string(self): | ||||||
|  |         def _name_to_string(n): | ||||||
|  |             return n.replace('_', ' ').capitalize() | ||||||
|  |         ss = '' | ||||||
|  |         offset = 0 | ||||||
|  |         for _f in self.FORMAT: | ||||||
|  |             _sz = calcsize(_f['format']) | ||||||
|  |             ss += ' {:04X} {:4s} {:>3d} '.format(offset, _f['format'], _sz) | ||||||
|  |             if 'name' in _f and getattr(self, _f['name']) is not None: | ||||||
|  |                 ss += '{:30s}  {}'.format(_name_to_string(_f['name']), getattr(self, _f['name'])) | ||||||
|  |             offset += _sz | ||||||
|  |             ss += '\n' | ||||||
|  |         return ss | ||||||
|  |  | ||||||
|  |     def __len__(self): | ||||||
|  |         fmt = "{}".format(self.endian) | ||||||
|  |         for _f in self.FORMAT: | ||||||
|  |             fmt += _f['format'] | ||||||
|  |         return calcsize(fmt) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def fixed_version(num): | ||||||
|  |     """ Decode a fixed 16:16 bit floating point number into a version code. | ||||||
|  |     :param num: fixed 16:16 floating point number as a 32-bit unsigned integer | ||||||
|  |     :return: version number (float) | ||||||
|  |     """ | ||||||
|  |     if num == 0x00005000: | ||||||
|  |         return 0.5 | ||||||
|  |     elif num == 0x00010000: | ||||||
|  |         return 1.0 | ||||||
|  |     elif num == 0x00020000: | ||||||
|  |         return 2.0 | ||||||
|  |     elif num == 0x00025000: | ||||||
|  |         return 2.5 | ||||||
|  |     elif num == 0x00030000: | ||||||
|  |         return 3.0 | ||||||
|  |     return num | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def binary_search_parameters(length): | ||||||
|  |     search_range = 1 | ||||||
|  |     entry_selector = 0 | ||||||
|  |     while search_range * 2 <= length: | ||||||
|  |         search_range *= 2 | ||||||
|  |         entry_selector += 1 | ||||||
|  |     return entry_selector, search_range, length - search_range | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Range: | ||||||
|  |     def __init__(self, start = 0, glyph=0): | ||||||
|  |         self.start = start | ||||||
|  |         self.expand(start) | ||||||
|  |         self.start_glyph = glyph | ||||||
|  |         self.iddelta = glyph - start | ||||||
|  |         self.offset = 0 | ||||||
|  |  | ||||||
|  |     def is_consecutive(self, n, g): | ||||||
|  |         return n == self.end and g == self.start_glyph + n - self.start | ||||||
|  |  | ||||||
|  |     def expand(self, n): | ||||||
|  |         self.end = (n + 1) & 0xffff | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return "CMAP: {} - {}  @  {}".format(self.start, self.end, self.iddelta) | ||||||
|  |  | ||||||
|  |     def as_map(self): | ||||||
|  |         # debugging.... | ||||||
|  |         return {n: n + self.iddelta for n in range(self.start, self.end)} | ||||||
|  |  | ||||||
|  |     def char_list(self): | ||||||
|  |         return range(self.start, self.end) | ||||||
|  |  | ||||||
|  |     def char_to_glyph(self, char, fh): | ||||||
|  |         if self.offset == 0: | ||||||
|  |             return self.get_glyph(char) | ||||||
|  |         ptr = self.get_offset(char) | ||||||
|  |         fh.seek(ptr) | ||||||
|  |         return self.get_glyph(unpack(">H", fh.read(2))[0]) | ||||||
|  |  | ||||||
|  |     def get_glyph(self, char): | ||||||
|  |         if char < self.start or char > self.end: | ||||||
|  |             return 0 | ||||||
|  |         return (char + self.iddelta) & 0xffff | ||||||
|  |  | ||||||
|  |     def get_offset(self, char): | ||||||
|  |         if char < self.start or char > self.end: | ||||||
|  |             return 0 | ||||||
|  |         return self.offset + 2 * (char - self.start) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def read_list_int16(fh, n): | ||||||
|  |     fmt = ">{}h".format(n) | ||||||
|  |     return unpack(fmt, fh.read(calcsize(fmt))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def read_list_uint16(fh, n): | ||||||
|  |     fmt = ">{}H".format(n) | ||||||
|  |     return unpack(fmt, fh.read(calcsize(fmt))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def read_list_uint32(fh, n): | ||||||
|  |     fmt = ">{}I".format(n) | ||||||
|  |     return unpack(fmt, fh.read(calcsize(fmt))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def ttf_checksum(data): | ||||||
|  |     data += b'\0' * (len(data) % 4) | ||||||
|  |     n_uint32 = int(len(data) / 4) | ||||||
|  |     chksum = 0 | ||||||
|  |     for val in unpack(">{}I".format(n_uint32), data): | ||||||
|  |         chksum += val | ||||||
|  |     return chksum & 0xFFFFFFFF | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ############################################################################# | ||||||
|  | ### | ||||||
|  | ### Glyph Utilities... | ||||||
|  | ### | ||||||
|  | ############################################################################# | ||||||
|  |  | ||||||
|  | # Flag Constants | ||||||
|  | GF_ARG_1_AND_2_ARE_WORDS = (1 << 0) | ||||||
|  | GF_ARGS_ARE_XY_VALUES = (1 << 1) | ||||||
|  | GF_ROUND_XY_TO_GRID = (1 << 2) | ||||||
|  | GF_WE_HAVE_A_SCALE = (1 << 3) | ||||||
|  | GF_RESERVED = (1 << 4) | ||||||
|  | GF_MORE_COMPONENTS = (1 << 5) | ||||||
|  | GF_WE_HAVE_AN_X_AND_Y_SCALE = (1 << 6) | ||||||
|  | GF_WE_HAVE_A_TWO_BY_TWO = (1 << 7) | ||||||
|  | GF_WE_HAVE_INSTRUCTIONS = (1 << 8) | ||||||
|  | GF_USE_MY_METRICS = (1 << 9) | ||||||
|  | GF_OVERLAP_COMPOUND = (1 << 10) | ||||||
|  | GF_SCALED_COMPONENT_OFFSET = (1 << 11) | ||||||
|  | GF_UNSCALED_COMPONENT_OFFSET = (1 << 12) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def glyf_skip_format(flags): | ||||||
|  |     """ Return the correct format for the data we will skip past based on flags set. """ | ||||||
|  |     skip = '>I' if flags & GF_ARG_1_AND_2_ARE_WORDS else '>H' | ||||||
|  |     if flags & GF_WE_HAVE_A_SCALE: | ||||||
|  |         return skip + 'H' | ||||||
|  |     elif flags & GF_WE_HAVE_AN_X_AND_Y_SCALE: | ||||||
|  |         return skip + 'I' | ||||||
|  |     elif flags & GF_WE_HAVE_A_TWO_BY_TWO: | ||||||
|  |         return skip + 'II' | ||||||
|  |     return skip | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def glyph_more_components(flag): | ||||||
|  |     return flag & GF_MORE_COMPONENTS | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def glyph_flags_decode(flag): | ||||||
|  |     print("Glyph flag = {:04X}".format(flag)) | ||||||
|  |     if flag & GF_ARG_1_AND_2_ARE_WORDS: | ||||||
|  |         print("GF_ARG_1_AND_2_ARE_WORDS") | ||||||
|  |     if flag & GF_ARGS_ARE_XY_VALUES: | ||||||
|  |         print("GF_ARGS_ARE_XY_VALUES") | ||||||
|  |     if flag & GF_ROUND_XY_TO_GRID: | ||||||
|  |         print("GF_ARGS_ROUND_XY_TO_GRID") | ||||||
|  |     if flag & GF_WE_HAVE_A_SCALE: | ||||||
|  |         print("GF_WE_HAVE_A_SCALE") | ||||||
|  |     if flag & GF_RESERVED: | ||||||
|  |         print("GF_RESERVED") | ||||||
|  |     if flag & GF_MORE_COMPONENTS: | ||||||
|  |         print("GF_MORE_COMPONENTS") | ||||||
|  |     if flag & GF_WE_HAVE_AN_X_AND_Y_SCALE: | ||||||
|  |         print("GF_WE_HAVE_AN_X_AND_Y_SCALE") | ||||||
|  |     if flag & GF_WE_HAVE_A_TWO_BY_TWO: | ||||||
|  |         print("GF_WE_HAVE_A_TWO_BY_TWO") | ||||||
|  |     if flag & GF_WE_HAVE_INSTRUCTIONS: | ||||||
|  |         print("GF_WE_HAVE_INSTRUCTIONS") | ||||||
|  |     if flag & GF_USE_MY_METRICS: | ||||||
|  |         print("GF_USE_MY_METRICS") | ||||||
|  |     if flag & GF_OVERLAP_COMPOUND: | ||||||
|  |         print("GF_OVERLAP_COMPOUND") | ||||||
|  |     if flag & GF_SCALED_COMPONENT_OFFSET: | ||||||
|  |         print("GF_SCALED_COMPONENT_OFFSET") | ||||||
|  |     if flag & GF_UNSCALED_COMPONENT_OFFSET: | ||||||
|  |         print("GF_UNSCALED_COMPONENT_OFFSET") | ||||||
		Reference in New Issue
	
	Block a user