blob: b4f4ff088b9fc64877d0936d0742277558f4d3fa [file] [log] [blame]
Just9682b412000-01-17 18:58:46 +00001"""fontTools.ttLib -- a package for dealing with TrueType fonts.
Just7842e561999-12-16 21:34:53 +00002
3This package offers translators to convert TrueType fonts to Python
Just9682b412000-01-17 18:58:46 +00004objects and vice versa, and additionally from Python to TTX (an XML-based
5text format) and vice versa.
Just7842e561999-12-16 21:34:53 +00006
7Example interactive session:
8
9Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL]
10Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
11>>> from fontTools import ttLib
12>>> tt = ttLib.TTFont("afont.ttf")
13>>> tt['maxp'].numGlyphs
14242
15>>> tt['OS/2'].achVendID
16'B&H\000'
17>>> tt['head'].unitsPerEm
182048
Just9682b412000-01-17 18:58:46 +000019>>> tt.saveXML("afont.ttx")
Just7842e561999-12-16 21:34:53 +000020Dumping 'LTSH' table...
21Dumping 'OS/2' table...
22Dumping 'VDMX' table...
23Dumping 'cmap' table...
24Dumping 'cvt ' table...
25Dumping 'fpgm' table...
26Dumping 'glyf' table...
27Dumping 'hdmx' table...
28Dumping 'head' table...
29Dumping 'hhea' table...
30Dumping 'hmtx' table...
31Dumping 'loca' table...
32Dumping 'maxp' table...
33Dumping 'name' table...
34Dumping 'post' table...
35Dumping 'prep' table...
36>>> tt2 = ttLib.TTFont()
Just9682b412000-01-17 18:58:46 +000037>>> tt2.importXML("afont.ttx")
Just7842e561999-12-16 21:34:53 +000038>>> tt2['maxp'].numGlyphs
39242
40>>>
41
42"""
43
Just7dcc9161999-12-29 13:06:08 +000044#
pabs37e91e772009-02-22 08:55:00 +000045# $Id: __init__.py,v 1.51 2009-02-22 08:55:00 pabs3 Exp $
Just7dcc9161999-12-29 13:06:08 +000046#
47
Behdad Esfahbod1ae29592014-01-14 15:07:50 +080048from __future__ import print_function, division, absolute_import
Behdad Esfahbodbb0beb72013-11-27 14:37:28 -050049from fontTools.misc.py23 import *
Behdad Esfahbod30e691e2013-11-27 17:27:45 -050050import os
51import sys
jvr1cff4cb2003-08-28 18:23:43 +000052
53haveMacSupport = 0
54if sys.platform == "mac":
55 haveMacSupport = 1
56elif 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
Just7842e561999-12-16 21:34:53 +000059
jvrd04a3bb2002-05-02 15:23:25 +000060
Just7842e561999-12-16 21:34:53 +000061class TTLibError(Exception): pass
62
63
Behdad Esfahbode388db52013-11-28 14:26:58 -050064class TTFont(object):
Just7842e561999-12-16 21:34:53 +000065
66 """The main font object. It manages file input and output, and offers
67 a convenient way of accessing tables.
pabs31344bc92010-01-09 09:12:11 +000068 Tables will be only decompiled when necessary, ie. when they're actually
Just7842e561999-12-16 21:34:53 +000069 accessed. This means that simple operations can be extremely fast.
70 """
71
jvr0011bb62002-05-23 09:42:45 +000072 def __init__(self, file=None, res_name_or_index=None,
Behdad Esfahboddc873722013-12-04 21:28:50 -050073 sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False,
74 verbose=False, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
Behdad Esfahbod283fb262013-12-16 00:50:48 -050075 fontNumber=-1, lazy=False, quiet=False):
Just7842e561999-12-16 21:34:53 +000076
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
jvrea9dfa92002-05-12 17:14:50 +000087 The 'checkChecksums' argument is used to specify how sfnt
Just7842e561999-12-16 21:34:53 +000088 checksums are treated upon reading a file from disk:
89 0: don't check (default)
jvr0011bb62002-05-23 09:42:45 +000090 1: check, print warnings if a wrong checksum is found
Just7842e561999-12-16 21:34:53 +000091 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 Esfahbod58d74162013-08-15 15:30:55 -040095 In this case you can optionally supply the 'sfntVersion' argument,
96 and a 'flavor' which can be None, or 'woff'.
Just88cb4f31999-12-18 18:06:25 +000097
Just98d780d1999-12-23 15:16:22 +000098 If the recalcBBoxes argument is false, a number of things will *not*
Just3e097c61999-12-23 14:44:16 +000099 be recalculated upon save/compile:
Just98d780d1999-12-23 15:16:22 +0000100 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 ;-).
Just9682b412000-01-17 18:58:46 +0000104 Additionally, upon importing an TTX file, this option cause glyphs
Just98d780d1999-12-23 15:16:22 +0000105 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.
jvr823f8cd2006-10-21 14:12:38 +0000108
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 Esfahbodebefbba2013-12-04 22:07:18 -0500113 values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested
jvr823f8cd2006-10-21 14:12:38 +0000114 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.
jvr2545f162008-03-01 09:30:17 +0000118
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 Esfahbod7ef23a82013-11-24 19:03:18 -0500123
124 If lazy is set to True, many data structures are loaded lazily, upon
125 access only.
Just7842e561999-12-16 21:34:53 +0000126 """
127
Behdad Esfahbodd8b32bf2013-08-19 15:27:14 -0400128 from fontTools.ttLib import sfnt
Just7842e561999-12-16 21:34:53 +0000129 self.verbose = verbose
Dave Crosslandd7efd562013-09-04 14:51:16 +0100130 self.quiet = quiet
Behdad Esfahbod7ef23a82013-11-24 19:03:18 -0500131 self.lazy = lazy
Just88cb4f31999-12-18 18:06:25 +0000132 self.recalcBBoxes = recalcBBoxes
Just7842e561999-12-16 21:34:53 +0000133 self.tables = {}
134 self.reader = None
jvr823f8cd2006-10-21 14:12:38 +0000135
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
jvr2545f162008-03-01 09:30:17 +0000141 self.ignoreDecompileErrors = ignoreDecompileErrors
jvr823f8cd2006-10-21 14:12:38 +0000142
Just7842e561999-12-16 21:34:53 +0000143 if not file:
144 self.sfntVersion = sfntVersion
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400145 self.flavor = flavor
146 self.flavorData = None
Just7842e561999-12-16 21:34:53 +0000147 return
jvr0bb3ba52003-08-22 18:52:22 +0000148 if not hasattr(file, "read"):
149 # assume file is a string
150 if haveMacSupport and res_name_or_index is not None:
Just7842e561999-12-16 21:34:53 +0000151 # on the mac, we deal with sfnt resources as well as flat files
Behdad Esfahbod2b06aaa2013-11-27 02:34:11 -0500152 from . import macUtils
Just7842e561999-12-16 21:34:53 +0000153 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
pabs37e91e772009-02-22 08:55:00 +0000165 self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber)
Just7842e561999-12-16 21:34:53 +0000166 self.sfntVersion = self.reader.sfntVersion
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400167 self.flavor = self.reader.flavor
168 self.flavorData = self.reader.flavorData
Just7842e561999-12-16 21:34:53 +0000169
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 Esfahboddc873722013-12-04 21:28:50 -0500175 def save(self, file, makeSuitcase=False, reorderTables=True):
Just7842e561999-12-16 21:34:53 +0000176 """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
Just88cb4f31999-12-18 18:06:25 +0000180 On the Mac, if makeSuitcase is true, a suitcase (resource fork)
181 file will we made instead of a flat .ttf file.
Just7842e561999-12-16 21:34:53 +0000182 """
Just0f675862000-10-02 07:51:42 +0000183 from fontTools.ttLib import sfnt
jvr0bb3ba52003-08-22 18:52:22 +0000184 if not hasattr(file, "write"):
Just0f675862000-10-02 07:51:42 +0000185 closeStream = 1
Just88cb4f31999-12-18 18:06:25 +0000186 if os.name == "mac" and makeSuitcase:
Behdad Esfahbod2b06aaa2013-11-27 02:34:11 -0500187 from . import macUtils
Just7842e561999-12-16 21:34:53 +0000188 file = macUtils.SFNTResourceWriter(file, self)
189 else:
190 file = open(file, "wb")
191 if os.name == "mac":
jvr91bca422012-10-18 12:49:22 +0000192 from fontTools.misc.macCreator import setMacCreatorAndType
193 setMacCreatorAndType(file.name, 'mdos', 'BINA')
Just7842e561999-12-16 21:34:53 +0000194 else:
Just0f675862000-10-02 07:51:42 +0000195 # assume "file" is a writable file object
196 closeStream = 0
Just7842e561999-12-16 21:34:53 +0000197
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500198 tags = list(self.keys())
jvr700df032003-08-22 19:44:08 +0000199 if "GlyphOrder" in tags:
200 tags.remove("GlyphOrder")
Just7842e561999-12-16 21:34:53 +0000201 numTables = len(tags)
jvr28ae1962004-11-16 10:37:59 +0000202 if reorderTables:
203 import tempfile
204 tmp = tempfile.TemporaryFile(prefix="ttx-fonttools")
205 else:
206 tmp = file
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400207 writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData)
Just7842e561999-12-16 21:34:53 +0000208
209 done = []
210 for tag in tags:
211 self._writeTable(tag, writer, done)
212
jvr28ae1962004-11-16 10:37:59 +0000213 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()
Just7842e561999-12-16 21:34:53 +0000223
Behdad Esfahbod38fdae62013-11-24 18:49:35 -0500224 def saveXML(self, fileOrPath, progress=None, quiet=False,
225 tables=None, skipTables=None, splitTables=False, disassembleInstructions=True,
Matt Fontainec33b0a22013-08-19 14:13:05 -0400226 bitmapGlyphDataFormat='raw'):
Just9682b412000-01-17 18:58:46 +0000227 """Export the font as TTX (an XML-based text file), or as a series of text
Just7dcc9161999-12-29 13:06:08 +0000228 files when splitTables is true. In the latter case, the 'fileOrPath'
229 argument should be a path to a directory.
Justff3499d2000-01-05 20:43:36 +0000230 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.
Just7dcc9161999-12-29 13:06:08 +0000233 """
jvrd04a3bb2002-05-02 15:23:25 +0000234 from fontTools import version
Behdad Esfahbodf65033e2013-09-17 16:41:32 -0400235 from fontTools.misc import xmlWriter
Just53602482000-02-01 15:29:03 +0000236
237 self.disassembleInstructions = disassembleInstructions
Matt Fontainec33b0a22013-08-19 14:13:05 -0400238 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
Just7842e561999-12-16 21:34:53 +0000239 if not tables:
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500240 tables = list(self.keys())
jvr700df032003-08-22 19:44:08 +0000241 if "GlyphOrder" not in tables:
242 tables = ["GlyphOrder"] + tables
Justff3499d2000-01-05 20:43:36 +0000243 if skipTables:
244 for tag in skipTables:
245 if tag in tables:
246 tables.remove(tag)
Just7842e561999-12-16 21:34:53 +0000247 numTables = len(tables)
Just7842e561999-12-16 21:34:53 +0000248 if progress:
jvr6ab979c2002-07-23 16:44:25 +0000249 progress.set(0, numTables)
250 idlefunc = getattr(progress, "idle", None)
251 else:
252 idlefunc = None
jvrfe665772002-05-22 20:15:10 +0000253
jvr6ab979c2002-07-23 16:44:25 +0000254 writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc)
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500255 writer.begintag("ttFont", sfntVersion=repr(self.sfntVersion)[1:-1],
jvrfe665772002-05-22 20:15:10 +0000256 ttLibVersion=version)
257 writer.newline()
258
Justd28479b1999-12-27 19:48:21 +0000259 if not splitTables:
Justd28479b1999-12-27 19:48:21 +0000260 writer.newline()
261 else:
jvr0f293032002-05-11 21:18:12 +0000262 # 'fileOrPath' must now be a path
263 path, ext = os.path.splitext(fileOrPath)
264 fileNameTemplate = path + ".%s" + ext
Justd28479b1999-12-27 19:48:21 +0000265
Just7842e561999-12-16 21:34:53 +0000266 for i in range(numTables):
jvr6ab979c2002-07-23 16:44:25 +0000267 if progress:
268 progress.set(i)
Just7842e561999-12-16 21:34:53 +0000269 tag = tables[i]
Justd28479b1999-12-27 19:48:21 +0000270 if splitTables:
jvr8307fa42002-05-13 16:21:51 +0000271 tablePath = fileNameTemplate % tagToIdentifier(tag)
jvr6ab979c2002-07-23 16:44:25 +0000272 tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc)
jvrfe665772002-05-22 20:15:10 +0000273 tableWriter.begintag("ttFont", ttLibVersion=version)
274 tableWriter.newline()
275 tableWriter.newline()
jvr0011bb62002-05-23 09:42:45 +0000276 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
Justd28479b1999-12-27 19:48:21 +0000277 writer.newline()
jvr0ecc4332002-05-15 07:50:06 +0000278 else:
jvrfe665772002-05-22 20:15:10 +0000279 tableWriter = writer
Dave Crosslandd7efd562013-09-04 14:51:16 +0100280 self._tableToXML(tableWriter, tag, progress, quiet)
Justd28479b1999-12-27 19:48:21 +0000281 if splitTables:
jvrfe665772002-05-22 20:15:10 +0000282 tableWriter.endtag("ttFont")
283 tableWriter.newline()
284 tableWriter.close()
jvr6ab979c2002-07-23 16:44:25 +0000285 if progress:
286 progress.set((i + 1))
jvrfe665772002-05-22 20:15:10 +0000287 writer.endtag("ttFont")
288 writer.newline()
289 writer.close()
Just7842e561999-12-16 21:34:53 +0000290 if self.verbose:
Just9682b412000-01-17 18:58:46 +0000291 debugmsg("Done dumping TTX")
Just7842e561999-12-16 21:34:53 +0000292
Dave Crosslandd7efd562013-09-04 14:51:16 +0100293 def _tableToXML(self, writer, tag, progress, quiet):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500294 if tag in self:
jvrfe665772002-05-22 20:15:10 +0000295 table = self[tag]
296 report = "Dumping '%s' table..." % tag
297 else:
298 report = "No '%s' table found." % tag
299 if progress:
jvr6ab979c2002-07-23 16:44:25 +0000300 progress.setLabel(report)
jvrfe665772002-05-22 20:15:10 +0000301 elif self.verbose:
302 debugmsg(report)
303 else:
Dave Crosslandd7efd562013-09-04 14:51:16 +0100304 if not quiet:
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500305 print(report)
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500306 if tag not in self:
jvrfe665772002-05-22 20:15:10 +0000307 return
jvr0011bb62002-05-23 09:42:45 +0000308 xmlTag = tagToXML(tag)
jvrfe665772002-05-22 20:15:10 +0000309 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 Esfahbod38fdae62013-11-24 18:49:35 -0500322 def importXML(self, file, progress=None, quiet=False):
jvrca4c4562002-05-01 21:06:11 +0000323 """Import a TTX file (an XML-based text format), so as to recreate
Just7842e561999-12-16 21:34:53 +0000324 a font object.
325 """
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500326 if "maxp" in self and "post" in self:
jvrd57c4342002-05-25 15:28:48 +0000327 # 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 ').
jvr22f06892002-05-25 14:56:29 +0000331 self.getGlyphOrder()
Behdad Esfahbod3ebfea42013-11-24 19:00:59 -0500332
333 from fontTools.misc import xmlReader
334
335 reader = xmlReader.XMLReader(file, self, progress, quiet)
336 reader.read()
Just7842e561999-12-16 21:34:53 +0000337
338 def isLoaded(self, tag):
339 """Return true if the table identified by 'tag' has been
340 decompiled and loaded into memory."""
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500341 return tag in self.tables
Just7842e561999-12-16 21:34:53 +0000342
343 def has_key(self, tag):
Just7842e561999-12-16 21:34:53 +0000344 if self.isLoaded(tag):
Behdad Esfahboddc873722013-12-04 21:28:50 -0500345 return True
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500346 elif self.reader and tag in self.reader:
Behdad Esfahboddc873722013-12-04 21:28:50 -0500347 return True
jvr0011bb62002-05-23 09:42:45 +0000348 elif tag == "GlyphOrder":
Behdad Esfahboddc873722013-12-04 21:28:50 -0500349 return True
Just7842e561999-12-16 21:34:53 +0000350 else:
Behdad Esfahboddc873722013-12-04 21:28:50 -0500351 return False
Just7842e561999-12-16 21:34:53 +0000352
jvr8df8f632003-08-25 13:15:50 +0000353 __contains__ = has_key
354
Just7842e561999-12-16 21:34:53 +0000355 def keys(self):
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500356 keys = list(self.tables.keys())
Just7842e561999-12-16 21:34:53 +0000357 if self.reader:
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500358 for key in list(self.reader.keys()):
Just7842e561999-12-16 21:34:53 +0000359 if key not in keys:
360 keys.append(key)
jvr700df032003-08-22 19:44:08 +0000361
jvr28ae1962004-11-16 10:37:59 +0000362 if "GlyphOrder" in keys:
363 keys.remove("GlyphOrder")
364 keys = sortedTagList(keys)
365 return ["GlyphOrder"] + keys
Just7842e561999-12-16 21:34:53 +0000366
367 def __len__(self):
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500368 return len(list(self.keys()))
Just7842e561999-12-16 21:34:53 +0000369
370 def __getitem__(self, tag):
Behdad Esfahbod960280b2013-11-27 18:16:43 -0500371 tag = Tag(tag)
Just7842e561999-12-16 21:34:53 +0000372 try:
373 return self.tables[tag]
374 except KeyError:
jvr0011bb62002-05-23 09:42:45 +0000375 if tag == "GlyphOrder":
Behdad Esfahbodc4af3962013-08-28 17:12:12 -0400376 table = GlyphOrder(tag)
jvr0011bb62002-05-23 09:42:45 +0000377 self.tables[tag] = table
378 return table
Just7842e561999-12-16 21:34:53 +0000379 if self.reader is not None:
Justf8fd4772000-01-03 23:00:10 +0000380 import traceback
Just7842e561999-12-16 21:34:53 +0000381 if self.verbose:
jvr6ab979c2002-07-23 16:44:25 +0000382 debugmsg("Reading '%s' table from disk" % tag)
Just7842e561999-12-16 21:34:53 +0000383 data = self.reader[tag]
jvr8307fa42002-05-13 16:21:51 +0000384 tableClass = getTableClass(tag)
385 table = tableClass(tag)
Just7842e561999-12-16 21:34:53 +0000386 self.tables[tag] = table
387 if self.verbose:
jvr6ab979c2002-07-23 16:44:25 +0000388 debugmsg("Decompiling '%s' table" % tag)
Justf8fd4772000-01-03 23:00:10 +0000389 try:
390 table.decompile(data, self)
jvr2545f162008-03-01 09:30:17 +0000391 except:
392 if not self.ignoreDecompileErrors:
393 raise
394 # fall back to DefaultTable, retaining the binary table data
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500395 print("An exception occurred during the decompilation of the '%s' table" % tag)
Behdad Esfahbod2b06aaa2013-11-27 02:34:11 -0500396 from .tables.DefaultTable import DefaultTable
Behdad Esfahbodb92c0802013-11-27 05:05:46 -0500397 file = StringIO()
Justf8fd4772000-01-03 23:00:10 +0000398 traceback.print_exc(file=file)
399 table = DefaultTable(tag)
400 table.ERROR = file.getvalue()
401 self.tables[tag] = table
402 table.decompile(data, self)
Just7842e561999-12-16 21:34:53 +0000403 return table
404 else:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500405 raise KeyError("'%s' table not found" % tag)
Just7842e561999-12-16 21:34:53 +0000406
407 def __setitem__(self, tag, table):
Behdad Esfahbod5cf40082013-11-27 19:51:59 -0500408 self.tables[Tag(tag)] = table
Just7842e561999-12-16 21:34:53 +0000409
410 def __delitem__(self, tag):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500411 if tag not in self:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500412 raise KeyError("'%s' table not found" % tag)
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500413 if tag in self.tables:
jvrf7074632002-05-04 22:04:02 +0000414 del self.tables[tag]
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500415 if self.reader and tag in self.reader:
jvrf7074632002-05-04 22:04:02 +0000416 del self.reader[tag]
Behdad Esfahbod9c5e2ce2013-12-19 11:38:56 -0500417
418 def get(self, tag, default=None):
419 try:
420 return self[tag]
421 except KeyError:
422 return default
Just7842e561999-12-16 21:34:53 +0000423
424 def setGlyphOrder(self, glyphOrder):
425 self.glyphOrder = glyphOrder
Just7842e561999-12-16 21:34:53 +0000426
427 def getGlyphOrder(self):
jvrcf4b3b32002-05-05 09:48:31 +0000428 try:
429 return self.glyphOrder
430 except AttributeError:
431 pass
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500432 if 'CFF ' in self:
jvr0b63b282002-05-13 11:26:38 +0000433 cff = self['CFF ']
jvr700df032003-08-22 19:44:08 +0000434 self.glyphOrder = cff.getGlyphOrder()
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500435 elif 'post' in self:
jvrcf4b3b32002-05-05 09:48:31 +0000436 # 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 #
Justc91a9512000-08-23 12:31:52 +0000444 self._getGlyphNamesFromCmap()
jvrcf4b3b32002-05-05 09:48:31 +0000445 else:
446 self.glyphOrder = glyphOrder
447 else:
448 self._getGlyphNamesFromCmap()
Just7842e561999-12-16 21:34:53 +0000449 return self.glyphOrder
450
451 def _getGlyphNamesFromCmap(self):
jvr9f1e14b2002-05-05 11:29:33 +0000452 #
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.
Just7842e561999-12-16 21:34:53 +0000479 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
jvr9f1e14b2002-05-05 11:29:33 +0000485 # to work with (so we don't get called recursively).
Just7842e561999-12-16 21:34:53 +0000486 self.glyphOrder = glyphOrder
jvr9f1e14b2002-05-05 11:29:33 +0000487 # Get a (new) temporary cmap (based on the just invented names)
Just7842e561999-12-16 21:34:53 +0000488 tempcmap = self['cmap'].getcmap(3, 1)
489 if tempcmap is not None:
490 # we have a unicode cmap
Just7dcc9161999-12-29 13:06:08 +0000491 from fontTools import agl
Just7842e561999-12-16 21:34:53 +0000492 cmap = tempcmap.cmap
493 # create a reverse cmap dict
494 reversecmap = {}
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500495 for unicode, name in list(cmap.items()):
Just7842e561999-12-16 21:34:53 +0000496 reversecmap[name] = unicode
Justa556f512001-02-23 21:58:57 +0000497 allNames = {}
Just7842e561999-12-16 21:34:53 +0000498 for i in range(numGlyphs):
499 tempName = glyphOrder[i]
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500500 if tempName in reversecmap:
Just7842e561999-12-16 21:34:53 +0000501 unicode = reversecmap[tempName]
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500502 if unicode in agl.UV2AGL:
Just7842e561999-12-16 21:34:53 +0000503 # get name from the Adobe Glyph List
Justa556f512001-02-23 21:58:57 +0000504 glyphName = agl.UV2AGL[unicode]
Just7842e561999-12-16 21:34:53 +0000505 else:
506 # create uni<CODE> name
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500507 glyphName = "uni%04X" % unicode
Justa556f512001-02-23 21:58:57 +0000508 tempName = glyphName
509 n = 1
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500510 while tempName in allNames:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500511 tempName = glyphName + "#" + repr(n)
Justa556f512001-02-23 21:58:57 +0000512 n = n + 1
513 glyphOrder[i] = tempName
514 allNames[tempName] = 1
jvr9f1e14b2002-05-05 11:29:33 +0000515 # Delete the temporary cmap table from the cache, so it can
516 # be parsed again with the right names.
Just7842e561999-12-16 21:34:53 +0000517 del self.tables['cmap']
518 else:
519 pass # no unicode cmap available, stick with the invented names
520 self.glyphOrder = glyphOrder
jvr9f1e14b2002-05-05 11:29:33 +0000521 if cmapLoading:
522 # restore partially loaded cmap, so it can continue loading
523 # using the proper names.
524 self.tables['cmap'] = cmapLoading
Just7842e561999-12-16 21:34:53 +0000525
526 def getGlyphNames(self):
527 """Get a list of glyph names, sorted alphabetically."""
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500528 glyphNames = sorted(self.getGlyphOrder()[:])
Just7842e561999-12-16 21:34:53 +0000529 return glyphNames
530
531 def getGlyphNames2(self):
Just8bc8cf81999-12-17 12:54:19 +0000532 """Get a list of glyph names, sorted alphabetically,
533 but not case sensitive.
534 """
Just7842e561999-12-16 21:34:53 +0000535 from fontTools.misc import textTools
536 return textTools.caselessSort(self.getGlyphOrder())
537
Behdad Esfahboddc873722013-12-04 21:28:50 -0500538 def getGlyphName(self, glyphID, requireReal=False):
jvr0b63b282002-05-13 11:26:38 +0000539 try:
540 return self.getGlyphOrder()[glyphID]
541 except IndexError:
jvr823f8cd2006-10-21 14:12:38 +0000542 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 Esfahboddc873722013-12-04 21:28:50 -0500557 def getGlyphID(self, glyphName, requireReal=False):
Just7842e561999-12-16 21:34:53 +0000558 if not hasattr(self, "_reverseGlyphOrderDict"):
559 self._buildReverseGlyphOrderDict()
560 glyphOrder = self.getGlyphOrder()
561 d = self._reverseGlyphOrderDict
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500562 if glyphName not in d:
Just7842e561999-12-16 21:34:53 +0000563 if glyphName in glyphOrder:
564 self._buildReverseGlyphOrderDict()
565 return self.getGlyphID(glyphName)
566 else:
Behdad Esfahbode5bee372013-12-04 22:46:29 -0500567 if requireReal:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500568 raise KeyError(glyphName)
Behdad Esfahbode5bee372013-12-04 22:46:29 -0500569 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)
jvr823f8cd2006-10-21 14:12:38 +0000576 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 Esfahbod9e6ef942013-12-04 16:31:44 -0500587 if glyphID is None:
jvr823f8cd2006-10-21 14:12:38 +0000588 glyphID = self.last_vid -1
589 self.last_vid = glyphID
590 self.reverseVIDDict[glyphName] = glyphID
591 self.VIDDict[glyphID] = glyphName
592 return glyphID
593
Just7842e561999-12-16 21:34:53 +0000594 glyphID = d[glyphName]
Behdad Esfahbod180ace62013-11-27 02:40:30 -0500595 if glyphName != glyphOrder[glyphID]:
Just7842e561999-12-16 21:34:53 +0000596 self._buildReverseGlyphOrderDict()
597 return self.getGlyphID(glyphName)
598 return glyphID
jvr823f8cd2006-10-21 14:12:38 +0000599
Behdad Esfahboddc873722013-12-04 21:28:50 -0500600 def getReverseGlyphMap(self, rebuild=False):
jvr823f8cd2006-10-21 14:12:38 +0000601 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
602 self._buildReverseGlyphOrderDict()
603 return self._reverseGlyphOrderDict
604
Just7842e561999-12-16 21:34:53 +0000605 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
jvr8307fa42002-05-13 16:21:51 +0000617 tableClass = getTableClass(tag)
618 for masterTable in tableClass.dependencies:
Just7842e561999-12-16 21:34:53 +0000619 if masterTable not in done:
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500620 if masterTable in self:
Just7842e561999-12-16 21:34:53 +0000621 self._writeTable(masterTable, writer, done)
622 else:
623 done.append(masterTable)
jvrcf4b3b32002-05-05 09:48:31 +0000624 tabledata = self.getTableData(tag)
Just7842e561999-12-16 21:34:53 +0000625 if self.verbose:
626 debugmsg("writing '%s' table to disk" % tag)
627 writer[tag] = tabledata
628 done.append(tag)
629
jvrcf4b3b32002-05-05 09:48:31 +0000630 def getTableData(self, tag):
631 """Returns raw table data, whether compiled or directly read from disk.
Just7842e561999-12-16 21:34:53 +0000632 """
Behdad Esfahbod5cf40082013-11-27 19:51:59 -0500633 tag = Tag(tag)
Just7842e561999-12-16 21:34:53 +0000634 if self.isLoaded(tag):
635 if self.verbose:
636 debugmsg("compiling '%s' table" % tag)
637 return self.tables[tag].compile(self)
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500638 elif self.reader and tag in self.reader:
Just7842e561999-12-16 21:34:53 +0000639 if self.verbose:
jvr6ab979c2002-07-23 16:44:25 +0000640 debugmsg("Reading '%s' table from disk" % tag)
Just7842e561999-12-16 21:34:53 +0000641 return self.reader[tag]
642 else:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500643 raise KeyError(tag)
jvr8df8f632003-08-25 13:15:50 +0000644
Behdad Esfahboddc873722013-12-04 21:28:50 -0500645 def getGlyphSet(self, preferCFF=True):
jvr8df8f632003-08-25 13:15:50 +0000646 """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 Esfahbodbc5e1cb2013-11-27 02:33:03 -0500657 if preferCFF and "CFF " in self:
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500658 return list(self["CFF "].cff.values())[0].CharStrings
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500659 if "glyf" in self:
jvr8df8f632003-08-25 13:15:50 +0000660 return _TTGlyphSet(self)
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500661 if "CFF " in self:
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500662 return list(self["CFF "].cff.values())[0].CharStrings
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500663 raise TTLibError("Font contains no outlines")
jvr8df8f632003-08-25 13:15:50 +0000664
665
Behdad Esfahbode388db52013-11-28 14:26:58 -0500666class _TTGlyphSet(object):
jvr8df8f632003-08-25 13:15:50 +0000667
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 Esfahbodc2297cd2013-11-27 06:26:55 -0500680 return list(self._ttFont["glyf"].keys())
jvr8df8f632003-08-25 13:15:50 +0000681
682 def has_key(self, glyphName):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500683 return glyphName in self._ttFont["glyf"]
jvr8df8f632003-08-25 13:15:50 +0000684
685 __contains__ = has_key
686
687 def __getitem__(self, glyphName):
688 return _TTGlyph(glyphName, self._ttFont)
689
jvr2e4cc022005-03-08 09:50:56 +0000690 def get(self, glyphName, default=None):
691 try:
692 return self[glyphName]
693 except KeyError:
694 return default
695
jvr8df8f632003-08-25 13:15:50 +0000696
Behdad Esfahbode388db52013-11-28 14:26:58 -0500697class _TTGlyph(object):
jvr8df8f632003-08-25 13:15:50 +0000698
jvrd028b7b2003-08-26 19:00:38 +0000699 """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.
jvr8df8f632003-08-25 13:15:50 +0000702 """
703
704 def __init__(self, glyphName, ttFont):
705 self._glyphName = glyphName
706 self._ttFont = ttFont
jvrd028b7b2003-08-26 19:00:38 +0000707 self.width, self.lsb = self._ttFont['hmtx'][self._glyphName]
jvr8df8f632003-08-25 13:15:50 +0000708
709 def draw(self, pen):
jvrd028b7b2003-08-26 19:00:38 +0000710 """Draw the glyph onto Pen. See fontTools.pens.basePen for details
711 how that works.
712 """
jvr8df8f632003-08-25 13:15:50 +0000713 glyfTable = self._ttFont['glyf']
714 glyph = glyfTable[self._glyphName]
jvr8df8f632003-08-25 13:15:50 +0000715 if hasattr(glyph, "xMin"):
jvrd028b7b2003-08-26 19:00:38 +0000716 offset = self.lsb - glyph.xMin
jvr8df8f632003-08-25 13:15:50 +0000717 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:
jvr1c9917b2003-08-25 13:20:38 +0000740 # 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.
jvr8df8f632003-08-25 13:15:50 +0000743 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()
Just7842e561999-12-16 21:34:53 +0000756
757
Behdad Esfahbode388db52013-11-28 14:26:58 -0500758class GlyphOrder(object):
jvr0011bb62002-05-23 09:42:45 +0000759
jvr22f06892002-05-25 14:56:29 +0000760 """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.
jvr0011bb62002-05-23 09:42:45 +0000762 """
763
Behdad Esfahbod50d6c722014-03-28 14:32:24 -0700764 def __init__(self, tag=None):
Behdad Esfahbodc4af3962013-08-28 17:12:12 -0400765 pass
jvr0011bb62002-05-23 09:42:45 +0000766
767 def toXML(self, writer, ttFont):
Behdad Esfahbodc4af3962013-08-28 17:12:12 -0400768 glyphOrder = ttFont.getGlyphOrder()
jvr4e5af602002-05-24 09:58:04 +0000769 writer.comment("The 'id' attribute is only for humans; "
770 "it is ignored when parsed.")
jvr0011bb62002-05-23 09:42:45 +0000771 writer.newline()
Behdad Esfahbodc4af3962013-08-28 17:12:12 -0400772 for i in range(len(glyphOrder)):
773 glyphName = glyphOrder[i]
jvr0011bb62002-05-23 09:42:45 +0000774 writer.simpletag("GlyphID", id=i, name=glyphName)
775 writer.newline()
776
Behdad Esfahbod3a9fd302013-11-27 03:19:32 -0500777 def fromXML(self, name, attrs, content, ttFont):
jvr0011bb62002-05-23 09:42:45 +0000778 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
Just7842e561999-12-16 21:34:53 +0000785def getTableModule(tag):
786 """Fetch the packer/unpacker module for a table.
787 Return None when no module is found.
788 """
Behdad Esfahbod2b06aaa2013-11-27 02:34:11 -0500789 from . import tables
jvr8307fa42002-05-13 16:21:51 +0000790 pyTag = tagToIdentifier(tag)
Just7842e561999-12-16 21:34:53 +0000791 try:
jvr0bb3ba52003-08-22 18:52:22 +0000792 __import__("fontTools.ttLib.tables." + pyTag)
Behdad Esfahbod223273f2013-11-27 05:09:00 -0500793 except ImportError as err:
Behdad Esfahbod400e9532013-08-16 10:53:36 -0400794 # 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
Just7842e561999-12-16 21:34:53 +0000802 else:
jvr8307fa42002-05-13 16:21:51 +0000803 return getattr(tables, pyTag)
Just7842e561999-12-16 21:34:53 +0000804
805
806def 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 Esfahbod2b06aaa2013-11-27 02:34:11 -0500812 from .tables.DefaultTable import DefaultTable
Just7842e561999-12-16 21:34:53 +0000813 return DefaultTable
jvr8307fa42002-05-13 16:21:51 +0000814 pyTag = tagToIdentifier(tag)
815 tableClass = getattr(module, "table_" + pyTag)
816 return tableClass
Just7842e561999-12-16 21:34:53 +0000817
818
Behdad Esfahbodd0a31f52014-03-28 14:04:01 -0700819def 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
jvr8307fa42002-05-13 16:21:51 +0000828def newTable(tag):
Just7842e561999-12-16 21:34:53 +0000829 """Return a new instance of a table."""
jvr8307fa42002-05-13 16:21:51 +0000830 tableClass = getTableClass(tag)
831 return tableClass(tag)
Just7842e561999-12-16 21:34:53 +0000832
833
834def _escapechar(c):
jvr8307fa42002-05-13 16:21:51 +0000835 """Helper function for tagToIdentifier()"""
Just7842e561999-12-16 21:34:53 +0000836 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 Esfahbod319c5fd2013-11-27 18:13:48 -0500842 return hex(byteord(c))[2:]
Just7842e561999-12-16 21:34:53 +0000843
844
jvr8307fa42002-05-13 16:21:51 +0000845def tagToIdentifier(tag):
Just7842e561999-12-16 21:34:53 +0000846 """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 Esfahbodd2f5d2f2013-11-27 17:54:42 -0500859 tag = Tag(tag)
jvre5ae28e2002-05-25 08:22:22 +0000860 if tag == "GlyphOrder":
861 return tag
Just7842e561999-12-16 21:34:53 +0000862 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
jvr8307fa42002-05-13 16:21:51 +0000873def identifierToTag(ident):
874 """the opposite of tagToIdentifier()"""
jvre5ae28e2002-05-25 08:22:22 +0000875 if ident == "GlyphOrder":
876 return ident
Just7842e561999-12-16 21:34:53 +0000877 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 Esfahbodb7a2d792013-11-27 15:19:40 -0500888 tag = tag + bytechr(int(ident[i:i+2], 16))
Just7842e561999-12-16 21:34:53 +0000889 # append trailing spaces
890 tag = tag + (4 - len(tag)) * ' '
Behdad Esfahbodd2f5d2f2013-11-27 17:54:42 -0500891 return Tag(tag)
Just7842e561999-12-16 21:34:53 +0000892
893
jvr8307fa42002-05-13 16:21:51 +0000894def tagToXML(tag):
895 """Similarly to tagToIdentifier(), this converts a TT tag
Just7842e561999-12-16 21:34:53 +0000896 to a valid XML element name. Since XML element names are
897 case sensitive, this is a fairly simple/readable translation.
898 """
Just7dcc9161999-12-29 13:06:08 +0000899 import re
Behdad Esfahbodd2f5d2f2013-11-27 17:54:42 -0500900 tag = Tag(tag)
Just7842e561999-12-16 21:34:53 +0000901 if tag == "OS/2":
902 return "OS_2"
jvr0011bb62002-05-23 09:42:45 +0000903 elif tag == "GlyphOrder":
jvr28ae1962004-11-16 10:37:59 +0000904 return tag
Just7842e561999-12-16 21:34:53 +0000905 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500906 return tag.strip()
Just7842e561999-12-16 21:34:53 +0000907 else:
jvr8307fa42002-05-13 16:21:51 +0000908 return tagToIdentifier(tag)
Just7842e561999-12-16 21:34:53 +0000909
910
jvr8307fa42002-05-13 16:21:51 +0000911def xmlToTag(tag):
912 """The opposite of tagToXML()"""
Just7842e561999-12-16 21:34:53 +0000913 if tag == "OS_2":
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500914 return Tag("OS/2")
Just7842e561999-12-16 21:34:53 +0000915 if len(tag) == 8:
jvr8307fa42002-05-13 16:21:51 +0000916 return identifierToTag(tag)
Just7842e561999-12-16 21:34:53 +0000917 else:
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500918 return Tag(tag + " " * (4 - len(tag)))
Just7842e561999-12-16 21:34:53 +0000919
920
921def debugmsg(msg):
922 import time
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500923 print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())))
Just7842e561999-12-16 21:34:53 +0000924
jvr700df032003-08-22 19:44:08 +0000925
jvr28ae1962004-11-16 10:37:59 +0000926# Table order as recommended in the OpenType specification 1.4
927TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX",
928 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf",
929 "kern", "name", "post", "gasp", "PCLT"]
jvr700df032003-08-22 19:44:08 +0000930
jvr28ae1962004-11-16 10:37:59 +0000931OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post",
932 "CFF "]
933
934def 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 Esfahbodac1b4352013-11-27 04:15:34 -0500939 tagList = sorted(tagList)
jvr28ae1962004-11-16 10:37:59 +0000940 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
jvr700df032003-08-22 19:44:08 +0000947 else:
jvr28ae1962004-11-16 10:37:59 +0000948 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
jvr700df032003-08-22 19:44:08 +0000956
957
Behdad Esfahboddc873722013-12-04 21:28:50 -0500958def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
jvr28ae1962004-11-16 10:37:59 +0000959 """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 Pournaderd7921e32013-10-08 21:29:22 -0700964 writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData)
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500965 tables = list(reader.keys())
jvr28ae1962004-11-16 10:37:59 +0000966 for tag in sortedTagList(tables, tableOrder):
967 writer[tag] = reader[tag]
968 writer.close()