First drop of code :-)
This commit is contained in:
		
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -52,3 +52,10 @@ docs/_build/ | ||||
|  | ||||
| # PyBuilder | ||||
| target/ | ||||
|  | ||||
| # Ignore PyCharm stuff | ||||
| .idea | ||||
|  | ||||
| # Ignore any fonts copied while testing! | ||||
| *.ttf | ||||
|  | ||||
|   | ||||
							
								
								
									
										57
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,2 +1,59 @@ | ||||
| # zttf | ||||
| 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