Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 1 | """fontTools.ttLib -- a package for dealing with TrueType fonts. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 2 | |
| 3 | This package offers translators to convert TrueType fonts to Python |
Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 4 | objects and vice versa, and additionally from Python to TTX (an XML-based |
| 5 | text format) and vice versa. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 6 | |
| 7 | Example interactive session: |
| 8 | |
| 9 | Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL] |
| 10 | Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam |
| 11 | >>> from fontTools import ttLib |
| 12 | >>> tt = ttLib.TTFont("afont.ttf") |
| 13 | >>> tt['maxp'].numGlyphs |
| 14 | 242 |
| 15 | >>> tt['OS/2'].achVendID |
| 16 | 'B&H\000' |
| 17 | >>> tt['head'].unitsPerEm |
| 18 | 2048 |
Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 19 | >>> tt.saveXML("afont.ttx") |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 20 | Dumping 'LTSH' table... |
| 21 | Dumping 'OS/2' table... |
| 22 | Dumping 'VDMX' table... |
| 23 | Dumping 'cmap' table... |
| 24 | Dumping 'cvt ' table... |
| 25 | Dumping 'fpgm' table... |
| 26 | Dumping 'glyf' table... |
| 27 | Dumping 'hdmx' table... |
| 28 | Dumping 'head' table... |
| 29 | Dumping 'hhea' table... |
| 30 | Dumping 'hmtx' table... |
| 31 | Dumping 'loca' table... |
| 32 | Dumping 'maxp' table... |
| 33 | Dumping 'name' table... |
| 34 | Dumping 'post' table... |
| 35 | Dumping 'prep' table... |
| 36 | >>> tt2 = ttLib.TTFont() |
Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 37 | >>> tt2.importXML("afont.ttx") |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 38 | >>> tt2['maxp'].numGlyphs |
| 39 | 242 |
| 40 | >>> |
| 41 | |
| 42 | """ |
| 43 | |
Just | 7dcc916 | 1999-12-29 13:06:08 +0000 | [diff] [blame] | 44 | # |
pabs3 | 7e91e77 | 2009-02-22 08:55:00 +0000 | [diff] [blame] | 45 | # $Id: __init__.py,v 1.51 2009-02-22 08:55:00 pabs3 Exp $ |
Just | 7dcc916 | 1999-12-29 13:06:08 +0000 | [diff] [blame] | 46 | # |
| 47 | |
Behdad Esfahbod | 1ae2959 | 2014-01-14 15:07:50 +0800 | [diff] [blame] | 48 | from __future__ import print_function, division, absolute_import |
Behdad Esfahbod | bb0beb7 | 2013-11-27 14:37:28 -0500 | [diff] [blame] | 49 | from fontTools.misc.py23 import * |
Behdad Esfahbod | 30e691e | 2013-11-27 17:27:45 -0500 | [diff] [blame] | 50 | import os |
| 51 | import sys |
jvr | 1cff4cb | 2003-08-28 18:23:43 +0000 | [diff] [blame] | 52 | |
| 53 | haveMacSupport = 0 |
| 54 | if sys.platform == "mac": |
| 55 | haveMacSupport = 1 |
| 56 | elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0): |
| 57 | # Python 2.2's Mac support is broken, so don't enable it there. |
| 58 | haveMacSupport = 1 |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 59 | |
jvr | d04a3bb | 2002-05-02 15:23:25 +0000 | [diff] [blame] | 60 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 61 | class TTLibError(Exception): pass |
| 62 | |
| 63 | |
Behdad Esfahbod | e388db5 | 2013-11-28 14:26:58 -0500 | [diff] [blame] | 64 | class TTFont(object): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 65 | |
| 66 | """The main font object. It manages file input and output, and offers |
| 67 | a convenient way of accessing tables. |
pabs3 | 1344bc9 | 2010-01-09 09:12:11 +0000 | [diff] [blame] | 68 | Tables will be only decompiled when necessary, ie. when they're actually |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 69 | accessed. This means that simple operations can be extremely fast. |
| 70 | """ |
| 71 | |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 72 | def __init__(self, file=None, res_name_or_index=None, |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 73 | sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, |
| 74 | verbose=False, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, |
Behdad Esfahbod | 283fb26 | 2013-12-16 00:50:48 -0500 | [diff] [blame] | 75 | fontNumber=-1, lazy=False, quiet=False): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 76 | |
| 77 | """The constructor can be called with a few different arguments. |
| 78 | When reading a font from disk, 'file' should be either a pathname |
| 79 | pointing to a file, or a readable file object. |
| 80 | |
| 81 | It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt |
| 82 | resource name or an sfnt resource index number or zero. The latter |
| 83 | case will cause TTLib to autodetect whether the file is a flat file |
| 84 | or a suitcase. (If it's a suitcase, only the first 'sfnt' resource |
| 85 | will be read!) |
| 86 | |
jvr | ea9dfa9 | 2002-05-12 17:14:50 +0000 | [diff] [blame] | 87 | The 'checkChecksums' argument is used to specify how sfnt |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 88 | checksums are treated upon reading a file from disk: |
| 89 | 0: don't check (default) |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 90 | 1: check, print warnings if a wrong checksum is found |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 91 | 2: check, raise an exception if a wrong checksum is found. |
| 92 | |
| 93 | The TTFont constructor can also be called without a 'file' |
| 94 | argument: this is the way to create a new empty font. |
Behdad Esfahbod | 58d7416 | 2013-08-15 15:30:55 -0400 | [diff] [blame] | 95 | In this case you can optionally supply the 'sfntVersion' argument, |
| 96 | and a 'flavor' which can be None, or 'woff'. |
Just | 88cb4f3 | 1999-12-18 18:06:25 +0000 | [diff] [blame] | 97 | |
Just | 98d780d | 1999-12-23 15:16:22 +0000 | [diff] [blame] | 98 | If the recalcBBoxes argument is false, a number of things will *not* |
Just | 3e097c6 | 1999-12-23 14:44:16 +0000 | [diff] [blame] | 99 | be recalculated upon save/compile: |
Just | 98d780d | 1999-12-23 15:16:22 +0000 | [diff] [blame] | 100 | 1) glyph bounding boxes |
| 101 | 2) maxp font bounding box |
| 102 | 3) hhea min/max values |
| 103 | (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). |
Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 104 | Additionally, upon importing an TTX file, this option cause glyphs |
Just | 98d780d | 1999-12-23 15:16:22 +0000 | [diff] [blame] | 105 | to be compiled right away. This should reduce memory consumption |
| 106 | greatly, and therefore should have some impact on the time needed |
| 107 | to parse/compile large fonts. |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 108 | |
| 109 | If the allowVID argument is set to true, then virtual GID's are |
| 110 | supported. Asking for a glyph ID with a glyph name or GID that is not in |
| 111 | the font will return a virtual GID. This is valid for GSUB and cmap |
| 112 | tables. For SING glyphlets, the cmap table is used to specify Unicode |
Behdad Esfahbod | ebefbba | 2013-12-04 22:07:18 -0500 | [diff] [blame] | 113 | values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 114 | and does not exist in the font, or the glyphname has the form glyphN |
| 115 | and does not exist in the font, then N is used as the virtual GID. |
| 116 | Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new |
| 117 | virtual GIDs, the next is one less than the previous. |
jvr | 2545f16 | 2008-03-01 09:30:17 +0000 | [diff] [blame] | 118 | |
| 119 | If ignoreDecompileErrors is set to True, exceptions raised in |
| 120 | individual tables during decompilation will be ignored, falling |
| 121 | back to the DefaultTable implementation, which simply keeps the |
| 122 | binary data. |
Behdad Esfahbod | 7ef23a8 | 2013-11-24 19:03:18 -0500 | [diff] [blame] | 123 | |
| 124 | If lazy is set to True, many data structures are loaded lazily, upon |
| 125 | access only. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 126 | """ |
| 127 | |
Behdad Esfahbod | d8b32bf | 2013-08-19 15:27:14 -0400 | [diff] [blame] | 128 | from fontTools.ttLib import sfnt |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 129 | self.verbose = verbose |
Dave Crossland | d7efd56 | 2013-09-04 14:51:16 +0100 | [diff] [blame] | 130 | self.quiet = quiet |
Behdad Esfahbod | 7ef23a8 | 2013-11-24 19:03:18 -0500 | [diff] [blame] | 131 | self.lazy = lazy |
Just | 88cb4f3 | 1999-12-18 18:06:25 +0000 | [diff] [blame] | 132 | self.recalcBBoxes = recalcBBoxes |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 133 | self.tables = {} |
| 134 | self.reader = None |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 135 | |
| 136 | # Permit the user to reference glyphs that are not int the font. |
| 137 | self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. |
| 138 | self.reverseVIDDict = {} |
| 139 | self.VIDDict = {} |
| 140 | self.allowVID = allowVID |
jvr | 2545f16 | 2008-03-01 09:30:17 +0000 | [diff] [blame] | 141 | self.ignoreDecompileErrors = ignoreDecompileErrors |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 142 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 143 | if not file: |
| 144 | self.sfntVersion = sfntVersion |
Behdad Esfahbod | 58d7416 | 2013-08-15 15:30:55 -0400 | [diff] [blame] | 145 | self.flavor = flavor |
| 146 | self.flavorData = None |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 147 | return |
jvr | 0bb3ba5 | 2003-08-22 18:52:22 +0000 | [diff] [blame] | 148 | if not hasattr(file, "read"): |
| 149 | # assume file is a string |
| 150 | if haveMacSupport and res_name_or_index is not None: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 151 | # on the mac, we deal with sfnt resources as well as flat files |
Behdad Esfahbod | 2b06aaa | 2013-11-27 02:34:11 -0500 | [diff] [blame] | 152 | from . import macUtils |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 153 | if res_name_or_index == 0: |
| 154 | if macUtils.getSFNTResIndices(file): |
| 155 | # get the first available sfnt font. |
| 156 | file = macUtils.SFNTResourceReader(file, 1) |
| 157 | else: |
| 158 | file = open(file, "rb") |
| 159 | else: |
| 160 | file = macUtils.SFNTResourceReader(file, res_name_or_index) |
| 161 | else: |
| 162 | file = open(file, "rb") |
| 163 | else: |
| 164 | pass # assume "file" is a readable file object |
pabs3 | 7e91e77 | 2009-02-22 08:55:00 +0000 | [diff] [blame] | 165 | self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 166 | self.sfntVersion = self.reader.sfntVersion |
Behdad Esfahbod | 58d7416 | 2013-08-15 15:30:55 -0400 | [diff] [blame] | 167 | self.flavor = self.reader.flavor |
| 168 | self.flavorData = self.reader.flavorData |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 169 | |
| 170 | def close(self): |
| 171 | """If we still have a reader object, close it.""" |
| 172 | if self.reader is not None: |
| 173 | self.reader.close() |
| 174 | |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 175 | def save(self, file, makeSuitcase=False, reorderTables=True): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 176 | """Save the font to disk. Similarly to the constructor, |
| 177 | the 'file' argument can be either a pathname or a writable |
| 178 | file object. |
| 179 | |
Just | 88cb4f3 | 1999-12-18 18:06:25 +0000 | [diff] [blame] | 180 | On the Mac, if makeSuitcase is true, a suitcase (resource fork) |
| 181 | file will we made instead of a flat .ttf file. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 182 | """ |
Just | 0f67586 | 2000-10-02 07:51:42 +0000 | [diff] [blame] | 183 | from fontTools.ttLib import sfnt |
jvr | 0bb3ba5 | 2003-08-22 18:52:22 +0000 | [diff] [blame] | 184 | if not hasattr(file, "write"): |
Just | 0f67586 | 2000-10-02 07:51:42 +0000 | [diff] [blame] | 185 | closeStream = 1 |
Just | 88cb4f3 | 1999-12-18 18:06:25 +0000 | [diff] [blame] | 186 | if os.name == "mac" and makeSuitcase: |
Behdad Esfahbod | 2b06aaa | 2013-11-27 02:34:11 -0500 | [diff] [blame] | 187 | from . import macUtils |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 188 | file = macUtils.SFNTResourceWriter(file, self) |
| 189 | else: |
| 190 | file = open(file, "wb") |
| 191 | if os.name == "mac": |
jvr | 91bca42 | 2012-10-18 12:49:22 +0000 | [diff] [blame] | 192 | from fontTools.misc.macCreator import setMacCreatorAndType |
| 193 | setMacCreatorAndType(file.name, 'mdos', 'BINA') |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 194 | else: |
Just | 0f67586 | 2000-10-02 07:51:42 +0000 | [diff] [blame] | 195 | # assume "file" is a writable file object |
| 196 | closeStream = 0 |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 197 | |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 198 | tags = list(self.keys()) |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 199 | if "GlyphOrder" in tags: |
| 200 | tags.remove("GlyphOrder") |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 201 | numTables = len(tags) |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 202 | if reorderTables: |
| 203 | import tempfile |
| 204 | tmp = tempfile.TemporaryFile(prefix="ttx-fonttools") |
| 205 | else: |
| 206 | tmp = file |
Behdad Esfahbod | b0dc6df | 2013-08-15 17:39:16 -0400 | [diff] [blame] | 207 | writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 208 | |
| 209 | done = [] |
| 210 | for tag in tags: |
| 211 | self._writeTable(tag, writer, done) |
| 212 | |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 213 | writer.close() |
| 214 | |
| 215 | if reorderTables: |
| 216 | tmp.flush() |
| 217 | tmp.seek(0) |
| 218 | reorderFontTables(tmp, file) |
| 219 | tmp.close() |
| 220 | |
| 221 | if closeStream: |
| 222 | file.close() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 223 | |
Behdad Esfahbod | 38fdae6 | 2013-11-24 18:49:35 -0500 | [diff] [blame] | 224 | def saveXML(self, fileOrPath, progress=None, quiet=False, |
| 225 | tables=None, skipTables=None, splitTables=False, disassembleInstructions=True, |
Matt Fontaine | c33b0a2 | 2013-08-19 14:13:05 -0400 | [diff] [blame] | 226 | bitmapGlyphDataFormat='raw'): |
Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 227 | """Export the font as TTX (an XML-based text file), or as a series of text |
Just | 7dcc916 | 1999-12-29 13:06:08 +0000 | [diff] [blame] | 228 | files when splitTables is true. In the latter case, the 'fileOrPath' |
| 229 | argument should be a path to a directory. |
Just | ff3499d | 2000-01-05 20:43:36 +0000 | [diff] [blame] | 230 | The 'tables' argument must either be false (dump all tables) or a |
| 231 | list of tables to dump. The 'skipTables' argument may be a list of tables |
| 232 | to skip, but only when the 'tables' argument is false. |
Just | 7dcc916 | 1999-12-29 13:06:08 +0000 | [diff] [blame] | 233 | """ |
jvr | d04a3bb | 2002-05-02 15:23:25 +0000 | [diff] [blame] | 234 | from fontTools import version |
Behdad Esfahbod | f65033e | 2013-09-17 16:41:32 -0400 | [diff] [blame] | 235 | from fontTools.misc import xmlWriter |
Just | 5360248 | 2000-02-01 15:29:03 +0000 | [diff] [blame] | 236 | |
| 237 | self.disassembleInstructions = disassembleInstructions |
Matt Fontaine | c33b0a2 | 2013-08-19 14:13:05 -0400 | [diff] [blame] | 238 | self.bitmapGlyphDataFormat = bitmapGlyphDataFormat |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 239 | if not tables: |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 240 | tables = list(self.keys()) |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 241 | if "GlyphOrder" not in tables: |
| 242 | tables = ["GlyphOrder"] + tables |
Just | ff3499d | 2000-01-05 20:43:36 +0000 | [diff] [blame] | 243 | if skipTables: |
| 244 | for tag in skipTables: |
| 245 | if tag in tables: |
| 246 | tables.remove(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 247 | numTables = len(tables) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 248 | if progress: |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 249 | progress.set(0, numTables) |
| 250 | idlefunc = getattr(progress, "idle", None) |
| 251 | else: |
| 252 | idlefunc = None |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 253 | |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 254 | writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc) |
Behdad Esfahbod | dc7e6f3 | 2013-11-27 02:44:56 -0500 | [diff] [blame] | 255 | writer.begintag("ttFont", sfntVersion=repr(self.sfntVersion)[1:-1], |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 256 | ttLibVersion=version) |
| 257 | writer.newline() |
| 258 | |
Just | d28479b | 1999-12-27 19:48:21 +0000 | [diff] [blame] | 259 | if not splitTables: |
Just | d28479b | 1999-12-27 19:48:21 +0000 | [diff] [blame] | 260 | writer.newline() |
| 261 | else: |
jvr | 0f29303 | 2002-05-11 21:18:12 +0000 | [diff] [blame] | 262 | # 'fileOrPath' must now be a path |
| 263 | path, ext = os.path.splitext(fileOrPath) |
| 264 | fileNameTemplate = path + ".%s" + ext |
Just | d28479b | 1999-12-27 19:48:21 +0000 | [diff] [blame] | 265 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 266 | for i in range(numTables): |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 267 | if progress: |
| 268 | progress.set(i) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 269 | tag = tables[i] |
Just | d28479b | 1999-12-27 19:48:21 +0000 | [diff] [blame] | 270 | if splitTables: |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 271 | tablePath = fileNameTemplate % tagToIdentifier(tag) |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 272 | tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc) |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 273 | tableWriter.begintag("ttFont", ttLibVersion=version) |
| 274 | tableWriter.newline() |
| 275 | tableWriter.newline() |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 276 | writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) |
Just | d28479b | 1999-12-27 19:48:21 +0000 | [diff] [blame] | 277 | writer.newline() |
jvr | 0ecc433 | 2002-05-15 07:50:06 +0000 | [diff] [blame] | 278 | else: |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 279 | tableWriter = writer |
Dave Crossland | d7efd56 | 2013-09-04 14:51:16 +0100 | [diff] [blame] | 280 | self._tableToXML(tableWriter, tag, progress, quiet) |
Just | d28479b | 1999-12-27 19:48:21 +0000 | [diff] [blame] | 281 | if splitTables: |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 282 | tableWriter.endtag("ttFont") |
| 283 | tableWriter.newline() |
| 284 | tableWriter.close() |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 285 | if progress: |
| 286 | progress.set((i + 1)) |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 287 | writer.endtag("ttFont") |
| 288 | writer.newline() |
| 289 | writer.close() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 290 | if self.verbose: |
Just | 9682b41 | 2000-01-17 18:58:46 +0000 | [diff] [blame] | 291 | debugmsg("Done dumping TTX") |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 292 | |
Dave Crossland | d7efd56 | 2013-09-04 14:51:16 +0100 | [diff] [blame] | 293 | def _tableToXML(self, writer, tag, progress, quiet): |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 294 | if tag in self: |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 295 | table = self[tag] |
| 296 | report = "Dumping '%s' table..." % tag |
| 297 | else: |
| 298 | report = "No '%s' table found." % tag |
| 299 | if progress: |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 300 | progress.setLabel(report) |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 301 | elif self.verbose: |
| 302 | debugmsg(report) |
| 303 | else: |
Dave Crossland | d7efd56 | 2013-09-04 14:51:16 +0100 | [diff] [blame] | 304 | if not quiet: |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 305 | print(report) |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 306 | if tag not in self: |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 307 | return |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 308 | xmlTag = tagToXML(tag) |
jvr | fe66577 | 2002-05-22 20:15:10 +0000 | [diff] [blame] | 309 | if hasattr(table, "ERROR"): |
| 310 | writer.begintag(xmlTag, ERROR="decompilation error") |
| 311 | else: |
| 312 | writer.begintag(xmlTag) |
| 313 | writer.newline() |
| 314 | if tag in ("glyf", "CFF "): |
| 315 | table.toXML(writer, self, progress) |
| 316 | else: |
| 317 | table.toXML(writer, self) |
| 318 | writer.endtag(xmlTag) |
| 319 | writer.newline() |
| 320 | writer.newline() |
| 321 | |
Behdad Esfahbod | 38fdae6 | 2013-11-24 18:49:35 -0500 | [diff] [blame] | 322 | def importXML(self, file, progress=None, quiet=False): |
jvr | ca4c456 | 2002-05-01 21:06:11 +0000 | [diff] [blame] | 323 | """Import a TTX file (an XML-based text format), so as to recreate |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 324 | a font object. |
| 325 | """ |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 326 | if "maxp" in self and "post" in self: |
jvr | d57c434 | 2002-05-25 15:28:48 +0000 | [diff] [blame] | 327 | # Make sure the glyph order is loaded, as it otherwise gets |
| 328 | # lost if the XML doesn't contain the glyph order, yet does |
| 329 | # contain the table which was originally used to extract the |
| 330 | # glyph names from (ie. 'post', 'cmap' or 'CFF '). |
jvr | 22f0689 | 2002-05-25 14:56:29 +0000 | [diff] [blame] | 331 | self.getGlyphOrder() |
Behdad Esfahbod | 3ebfea4 | 2013-11-24 19:00:59 -0500 | [diff] [blame] | 332 | |
| 333 | from fontTools.misc import xmlReader |
| 334 | |
| 335 | reader = xmlReader.XMLReader(file, self, progress, quiet) |
| 336 | reader.read() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 337 | |
| 338 | def isLoaded(self, tag): |
| 339 | """Return true if the table identified by 'tag' has been |
| 340 | decompiled and loaded into memory.""" |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 341 | return tag in self.tables |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 342 | |
| 343 | def has_key(self, tag): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 344 | if self.isLoaded(tag): |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 345 | return True |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 346 | elif self.reader and tag in self.reader: |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 347 | return True |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 348 | elif tag == "GlyphOrder": |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 349 | return True |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 350 | else: |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 351 | return False |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 352 | |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 353 | __contains__ = has_key |
| 354 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 355 | def keys(self): |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 356 | keys = list(self.tables.keys()) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 357 | if self.reader: |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 358 | for key in list(self.reader.keys()): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 359 | if key not in keys: |
| 360 | keys.append(key) |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 361 | |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 362 | if "GlyphOrder" in keys: |
| 363 | keys.remove("GlyphOrder") |
| 364 | keys = sortedTagList(keys) |
| 365 | return ["GlyphOrder"] + keys |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 366 | |
| 367 | def __len__(self): |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 368 | return len(list(self.keys())) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 369 | |
| 370 | def __getitem__(self, tag): |
Behdad Esfahbod | 960280b | 2013-11-27 18:16:43 -0500 | [diff] [blame] | 371 | tag = Tag(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 372 | try: |
| 373 | return self.tables[tag] |
| 374 | except KeyError: |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 375 | if tag == "GlyphOrder": |
Behdad Esfahbod | c4af396 | 2013-08-28 17:12:12 -0400 | [diff] [blame] | 376 | table = GlyphOrder(tag) |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 377 | self.tables[tag] = table |
| 378 | return table |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 379 | if self.reader is not None: |
Just | f8fd477 | 2000-01-03 23:00:10 +0000 | [diff] [blame] | 380 | import traceback |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 381 | if self.verbose: |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 382 | debugmsg("Reading '%s' table from disk" % tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 383 | data = self.reader[tag] |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 384 | tableClass = getTableClass(tag) |
| 385 | table = tableClass(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 386 | self.tables[tag] = table |
| 387 | if self.verbose: |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 388 | debugmsg("Decompiling '%s' table" % tag) |
Just | f8fd477 | 2000-01-03 23:00:10 +0000 | [diff] [blame] | 389 | try: |
| 390 | table.decompile(data, self) |
jvr | 2545f16 | 2008-03-01 09:30:17 +0000 | [diff] [blame] | 391 | except: |
| 392 | if not self.ignoreDecompileErrors: |
| 393 | raise |
| 394 | # fall back to DefaultTable, retaining the binary table data |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 395 | print("An exception occurred during the decompilation of the '%s' table" % tag) |
Behdad Esfahbod | 2b06aaa | 2013-11-27 02:34:11 -0500 | [diff] [blame] | 396 | from .tables.DefaultTable import DefaultTable |
Behdad Esfahbod | b92c080 | 2013-11-27 05:05:46 -0500 | [diff] [blame] | 397 | file = StringIO() |
Just | f8fd477 | 2000-01-03 23:00:10 +0000 | [diff] [blame] | 398 | traceback.print_exc(file=file) |
| 399 | table = DefaultTable(tag) |
| 400 | table.ERROR = file.getvalue() |
| 401 | self.tables[tag] = table |
| 402 | table.decompile(data, self) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 403 | return table |
| 404 | else: |
Behdad Esfahbod | cd5aad9 | 2013-11-27 02:42:28 -0500 | [diff] [blame] | 405 | raise KeyError("'%s' table not found" % tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 406 | |
| 407 | def __setitem__(self, tag, table): |
Behdad Esfahbod | 5cf4008 | 2013-11-27 19:51:59 -0500 | [diff] [blame] | 408 | self.tables[Tag(tag)] = table |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 409 | |
| 410 | def __delitem__(self, tag): |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 411 | if tag not in self: |
Behdad Esfahbod | cd5aad9 | 2013-11-27 02:42:28 -0500 | [diff] [blame] | 412 | raise KeyError("'%s' table not found" % tag) |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 413 | if tag in self.tables: |
jvr | f707463 | 2002-05-04 22:04:02 +0000 | [diff] [blame] | 414 | del self.tables[tag] |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 415 | if self.reader and tag in self.reader: |
jvr | f707463 | 2002-05-04 22:04:02 +0000 | [diff] [blame] | 416 | del self.reader[tag] |
Behdad Esfahbod | 9c5e2ce | 2013-12-19 11:38:56 -0500 | [diff] [blame] | 417 | |
| 418 | def get(self, tag, default=None): |
| 419 | try: |
| 420 | return self[tag] |
| 421 | except KeyError: |
| 422 | return default |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 423 | |
| 424 | def setGlyphOrder(self, glyphOrder): |
| 425 | self.glyphOrder = glyphOrder |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 426 | |
| 427 | def getGlyphOrder(self): |
jvr | cf4b3b3 | 2002-05-05 09:48:31 +0000 | [diff] [blame] | 428 | try: |
| 429 | return self.glyphOrder |
| 430 | except AttributeError: |
| 431 | pass |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 432 | if 'CFF ' in self: |
jvr | 0b63b28 | 2002-05-13 11:26:38 +0000 | [diff] [blame] | 433 | cff = self['CFF '] |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 434 | self.glyphOrder = cff.getGlyphOrder() |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 435 | elif 'post' in self: |
jvr | cf4b3b3 | 2002-05-05 09:48:31 +0000 | [diff] [blame] | 436 | # TrueType font |
| 437 | glyphOrder = self['post'].getGlyphOrder() |
| 438 | if glyphOrder is None: |
| 439 | # |
| 440 | # No names found in the 'post' table. |
| 441 | # Try to create glyph names from the unicode cmap (if available) |
| 442 | # in combination with the Adobe Glyph List (AGL). |
| 443 | # |
Just | c91a951 | 2000-08-23 12:31:52 +0000 | [diff] [blame] | 444 | self._getGlyphNamesFromCmap() |
jvr | cf4b3b3 | 2002-05-05 09:48:31 +0000 | [diff] [blame] | 445 | else: |
| 446 | self.glyphOrder = glyphOrder |
| 447 | else: |
| 448 | self._getGlyphNamesFromCmap() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 449 | return self.glyphOrder |
| 450 | |
| 451 | def _getGlyphNamesFromCmap(self): |
jvr | 9f1e14b | 2002-05-05 11:29:33 +0000 | [diff] [blame] | 452 | # |
| 453 | # This is rather convoluted, but then again, it's an interesting problem: |
| 454 | # - we need to use the unicode values found in the cmap table to |
| 455 | # build glyph names (eg. because there is only a minimal post table, |
| 456 | # or none at all). |
| 457 | # - but the cmap parser also needs glyph names to work with... |
| 458 | # So here's what we do: |
| 459 | # - make up glyph names based on glyphID |
| 460 | # - load a temporary cmap table based on those names |
| 461 | # - extract the unicode values, build the "real" glyph names |
| 462 | # - unload the temporary cmap table |
| 463 | # |
| 464 | if self.isLoaded("cmap"): |
| 465 | # Bootstrapping: we're getting called by the cmap parser |
| 466 | # itself. This means self.tables['cmap'] contains a partially |
| 467 | # loaded cmap, making it impossible to get at a unicode |
| 468 | # subtable here. We remove the partially loaded cmap and |
| 469 | # restore it later. |
| 470 | # This only happens if the cmap table is loaded before any |
| 471 | # other table that does f.getGlyphOrder() or f.getGlyphName(). |
| 472 | cmapLoading = self.tables['cmap'] |
| 473 | del self.tables['cmap'] |
| 474 | else: |
| 475 | cmapLoading = None |
| 476 | # Make up glyph names based on glyphID, which will be used by the |
| 477 | # temporary cmap and by the real cmap in case we don't find a unicode |
| 478 | # cmap. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 479 | numGlyphs = int(self['maxp'].numGlyphs) |
| 480 | glyphOrder = [None] * numGlyphs |
| 481 | glyphOrder[0] = ".notdef" |
| 482 | for i in range(1, numGlyphs): |
| 483 | glyphOrder[i] = "glyph%.5d" % i |
| 484 | # Set the glyph order, so the cmap parser has something |
jvr | 9f1e14b | 2002-05-05 11:29:33 +0000 | [diff] [blame] | 485 | # to work with (so we don't get called recursively). |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 486 | self.glyphOrder = glyphOrder |
jvr | 9f1e14b | 2002-05-05 11:29:33 +0000 | [diff] [blame] | 487 | # Get a (new) temporary cmap (based on the just invented names) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 488 | tempcmap = self['cmap'].getcmap(3, 1) |
| 489 | if tempcmap is not None: |
| 490 | # we have a unicode cmap |
Just | 7dcc916 | 1999-12-29 13:06:08 +0000 | [diff] [blame] | 491 | from fontTools import agl |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 492 | cmap = tempcmap.cmap |
| 493 | # create a reverse cmap dict |
| 494 | reversecmap = {} |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 495 | for unicode, name in list(cmap.items()): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 496 | reversecmap[name] = unicode |
Just | a556f51 | 2001-02-23 21:58:57 +0000 | [diff] [blame] | 497 | allNames = {} |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 498 | for i in range(numGlyphs): |
| 499 | tempName = glyphOrder[i] |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 500 | if tempName in reversecmap: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 501 | unicode = reversecmap[tempName] |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 502 | if unicode in agl.UV2AGL: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 503 | # get name from the Adobe Glyph List |
Just | a556f51 | 2001-02-23 21:58:57 +0000 | [diff] [blame] | 504 | glyphName = agl.UV2AGL[unicode] |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 505 | else: |
| 506 | # create uni<CODE> name |
Behdad Esfahbod | 14fb031 | 2013-11-27 05:47:34 -0500 | [diff] [blame] | 507 | glyphName = "uni%04X" % unicode |
Just | a556f51 | 2001-02-23 21:58:57 +0000 | [diff] [blame] | 508 | tempName = glyphName |
| 509 | n = 1 |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 510 | while tempName in allNames: |
Behdad Esfahbod | dc7e6f3 | 2013-11-27 02:44:56 -0500 | [diff] [blame] | 511 | tempName = glyphName + "#" + repr(n) |
Just | a556f51 | 2001-02-23 21:58:57 +0000 | [diff] [blame] | 512 | n = n + 1 |
| 513 | glyphOrder[i] = tempName |
| 514 | allNames[tempName] = 1 |
jvr | 9f1e14b | 2002-05-05 11:29:33 +0000 | [diff] [blame] | 515 | # Delete the temporary cmap table from the cache, so it can |
| 516 | # be parsed again with the right names. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 517 | del self.tables['cmap'] |
| 518 | else: |
| 519 | pass # no unicode cmap available, stick with the invented names |
| 520 | self.glyphOrder = glyphOrder |
jvr | 9f1e14b | 2002-05-05 11:29:33 +0000 | [diff] [blame] | 521 | if cmapLoading: |
| 522 | # restore partially loaded cmap, so it can continue loading |
| 523 | # using the proper names. |
| 524 | self.tables['cmap'] = cmapLoading |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 525 | |
| 526 | def getGlyphNames(self): |
| 527 | """Get a list of glyph names, sorted alphabetically.""" |
Behdad Esfahbod | ac1b435 | 2013-11-27 04:15:34 -0500 | [diff] [blame] | 528 | glyphNames = sorted(self.getGlyphOrder()[:]) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 529 | return glyphNames |
| 530 | |
| 531 | def getGlyphNames2(self): |
Just | 8bc8cf8 | 1999-12-17 12:54:19 +0000 | [diff] [blame] | 532 | """Get a list of glyph names, sorted alphabetically, |
| 533 | but not case sensitive. |
| 534 | """ |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 535 | from fontTools.misc import textTools |
| 536 | return textTools.caselessSort(self.getGlyphOrder()) |
| 537 | |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 538 | def getGlyphName(self, glyphID, requireReal=False): |
jvr | 0b63b28 | 2002-05-13 11:26:38 +0000 | [diff] [blame] | 539 | try: |
| 540 | return self.getGlyphOrder()[glyphID] |
| 541 | except IndexError: |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 542 | if requireReal or not self.allowVID: |
| 543 | # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in |
| 544 | # the cmap table than there are glyphs. I don't think it's legal... |
| 545 | return "glyph%.5d" % glyphID |
| 546 | else: |
| 547 | # user intends virtual GID support |
| 548 | try: |
| 549 | glyphName = self.VIDDict[glyphID] |
| 550 | except KeyError: |
| 551 | glyphName ="glyph%.5d" % glyphID |
| 552 | self.last_vid = min(glyphID, self.last_vid ) |
| 553 | self.reverseVIDDict[glyphName] = glyphID |
| 554 | self.VIDDict[glyphID] = glyphName |
| 555 | return glyphName |
| 556 | |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 557 | def getGlyphID(self, glyphName, requireReal=False): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 558 | if not hasattr(self, "_reverseGlyphOrderDict"): |
| 559 | self._buildReverseGlyphOrderDict() |
| 560 | glyphOrder = self.getGlyphOrder() |
| 561 | d = self._reverseGlyphOrderDict |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 562 | if glyphName not in d: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 563 | if glyphName in glyphOrder: |
| 564 | self._buildReverseGlyphOrderDict() |
| 565 | return self.getGlyphID(glyphName) |
| 566 | else: |
Behdad Esfahbod | e5bee37 | 2013-12-04 22:46:29 -0500 | [diff] [blame] | 567 | if requireReal: |
Behdad Esfahbod | cd5aad9 | 2013-11-27 02:42:28 -0500 | [diff] [blame] | 568 | raise KeyError(glyphName) |
Behdad Esfahbod | e5bee37 | 2013-12-04 22:46:29 -0500 | [diff] [blame] | 569 | elif not self.allowVID: |
| 570 | # Handle glyphXXX only |
| 571 | if glyphName[:5] == "glyph": |
| 572 | try: |
| 573 | return int(glyphName[5:]) |
| 574 | except (NameError, ValueError): |
| 575 | raise KeyError(glyphName) |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 576 | else: |
| 577 | # user intends virtual GID support |
| 578 | try: |
| 579 | glyphID = self.reverseVIDDict[glyphName] |
| 580 | except KeyError: |
| 581 | # if name is in glyphXXX format, use the specified name. |
| 582 | if glyphName[:5] == "glyph": |
| 583 | try: |
| 584 | glyphID = int(glyphName[5:]) |
| 585 | except (NameError, ValueError): |
| 586 | glyphID = None |
Behdad Esfahbod | 9e6ef94 | 2013-12-04 16:31:44 -0500 | [diff] [blame] | 587 | if glyphID is None: |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 588 | glyphID = self.last_vid -1 |
| 589 | self.last_vid = glyphID |
| 590 | self.reverseVIDDict[glyphName] = glyphID |
| 591 | self.VIDDict[glyphID] = glyphName |
| 592 | return glyphID |
| 593 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 594 | glyphID = d[glyphName] |
Behdad Esfahbod | 180ace6 | 2013-11-27 02:40:30 -0500 | [diff] [blame] | 595 | if glyphName != glyphOrder[glyphID]: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 596 | self._buildReverseGlyphOrderDict() |
| 597 | return self.getGlyphID(glyphName) |
| 598 | return glyphID |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 599 | |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 600 | def getReverseGlyphMap(self, rebuild=False): |
jvr | 823f8cd | 2006-10-21 14:12:38 +0000 | [diff] [blame] | 601 | if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): |
| 602 | self._buildReverseGlyphOrderDict() |
| 603 | return self._reverseGlyphOrderDict |
| 604 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 605 | def _buildReverseGlyphOrderDict(self): |
| 606 | self._reverseGlyphOrderDict = d = {} |
| 607 | glyphOrder = self.getGlyphOrder() |
| 608 | for glyphID in range(len(glyphOrder)): |
| 609 | d[glyphOrder[glyphID]] = glyphID |
| 610 | |
| 611 | def _writeTable(self, tag, writer, done): |
| 612 | """Internal helper function for self.save(). Keeps track of |
| 613 | inter-table dependencies. |
| 614 | """ |
| 615 | if tag in done: |
| 616 | return |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 617 | tableClass = getTableClass(tag) |
| 618 | for masterTable in tableClass.dependencies: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 619 | if masterTable not in done: |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 620 | if masterTable in self: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 621 | self._writeTable(masterTable, writer, done) |
| 622 | else: |
| 623 | done.append(masterTable) |
jvr | cf4b3b3 | 2002-05-05 09:48:31 +0000 | [diff] [blame] | 624 | tabledata = self.getTableData(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 625 | if self.verbose: |
| 626 | debugmsg("writing '%s' table to disk" % tag) |
| 627 | writer[tag] = tabledata |
| 628 | done.append(tag) |
| 629 | |
jvr | cf4b3b3 | 2002-05-05 09:48:31 +0000 | [diff] [blame] | 630 | def getTableData(self, tag): |
| 631 | """Returns raw table data, whether compiled or directly read from disk. |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 632 | """ |
Behdad Esfahbod | 5cf4008 | 2013-11-27 19:51:59 -0500 | [diff] [blame] | 633 | tag = Tag(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 634 | if self.isLoaded(tag): |
| 635 | if self.verbose: |
| 636 | debugmsg("compiling '%s' table" % tag) |
| 637 | return self.tables[tag].compile(self) |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 638 | elif self.reader and tag in self.reader: |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 639 | if self.verbose: |
jvr | 6ab979c | 2002-07-23 16:44:25 +0000 | [diff] [blame] | 640 | debugmsg("Reading '%s' table from disk" % tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 641 | return self.reader[tag] |
| 642 | else: |
Behdad Esfahbod | cd5aad9 | 2013-11-27 02:42:28 -0500 | [diff] [blame] | 643 | raise KeyError(tag) |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 644 | |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 645 | def getGlyphSet(self, preferCFF=True): |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 646 | """Return a generic GlyphSet, which is a dict-like object |
| 647 | mapping glyph names to glyph objects. The returned glyph objects |
| 648 | have a .draw() method that supports the Pen protocol, and will |
| 649 | have an attribute named 'width', but only *after* the .draw() method |
| 650 | has been called. |
| 651 | |
| 652 | If the font is CFF-based, the outlines will be taken from the 'CFF ' |
| 653 | table. Otherwise the outlines will be taken from the 'glyf' table. |
| 654 | If the font contains both a 'CFF ' and a 'glyf' table, you can use |
| 655 | the 'preferCFF' argument to specify which one should be taken. |
| 656 | """ |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 657 | if preferCFF and "CFF " in self: |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 658 | return list(self["CFF "].cff.values())[0].CharStrings |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 659 | if "glyf" in self: |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 660 | return _TTGlyphSet(self) |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 661 | if "CFF " in self: |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 662 | return list(self["CFF "].cff.values())[0].CharStrings |
Behdad Esfahbod | cd5aad9 | 2013-11-27 02:42:28 -0500 | [diff] [blame] | 663 | raise TTLibError("Font contains no outlines") |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 664 | |
| 665 | |
Behdad Esfahbod | e388db5 | 2013-11-28 14:26:58 -0500 | [diff] [blame] | 666 | class _TTGlyphSet(object): |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 667 | |
| 668 | """Generic dict-like GlyphSet class, meant as a TrueType counterpart |
| 669 | to CFF's CharString dict. See TTFont.getGlyphSet(). |
| 670 | """ |
| 671 | |
| 672 | # This class is distinct from the 'glyf' table itself because we need |
| 673 | # access to the 'hmtx' table, which could cause a dependency problem |
| 674 | # there when reading from XML. |
| 675 | |
| 676 | def __init__(self, ttFont): |
| 677 | self._ttFont = ttFont |
| 678 | |
| 679 | def keys(self): |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 680 | return list(self._ttFont["glyf"].keys()) |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 681 | |
| 682 | def has_key(self, glyphName): |
Behdad Esfahbod | bc5e1cb | 2013-11-27 02:33:03 -0500 | [diff] [blame] | 683 | return glyphName in self._ttFont["glyf"] |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 684 | |
| 685 | __contains__ = has_key |
| 686 | |
| 687 | def __getitem__(self, glyphName): |
| 688 | return _TTGlyph(glyphName, self._ttFont) |
| 689 | |
jvr | 2e4cc02 | 2005-03-08 09:50:56 +0000 | [diff] [blame] | 690 | def get(self, glyphName, default=None): |
| 691 | try: |
| 692 | return self[glyphName] |
| 693 | except KeyError: |
| 694 | return default |
| 695 | |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 696 | |
Behdad Esfahbod | e388db5 | 2013-11-28 14:26:58 -0500 | [diff] [blame] | 697 | class _TTGlyph(object): |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 698 | |
jvr | d028b7b | 2003-08-26 19:00:38 +0000 | [diff] [blame] | 699 | """Wrapper for a TrueType glyph that supports the Pen protocol, meaning |
| 700 | that it has a .draw() method that takes a pen object as its only |
| 701 | argument. Additionally there is a 'width' attribute. |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 702 | """ |
| 703 | |
| 704 | def __init__(self, glyphName, ttFont): |
| 705 | self._glyphName = glyphName |
| 706 | self._ttFont = ttFont |
jvr | d028b7b | 2003-08-26 19:00:38 +0000 | [diff] [blame] | 707 | self.width, self.lsb = self._ttFont['hmtx'][self._glyphName] |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 708 | |
| 709 | def draw(self, pen): |
jvr | d028b7b | 2003-08-26 19:00:38 +0000 | [diff] [blame] | 710 | """Draw the glyph onto Pen. See fontTools.pens.basePen for details |
| 711 | how that works. |
| 712 | """ |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 713 | glyfTable = self._ttFont['glyf'] |
| 714 | glyph = glyfTable[self._glyphName] |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 715 | if hasattr(glyph, "xMin"): |
jvr | d028b7b | 2003-08-26 19:00:38 +0000 | [diff] [blame] | 716 | offset = self.lsb - glyph.xMin |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 717 | else: |
| 718 | offset = 0 |
| 719 | if glyph.isComposite(): |
| 720 | for component in glyph: |
| 721 | glyphName, transform = component.getComponentInfo() |
| 722 | pen.addComponent(glyphName, transform) |
| 723 | else: |
| 724 | coordinates, endPts, flags = glyph.getCoordinates(glyfTable) |
| 725 | if offset: |
| 726 | coordinates = coordinates + (offset, 0) |
| 727 | start = 0 |
| 728 | for end in endPts: |
| 729 | end = end + 1 |
| 730 | contour = coordinates[start:end].tolist() |
| 731 | cFlags = flags[start:end].tolist() |
| 732 | start = end |
| 733 | if 1 not in cFlags: |
| 734 | # There is not a single on-curve point on the curve, |
| 735 | # use pen.qCurveTo's special case by specifying None |
| 736 | # as the on-curve point. |
| 737 | contour.append(None) |
| 738 | pen.qCurveTo(*contour) |
| 739 | else: |
jvr | 1c9917b | 2003-08-25 13:20:38 +0000 | [diff] [blame] | 740 | # Shuffle the points so that contour the is guaranteed |
| 741 | # to *end* in an on-curve point, which we'll use for |
| 742 | # the moveTo. |
jvr | 8df8f63 | 2003-08-25 13:15:50 +0000 | [diff] [blame] | 743 | firstOnCurve = cFlags.index(1) + 1 |
| 744 | contour = contour[firstOnCurve:] + contour[:firstOnCurve] |
| 745 | cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] |
| 746 | pen.moveTo(contour[-1]) |
| 747 | while contour: |
| 748 | nextOnCurve = cFlags.index(1) + 1 |
| 749 | if nextOnCurve == 1: |
| 750 | pen.lineTo(contour[0]) |
| 751 | else: |
| 752 | pen.qCurveTo(*contour[:nextOnCurve]) |
| 753 | contour = contour[nextOnCurve:] |
| 754 | cFlags = cFlags[nextOnCurve:] |
| 755 | pen.closePath() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 756 | |
| 757 | |
Behdad Esfahbod | e388db5 | 2013-11-28 14:26:58 -0500 | [diff] [blame] | 758 | class GlyphOrder(object): |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 759 | |
jvr | 22f0689 | 2002-05-25 14:56:29 +0000 | [diff] [blame] | 760 | """A pseudo table. The glyph order isn't in the font as a separate |
| 761 | table, but it's nice to present it as such in the TTX format. |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 762 | """ |
| 763 | |
Behdad Esfahbod | 50d6c72 | 2014-03-28 14:32:24 -0700 | [diff] [blame^] | 764 | def __init__(self, tag=None): |
Behdad Esfahbod | c4af396 | 2013-08-28 17:12:12 -0400 | [diff] [blame] | 765 | pass |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 766 | |
| 767 | def toXML(self, writer, ttFont): |
Behdad Esfahbod | c4af396 | 2013-08-28 17:12:12 -0400 | [diff] [blame] | 768 | glyphOrder = ttFont.getGlyphOrder() |
jvr | 4e5af60 | 2002-05-24 09:58:04 +0000 | [diff] [blame] | 769 | writer.comment("The 'id' attribute is only for humans; " |
| 770 | "it is ignored when parsed.") |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 771 | writer.newline() |
Behdad Esfahbod | c4af396 | 2013-08-28 17:12:12 -0400 | [diff] [blame] | 772 | for i in range(len(glyphOrder)): |
| 773 | glyphName = glyphOrder[i] |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 774 | writer.simpletag("GlyphID", id=i, name=glyphName) |
| 775 | writer.newline() |
| 776 | |
Behdad Esfahbod | 3a9fd30 | 2013-11-27 03:19:32 -0500 | [diff] [blame] | 777 | def fromXML(self, name, attrs, content, ttFont): |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 778 | if not hasattr(self, "glyphOrder"): |
| 779 | self.glyphOrder = [] |
| 780 | ttFont.setGlyphOrder(self.glyphOrder) |
| 781 | if name == "GlyphID": |
| 782 | self.glyphOrder.append(attrs["name"]) |
| 783 | |
| 784 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 785 | def getTableModule(tag): |
| 786 | """Fetch the packer/unpacker module for a table. |
| 787 | Return None when no module is found. |
| 788 | """ |
Behdad Esfahbod | 2b06aaa | 2013-11-27 02:34:11 -0500 | [diff] [blame] | 789 | from . import tables |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 790 | pyTag = tagToIdentifier(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 791 | try: |
jvr | 0bb3ba5 | 2003-08-22 18:52:22 +0000 | [diff] [blame] | 792 | __import__("fontTools.ttLib.tables." + pyTag) |
Behdad Esfahbod | 223273f | 2013-11-27 05:09:00 -0500 | [diff] [blame] | 793 | except ImportError as err: |
Behdad Esfahbod | 400e953 | 2013-08-16 10:53:36 -0400 | [diff] [blame] | 794 | # If pyTag is found in the ImportError message, |
| 795 | # means table is not implemented. If it's not |
| 796 | # there, then some other module is missing, don't |
| 797 | # suppress the error. |
| 798 | if str(err).find(pyTag) >= 0: |
| 799 | return None |
| 800 | else: |
| 801 | raise err |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 802 | else: |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 803 | return getattr(tables, pyTag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 804 | |
| 805 | |
| 806 | def getTableClass(tag): |
| 807 | """Fetch the packer/unpacker class for a table. |
| 808 | Return None when no class is found. |
| 809 | """ |
| 810 | module = getTableModule(tag) |
| 811 | if module is None: |
Behdad Esfahbod | 2b06aaa | 2013-11-27 02:34:11 -0500 | [diff] [blame] | 812 | from .tables.DefaultTable import DefaultTable |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 813 | return DefaultTable |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 814 | pyTag = tagToIdentifier(tag) |
| 815 | tableClass = getattr(module, "table_" + pyTag) |
| 816 | return tableClass |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 817 | |
| 818 | |
Behdad Esfahbod | d0a31f5 | 2014-03-28 14:04:01 -0700 | [diff] [blame] | 819 | def getClassTag(klass): |
| 820 | """Fetch the table tag for a class object.""" |
| 821 | name = klass.__name__ |
| 822 | assert name[:6] == 'table_' |
| 823 | name = name[6:] # Chop 'table_' |
| 824 | return identifierToTag(name) |
| 825 | |
| 826 | |
| 827 | |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 828 | def newTable(tag): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 829 | """Return a new instance of a table.""" |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 830 | tableClass = getTableClass(tag) |
| 831 | return tableClass(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 832 | |
| 833 | |
| 834 | def _escapechar(c): |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 835 | """Helper function for tagToIdentifier()""" |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 836 | import re |
| 837 | if re.match("[a-z0-9]", c): |
| 838 | return "_" + c |
| 839 | elif re.match("[A-Z]", c): |
| 840 | return c + "_" |
| 841 | else: |
Behdad Esfahbod | 319c5fd | 2013-11-27 18:13:48 -0500 | [diff] [blame] | 842 | return hex(byteord(c))[2:] |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 843 | |
| 844 | |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 845 | def tagToIdentifier(tag): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 846 | """Convert a table tag to a valid (but UGLY) python identifier, |
| 847 | as well as a filename that's guaranteed to be unique even on a |
| 848 | caseless file system. Each character is mapped to two characters. |
| 849 | Lowercase letters get an underscore before the letter, uppercase |
| 850 | letters get an underscore after the letter. Trailing spaces are |
| 851 | trimmed. Illegal characters are escaped as two hex bytes. If the |
| 852 | result starts with a number (as the result of a hex escape), an |
| 853 | extra underscore is prepended. Examples: |
| 854 | 'glyf' -> '_g_l_y_f' |
| 855 | 'cvt ' -> '_c_v_t' |
| 856 | 'OS/2' -> 'O_S_2f_2' |
| 857 | """ |
| 858 | import re |
Behdad Esfahbod | d2f5d2f | 2013-11-27 17:54:42 -0500 | [diff] [blame] | 859 | tag = Tag(tag) |
jvr | e5ae28e | 2002-05-25 08:22:22 +0000 | [diff] [blame] | 860 | if tag == "GlyphOrder": |
| 861 | return tag |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 862 | assert len(tag) == 4, "tag should be 4 characters long" |
| 863 | while len(tag) > 1 and tag[-1] == ' ': |
| 864 | tag = tag[:-1] |
| 865 | ident = "" |
| 866 | for c in tag: |
| 867 | ident = ident + _escapechar(c) |
| 868 | if re.match("[0-9]", ident): |
| 869 | ident = "_" + ident |
| 870 | return ident |
| 871 | |
| 872 | |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 873 | def identifierToTag(ident): |
| 874 | """the opposite of tagToIdentifier()""" |
jvr | e5ae28e | 2002-05-25 08:22:22 +0000 | [diff] [blame] | 875 | if ident == "GlyphOrder": |
| 876 | return ident |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 877 | if len(ident) % 2 and ident[0] == "_": |
| 878 | ident = ident[1:] |
| 879 | assert not (len(ident) % 2) |
| 880 | tag = "" |
| 881 | for i in range(0, len(ident), 2): |
| 882 | if ident[i] == "_": |
| 883 | tag = tag + ident[i+1] |
| 884 | elif ident[i+1] == "_": |
| 885 | tag = tag + ident[i] |
| 886 | else: |
| 887 | # assume hex |
Behdad Esfahbod | b7a2d79 | 2013-11-27 15:19:40 -0500 | [diff] [blame] | 888 | tag = tag + bytechr(int(ident[i:i+2], 16)) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 889 | # append trailing spaces |
| 890 | tag = tag + (4 - len(tag)) * ' ' |
Behdad Esfahbod | d2f5d2f | 2013-11-27 17:54:42 -0500 | [diff] [blame] | 891 | return Tag(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 892 | |
| 893 | |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 894 | def tagToXML(tag): |
| 895 | """Similarly to tagToIdentifier(), this converts a TT tag |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 896 | to a valid XML element name. Since XML element names are |
| 897 | case sensitive, this is a fairly simple/readable translation. |
| 898 | """ |
Just | 7dcc916 | 1999-12-29 13:06:08 +0000 | [diff] [blame] | 899 | import re |
Behdad Esfahbod | d2f5d2f | 2013-11-27 17:54:42 -0500 | [diff] [blame] | 900 | tag = Tag(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 901 | if tag == "OS/2": |
| 902 | return "OS_2" |
jvr | 0011bb6 | 2002-05-23 09:42:45 +0000 | [diff] [blame] | 903 | elif tag == "GlyphOrder": |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 904 | return tag |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 905 | if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): |
Behdad Esfahbod | 14fb031 | 2013-11-27 05:47:34 -0500 | [diff] [blame] | 906 | return tag.strip() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 907 | else: |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 908 | return tagToIdentifier(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 909 | |
| 910 | |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 911 | def xmlToTag(tag): |
| 912 | """The opposite of tagToXML()""" |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 913 | if tag == "OS_2": |
Behdad Esfahbod | 153ec40 | 2013-12-04 01:15:46 -0500 | [diff] [blame] | 914 | return Tag("OS/2") |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 915 | if len(tag) == 8: |
jvr | 8307fa4 | 2002-05-13 16:21:51 +0000 | [diff] [blame] | 916 | return identifierToTag(tag) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 917 | else: |
Behdad Esfahbod | 153ec40 | 2013-12-04 01:15:46 -0500 | [diff] [blame] | 918 | return Tag(tag + " " * (4 - len(tag))) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 919 | |
| 920 | |
| 921 | def debugmsg(msg): |
| 922 | import time |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 923 | print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time()))) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 924 | |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 925 | |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 926 | # Table order as recommended in the OpenType specification 1.4 |
| 927 | TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", |
| 928 | "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", |
| 929 | "kern", "name", "post", "gasp", "PCLT"] |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 930 | |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 931 | OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", |
| 932 | "CFF "] |
| 933 | |
| 934 | def sortedTagList(tagList, tableOrder=None): |
| 935 | """Return a sorted copy of tagList, sorted according to the OpenType |
| 936 | specification, or according to a custom tableOrder. If given and not |
| 937 | None, tableOrder needs to be a list of tag names. |
| 938 | """ |
Behdad Esfahbod | ac1b435 | 2013-11-27 04:15:34 -0500 | [diff] [blame] | 939 | tagList = sorted(tagList) |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 940 | if tableOrder is None: |
| 941 | if "DSIG" in tagList: |
| 942 | # DSIG should be last (XXX spec reference?) |
| 943 | tagList.remove("DSIG") |
| 944 | tagList.append("DSIG") |
| 945 | if "CFF " in tagList: |
| 946 | tableOrder = OTFTableOrder |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 947 | else: |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 948 | tableOrder = TTFTableOrder |
| 949 | orderedTables = [] |
| 950 | for tag in tableOrder: |
| 951 | if tag in tagList: |
| 952 | orderedTables.append(tag) |
| 953 | tagList.remove(tag) |
| 954 | orderedTables.extend(tagList) |
| 955 | return orderedTables |
jvr | 700df03 | 2003-08-22 19:44:08 +0000 | [diff] [blame] | 956 | |
| 957 | |
Behdad Esfahbod | dc87372 | 2013-12-04 21:28:50 -0500 | [diff] [blame] | 958 | def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 959 | """Rewrite a font file, ordering the tables as recommended by the |
| 960 | OpenType specification 1.4. |
| 961 | """ |
| 962 | from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter |
| 963 | reader = SFNTReader(inFile, checkChecksums=checkChecksums) |
Roozbeh Pournader | d7921e3 | 2013-10-08 21:29:22 -0700 | [diff] [blame] | 964 | writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) |
Behdad Esfahbod | c2297cd | 2013-11-27 06:26:55 -0500 | [diff] [blame] | 965 | tables = list(reader.keys()) |
jvr | 28ae196 | 2004-11-16 10:37:59 +0000 | [diff] [blame] | 966 | for tag in sortedTagList(tables, tableOrder): |
| 967 | writer[tag] = reader[tag] |
| 968 | writer.close() |