1from fontTools.misc import xmlWriter
2from fontTools.misc.py23 import Tag, byteord, tostr
3from fontTools.misc.loggingTools import deprecateArgument
4from fontTools.ttLib import TTLibError
5from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
6from io import BytesIO, StringIO
7import os
8import logging
9import traceback
10
11log = logging.getLogger(__name__)
12
13class TTFont(object):
14
15	"""The main font object. It manages file input and output, and offers
16	a convenient way of accessing tables.
17	Tables will be only decompiled when necessary, ie. when they're actually
18	accessed. This means that simple operations can be extremely fast.
19	"""
20
21	def __init__(self, file=None, res_name_or_index=None,
22			sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
23			verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
24			recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
25			_tableCache=None):
26
27		"""The constructor can be called with a few different arguments.
28		When reading a font from disk, 'file' should be either a pathname
29		pointing to a file, or a readable file object.
30
31		It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
32		resource name or an sfnt resource index number or zero. The latter
33		case will cause TTLib to autodetect whether the file is a flat file
34		or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
35		will be read!)
36
37		The 'checkChecksums' argument is used to specify how sfnt
38		checksums are treated upon reading a file from disk:
39			0: don't check (default)
40			1: check, print warnings if a wrong checksum is found
41			2: check, raise an exception if a wrong checksum is found.
42
43		The TTFont constructor can also be called without a 'file'
44		argument: this is the way to create a new empty font.
45		In this case you can optionally supply the 'sfntVersion' argument,
46		and a 'flavor' which can be None, 'woff', or 'woff2'.
47
48		If the recalcBBoxes argument is false, a number of things will *not*
49		be recalculated upon save/compile:
50			1) 'glyf' glyph bounding boxes
51			2) 'CFF ' font bounding box
52			3) 'head' font bounding box
53			4) 'hhea' min/max values
54			5) 'vhea' min/max values
55		(1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-).
56		Additionally, upon importing an TTX file, this option cause glyphs
57		to be compiled right away. This should reduce memory consumption
58		greatly, and therefore should have some impact on the time needed
59		to parse/compile large fonts.
60
61		If the recalcTimestamp argument is false, the modified timestamp in the
62		'head' table will *not* be recalculated upon save/compile.
63
64		If the allowVID argument is set to true, then virtual GID's are
65		supported. Asking for a glyph ID with a glyph name or GID that is not in
66		the font will return a virtual GID.   This is valid for GSUB and cmap
67		tables. For SING glyphlets, the cmap table is used to specify Unicode
68		values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested
69		and does not exist in the font, or the glyphname has the form glyphN
70		and does not exist in the font, then N is used as the virtual GID.
71		Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new
72		virtual GIDs, the next is one less than the previous.
73
74		If ignoreDecompileErrors is set to True, exceptions raised in
75		individual tables during decompilation will be ignored, falling
76		back to the DefaultTable implementation, which simply keeps the
77		binary data.
78
79		If lazy is set to True, many data structures are loaded lazily, upon
80		access only.  If it is set to False, many data structures are loaded
81		immediately.  The default is lazy=None which is somewhere in between.
82		"""
83
84		for name in ("verbose", "quiet"):
85			val = locals().get(name)
86			if val is not None:
87				deprecateArgument(name, "configure logging instead")
88			setattr(self, name, val)
89
90		self.lazy = lazy
91		self.recalcBBoxes = recalcBBoxes
92		self.recalcTimestamp = recalcTimestamp
93		self.tables = {}
94		self.reader = None
95
96		# Permit the user to reference glyphs that are not int the font.
97		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.
98		self.reverseVIDDict = {}
99		self.VIDDict = {}
100		self.allowVID = allowVID
101		self.ignoreDecompileErrors = ignoreDecompileErrors
102
103		if not file:
104			self.sfntVersion = sfntVersion
105			self.flavor = flavor
106			self.flavorData = None
107			return
108		if not hasattr(file, "read"):
109			closeStream = True
110			# assume file is a string
111			if res_name_or_index is not None:
112				# see if it contains 'sfnt' resources in the resource or data fork
113				from . import macUtils
114				if res_name_or_index == 0:
115					if macUtils.getSFNTResIndices(file):
116						# get the first available sfnt font.
117						file = macUtils.SFNTResourceReader(file, 1)
118					else:
119						file = open(file, "rb")
120				else:
121					file = macUtils.SFNTResourceReader(file, res_name_or_index)
122			else:
123				file = open(file, "rb")
124		else:
125			# assume "file" is a readable file object
126			closeStream = False
127			file.seek(0)
128
129		if not self.lazy:
130			# read input file in memory and wrap a stream around it to allow overwriting
131			file.seek(0)
132			tmp = BytesIO(file.read())
133			if hasattr(file, 'name'):
134				# save reference to input file name
135				tmp.name = file.name
136			if closeStream:
137				file.close()
138			file = tmp
139		self._tableCache = _tableCache
140		self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber)
141		self.sfntVersion = self.reader.sfntVersion
142		self.flavor = self.reader.flavor
143		self.flavorData = self.reader.flavorData
144
145	def __enter__(self):
146		return self
147
148	def __exit__(self, type, value, traceback):
149		self.close()
150
151	def close(self):
152		"""If we still have a reader object, close it."""
153		if self.reader is not None:
154			self.reader.close()
155
156	def save(self, file, reorderTables=True):
157		"""Save the font to disk. Similarly to the constructor,
158		the 'file' argument can be either a pathname or a writable
159		file object.
160		"""
161		if not hasattr(file, "write"):
162			if self.lazy and self.reader.file.name == file:
163				raise TTLibError(
164					"Can't overwrite TTFont when 'lazy' attribute is True")
165			createStream = True
166		else:
167			# assume "file" is a writable file object
168			createStream = False
169
170		tmp = BytesIO()
171
172		writer_reordersTables = self._save(tmp)
173
174		if not (reorderTables is None or writer_reordersTables or
175				(reorderTables is False and self.reader is None)):
176			if reorderTables is False:
177				# sort tables using the original font's order
178				tableOrder = list(self.reader.keys())
179			else:
180				# use the recommended order from the OpenType specification
181				tableOrder = None
182			tmp.flush()
183			tmp2 = BytesIO()
184			reorderFontTables(tmp, tmp2, tableOrder)
185			tmp.close()
186			tmp = tmp2
187
188		if createStream:
189			# "file" is a path
190			with open(file, "wb") as file:
191				file.write(tmp.getvalue())
192		else:
193			file.write(tmp.getvalue())
194
195		tmp.close()
196
197	def _save(self, file, tableCache=None):
198		"""Internal function, to be shared by save() and TTCollection.save()"""
199
200		if self.recalcTimestamp and 'head' in self:
201			self['head']  # make sure 'head' is loaded so the recalculation is actually done
202
203		tags = list(self.keys())
204		if "GlyphOrder" in tags:
205			tags.remove("GlyphOrder")
206		numTables = len(tags)
207		# write to a temporary stream to allow saving to unseekable streams
208		writer = SFNTWriter(file, numTables, self.sfntVersion, self.flavor, self.flavorData)
209
210		done = []
211		for tag in tags:
212			self._writeTable(tag, writer, done, tableCache)
213
214		writer.close()
215
216		return writer.reordersTables()
217
218	def saveXML(self, fileOrPath, newlinestr=None, **kwargs):
219		"""Export the font as TTX (an XML-based text file), or as a series of text
220		files when splitTables is true. In the latter case, the 'fileOrPath'
221		argument should be a path to a directory.
222		The 'tables' argument must either be false (dump all tables) or a
223		list of tables to dump. The 'skipTables' argument may be a list of tables
224		to skip, but only when the 'tables' argument is false.
225		"""
226
227		writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
228		self._saveXML(writer, **kwargs)
229		writer.close()
230
231	def _saveXML(self, writer,
232		     writeVersion=True,
233		     quiet=None, tables=None, skipTables=None, splitTables=False,
234		     splitGlyphs=False, disassembleInstructions=True,
235		     bitmapGlyphDataFormat='raw'):
236
237		if quiet is not None:
238			deprecateArgument("quiet", "configure logging instead")
239
240		self.disassembleInstructions = disassembleInstructions
241		self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
242		if not tables:
243			tables = list(self.keys())
244			if "GlyphOrder" not in tables:
245				tables = ["GlyphOrder"] + tables
246			if skipTables:
247				for tag in skipTables:
248					if tag in tables:
249						tables.remove(tag)
250		numTables = len(tables)
251
252		if writeVersion:
253			from fontTools import version
254			version = ".".join(version.split('.')[:2])
255			writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
256					ttLibVersion=version)
257		else:
258			writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1])
259		writer.newline()
260
261		# always splitTables if splitGlyphs is enabled
262		splitTables = splitTables or splitGlyphs
263
264		if not splitTables:
265			writer.newline()
266		else:
267			path, ext = os.path.splitext(writer.filename)
268			fileNameTemplate = path + ".%s" + ext
269
270		for i in range(numTables):
271			tag = tables[i]
272			if splitTables:
273				tablePath = fileNameTemplate % tagToIdentifier(tag)
274				tableWriter = xmlWriter.XMLWriter(tablePath,
275						newlinestr=writer.newlinestr)
276				tableWriter.begintag("ttFont", ttLibVersion=version)
277				tableWriter.newline()
278				tableWriter.newline()
279				writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
280				writer.newline()
281			else:
282				tableWriter = writer
283			self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs)
284			if splitTables:
285				tableWriter.endtag("ttFont")
286				tableWriter.newline()
287				tableWriter.close()
288		writer.endtag("ttFont")
289		writer.newline()
290
291	def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False):
292		if quiet is not None:
293			deprecateArgument("quiet", "configure logging instead")
294		if tag in self:
295			table = self[tag]
296			report = "Dumping '%s' table..." % tag
297		else:
298			report = "No '%s' table found." % tag
299		log.info(report)
300		if tag not in self:
301			return
302		xmlTag = tagToXML(tag)
303		attrs = dict()
304		if hasattr(table, "ERROR"):
305			attrs['ERROR'] = "decompilation error"
306		from .tables.DefaultTable import DefaultTable
307		if table.__class__ == DefaultTable:
308			attrs['raw'] = True
309		writer.begintag(xmlTag, **attrs)
310		writer.newline()
311		if tag == "glyf":
312			table.toXML(writer, self, splitGlyphs=splitGlyphs)
313		else:
314			table.toXML(writer, self)
315		writer.endtag(xmlTag)
316		writer.newline()
317		writer.newline()
318
319	def importXML(self, fileOrPath, quiet=None):
320		"""Import a TTX file (an XML-based text format), so as to recreate
321		a font object.
322		"""
323		if quiet is not None:
324			deprecateArgument("quiet", "configure logging instead")
325
326		if "maxp" in self and "post" in self:
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 ').
331			self.getGlyphOrder()
332
333		from fontTools.misc import xmlReader
334
335		reader = xmlReader.XMLReader(fileOrPath, self)
336		reader.read()
337
338	def isLoaded(self, tag):
339		"""Return true if the table identified by 'tag' has been
340		decompiled and loaded into memory."""
341		return tag in self.tables
342
343	def has_key(self, tag):
344		if self.isLoaded(tag):
345			return True
346		elif self.reader and tag in self.reader:
347			return True
348		elif tag == "GlyphOrder":
349			return True
350		else:
351			return False
352
353	__contains__ = has_key
354
355	def keys(self):
356		keys = list(self.tables.keys())
357		if self.reader:
358			for key in list(self.reader.keys()):
359				if key not in keys:
360					keys.append(key)
361
362		if "GlyphOrder" in keys:
363			keys.remove("GlyphOrder")
364		keys = sortedTagList(keys)
365		return ["GlyphOrder"] + keys
366
367	def __len__(self):
368		return len(list(self.keys()))
369
370	def __getitem__(self, tag):
371		tag = Tag(tag)
372		table = self.tables.get(tag)
373		if table is None:
374			if tag == "GlyphOrder":
375				table = GlyphOrder(tag)
376				self.tables[tag] = table
377			elif self.reader is not None:
378				table = self._readTable(tag)
379			else:
380				raise KeyError("'%s' table not found" % tag)
381		return table
382
383	def _readTable(self, tag):
384		log.debug("Reading '%s' table from disk", tag)
385		data = self.reader[tag]
386		if self._tableCache is not None:
387			table = self._tableCache.get((tag, data))
388			if table is not None:
389				return table
390		tableClass = getTableClass(tag)
391		table = tableClass(tag)
392		self.tables[tag] = table
393		log.debug("Decompiling '%s' table", tag)
394		try:
395			table.decompile(data, self)
396		except Exception:
397			if not self.ignoreDecompileErrors:
398				raise
399			# fall back to DefaultTable, retaining the binary table data
400			log.exception(
401				"An exception occurred during the decompilation of the '%s' table", tag)
402			from .tables.DefaultTable import DefaultTable
403			file = StringIO()
404			traceback.print_exc(file=file)
405			table = DefaultTable(tag)
406			table.ERROR = file.getvalue()
407			self.tables[tag] = table
408			table.decompile(data, self)
409		if self._tableCache is not None:
410			self._tableCache[(tag, data)] = table
411		return table
412
413	def __setitem__(self, tag, table):
414		self.tables[Tag(tag)] = table
415
416	def __delitem__(self, tag):
417		if tag not in self:
418			raise KeyError("'%s' table not found" % tag)
419		if tag in self.tables:
420			del self.tables[tag]
421		if self.reader and tag in self.reader:
422			del self.reader[tag]
423
424	def get(self, tag, default=None):
425		try:
426			return self[tag]
427		except KeyError:
428			return default
429
430	def setGlyphOrder(self, glyphOrder):
431		self.glyphOrder = glyphOrder
432
433	def getGlyphOrder(self):
434		try:
435			return self.glyphOrder
436		except AttributeError:
437			pass
438		if 'CFF ' in self:
439			cff = self['CFF ']
440			self.glyphOrder = cff.getGlyphOrder()
441		elif 'post' in self:
442			# TrueType font
443			glyphOrder = self['post'].getGlyphOrder()
444			if glyphOrder is None:
445				#
446				# No names found in the 'post' table.
447				# Try to create glyph names from the unicode cmap (if available)
448				# in combination with the Adobe Glyph List (AGL).
449				#
450				self._getGlyphNamesFromCmap()
451			else:
452				self.glyphOrder = glyphOrder
453		else:
454			self._getGlyphNamesFromCmap()
455		return self.glyphOrder
456
457	def _getGlyphNamesFromCmap(self):
458		#
459		# This is rather convoluted, but then again, it's an interesting problem:
460		# - we need to use the unicode values found in the cmap table to
461		#   build glyph names (eg. because there is only a minimal post table,
462		#   or none at all).
463		# - but the cmap parser also needs glyph names to work with...
464		# So here's what we do:
465		# - make up glyph names based on glyphID
466		# - load a temporary cmap table based on those names
467		# - extract the unicode values, build the "real" glyph names
468		# - unload the temporary cmap table
469		#
470		if self.isLoaded("cmap"):
471			# Bootstrapping: we're getting called by the cmap parser
472			# itself. This means self.tables['cmap'] contains a partially
473			# loaded cmap, making it impossible to get at a unicode
474			# subtable here. We remove the partially loaded cmap and
475			# restore it later.
476			# This only happens if the cmap table is loaded before any
477			# other table that does f.getGlyphOrder()  or f.getGlyphName().
478			cmapLoading = self.tables['cmap']
479			del self.tables['cmap']
480		else:
481			cmapLoading = None
482		# Make up glyph names based on glyphID, which will be used by the
483		# temporary cmap and by the real cmap in case we don't find a unicode
484		# cmap.
485		numGlyphs = int(self['maxp'].numGlyphs)
486		glyphOrder = [None] * numGlyphs
487		glyphOrder[0] = ".notdef"
488		for i in range(1, numGlyphs):
489			glyphOrder[i] = "glyph%.5d" % i
490		# Set the glyph order, so the cmap parser has something
491		# to work with (so we don't get called recursively).
492		self.glyphOrder = glyphOrder
493
494		# Make up glyph names based on the reversed cmap table. Because some
495		# glyphs (eg. ligatures or alternates) may not be reachable via cmap,
496		# this naming table will usually not cover all glyphs in the font.
497		# If the font has no Unicode cmap table, reversecmap will be empty.
498		if 'cmap' in self:
499			reversecmap = self['cmap'].buildReversed()
500		else:
501			reversecmap = {}
502		useCount = {}
503		for i in range(numGlyphs):
504			tempName = glyphOrder[i]
505			if tempName in reversecmap:
506				# If a font maps both U+0041 LATIN CAPITAL LETTER A and
507				# U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph,
508				# we prefer naming the glyph as "A".
509				glyphName = self._makeGlyphName(min(reversecmap[tempName]))
510				numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1
511				if numUses > 1:
512					glyphName = "%s.alt%d" % (glyphName, numUses - 1)
513				glyphOrder[i] = glyphName
514
515		if 'cmap' in self:
516			# Delete the temporary cmap table from the cache, so it can
517			# be parsed again with the right names.
518			del self.tables['cmap']
519			self.glyphOrder = glyphOrder
520			if cmapLoading:
521				# restore partially loaded cmap, so it can continue loading
522				# using the proper names.
523				self.tables['cmap'] = cmapLoading
524
525	@staticmethod
526	def _makeGlyphName(codepoint):
527		from fontTools import agl  # Adobe Glyph List
528		if codepoint in agl.UV2AGL:
529			return agl.UV2AGL[codepoint]
530		elif codepoint <= 0xFFFF:
531			return "uni%04X" % codepoint
532		else:
533			return "u%X" % codepoint
534
535	def getGlyphNames(self):
536		"""Get a list of glyph names, sorted alphabetically."""
537		glyphNames = sorted(self.getGlyphOrder())
538		return glyphNames
539
540	def getGlyphNames2(self):
541		"""Get a list of glyph names, sorted alphabetically,
542		but not case sensitive.
543		"""
544		from fontTools.misc import textTools
545		return textTools.caselessSort(self.getGlyphOrder())
546
547	def getGlyphName(self, glyphID, requireReal=False):
548		try:
549			return self.getGlyphOrder()[glyphID]
550		except IndexError:
551			if requireReal or not self.allowVID:
552				# XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in
553				# the cmap table than there are glyphs. I don't think it's legal...
554				return "glyph%.5d" % glyphID
555			else:
556				# user intends virtual GID support
557				try:
558					glyphName = self.VIDDict[glyphID]
559				except KeyError:
560					glyphName  ="glyph%.5d" % glyphID
561					self.last_vid = min(glyphID, self.last_vid )
562					self.reverseVIDDict[glyphName] = glyphID
563					self.VIDDict[glyphID] = glyphName
564				return glyphName
565
566	def getGlyphID(self, glyphName, requireReal=False):
567		if not hasattr(self, "_reverseGlyphOrderDict"):
568			self._buildReverseGlyphOrderDict()
569		glyphOrder = self.getGlyphOrder()
570		d = self._reverseGlyphOrderDict
571		if glyphName not in d:
572			if glyphName in glyphOrder:
573				self._buildReverseGlyphOrderDict()
574				return self.getGlyphID(glyphName)
575			else:
576				if requireReal:
577					raise KeyError(glyphName)
578				elif not self.allowVID:
579					# Handle glyphXXX only
580					if glyphName[:5] == "glyph":
581						try:
582							return int(glyphName[5:])
583						except (NameError, ValueError):
584							raise KeyError(glyphName)
585				else:
586					# user intends virtual GID support
587					try:
588						glyphID = self.reverseVIDDict[glyphName]
589					except KeyError:
590						# if name is in glyphXXX format, use the specified name.
591						if glyphName[:5] == "glyph":
592							try:
593								glyphID = int(glyphName[5:])
594							except (NameError, ValueError):
595								glyphID = None
596						if glyphID is None:
597							glyphID = self.last_vid -1
598							self.last_vid = glyphID
599						self.reverseVIDDict[glyphName] = glyphID
600						self.VIDDict[glyphID] = glyphName
601					return glyphID
602
603		glyphID = d[glyphName]
604		if glyphName != glyphOrder[glyphID]:
605			self._buildReverseGlyphOrderDict()
606			return self.getGlyphID(glyphName)
607		return glyphID
608
609	def getReverseGlyphMap(self, rebuild=False):
610		if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
611			self._buildReverseGlyphOrderDict()
612		return self._reverseGlyphOrderDict
613
614	def _buildReverseGlyphOrderDict(self):
615		self._reverseGlyphOrderDict = d = {}
616		glyphOrder = self.getGlyphOrder()
617		for glyphID in range(len(glyphOrder)):
618			d[glyphOrder[glyphID]] = glyphID
619
620	def _writeTable(self, tag, writer, done, tableCache=None):
621		"""Internal helper function for self.save(). Keeps track of
622		inter-table dependencies.
623		"""
624		if tag in done:
625			return
626		tableClass = getTableClass(tag)
627		for masterTable in tableClass.dependencies:
628			if masterTable not in done:
629				if masterTable in self:
630					self._writeTable(masterTable, writer, done, tableCache)
631				else:
632					done.append(masterTable)
633		done.append(tag)
634		tabledata = self.getTableData(tag)
635		if tableCache is not None:
636			entry = tableCache.get((Tag(tag), tabledata))
637			if entry is not None:
638				log.debug("reusing '%s' table", tag)
639				writer.setEntry(tag, entry)
640				return
641		log.debug("Writing '%s' table to disk", tag)
642		writer[tag] = tabledata
643		if tableCache is not None:
644			tableCache[(Tag(tag), tabledata)] = writer[tag]
645
646	def getTableData(self, tag):
647		"""Returns raw table data, whether compiled or directly read from disk.
648		"""
649		tag = Tag(tag)
650		if self.isLoaded(tag):
651			log.debug("Compiling '%s' table", tag)
652			return self.tables[tag].compile(self)
653		elif self.reader and tag in self.reader:
654			log.debug("Reading '%s' table from disk", tag)
655			return self.reader[tag]
656		else:
657			raise KeyError(tag)
658
659	def getGlyphSet(self, preferCFF=True):
660		"""Return a generic GlyphSet, which is a dict-like object
661		mapping glyph names to glyph objects. The returned glyph objects
662		have a .draw() method that supports the Pen protocol, and will
663		have an attribute named 'width'.
664
665		If the font is CFF-based, the outlines will be taken from the 'CFF ' or
666		'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table.
667		If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use
668		the 'preferCFF' argument to specify which one should be taken. If the
669		font contains both a 'CFF ' and a 'CFF2' table, the latter is taken.
670		"""
671		glyphs = None
672		if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or
673		   ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))):
674			table_tag = "CFF2" if "CFF2" in self else "CFF "
675			glyphs = _TTGlyphSet(self,
676			    list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF)
677
678		if glyphs is None and "glyf" in self:
679			glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf)
680
681		if glyphs is None:
682			raise TTLibError("Font contains no outlines")
683
684		return glyphs
685
686	def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))):
687		"""Return the 'best' unicode cmap dictionary available in the font,
688		or None, if no unicode cmap subtable is available.
689
690		By default it will search for the following (platformID, platEncID)
691		pairs:
692			(3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0)
693		This can be customized via the cmapPreferences argument.
694		"""
695		return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
696
697
698class _TTGlyphSet(object):
699
700	"""Generic dict-like GlyphSet class that pulls metrics from hmtx and
701	glyph shape from TrueType or CFF.
702	"""
703
704	def __init__(self, ttFont, glyphs, glyphType):
705		"""Construct a new glyphset.
706
707		Args:
708			font (TTFont): The font object (used to get metrics).
709			glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
710			glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
711		"""
712		self._glyphs = glyphs
713		self._hmtx = ttFont['hmtx']
714		self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
715		self._glyphType = glyphType
716
717	def keys(self):
718		return list(self._glyphs.keys())
719
720	def has_key(self, glyphName):
721		return glyphName in self._glyphs
722
723	__contains__ = has_key
724
725	def __getitem__(self, glyphName):
726		horizontalMetrics = self._hmtx[glyphName]
727		verticalMetrics = self._vmtx[glyphName] if self._vmtx else None
728		return self._glyphType(
729			self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics)
730
731	def __len__(self):
732		return len(self._glyphs)
733
734	def get(self, glyphName, default=None):
735		try:
736			return self[glyphName]
737		except KeyError:
738			return default
739
740class _TTGlyph(object):
741
742	"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
743	that it has .draw() and .drawPoints() methods that take a pen object as
744	their only argument. Additionally there are 'width' and 'lsb' attributes,
745	read from the 'hmtx' table.
746
747	If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
748	attributes.
749	"""
750
751	def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
752		"""Construct a new _TTGlyph.
753
754		Args:
755			glyphset (_TTGlyphSet): A glyphset object used to resolve components.
756			glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
757			horizontalMetrics (int, int): The glyph's width and left sidebearing.
758		"""
759		self._glyphset = glyphset
760		self._glyph = glyph
761		self.width, self.lsb = horizontalMetrics
762		if verticalMetrics:
763			self.height, self.tsb = verticalMetrics
764		else:
765			self.height, self.tsb = None, None
766
767	def draw(self, pen):
768		"""Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
769		how that works.
770		"""
771		self._glyph.draw(pen)
772
773	def drawPoints(self, pen):
774		# drawPoints is only implemented for _TTGlyphGlyf at this time.
775		raise NotImplementedError()
776
777class _TTGlyphCFF(_TTGlyph):
778	pass
779
780class _TTGlyphGlyf(_TTGlyph):
781
782	def draw(self, pen):
783		"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
784		how that works.
785		"""
786		glyfTable = self._glyphset._glyphs
787		glyph = self._glyph
788		offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
789		glyph.draw(pen, glyfTable, offset)
790
791	def drawPoints(self, pen):
792		"""Draw the glyph onto PointPen. See fontTools.pens.pointPen
793		for details how that works.
794		"""
795		glyfTable = self._glyphset._glyphs
796		glyph = self._glyph
797		offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
798		glyph.drawPoints(pen, glyfTable, offset)
799
800
801class GlyphOrder(object):
802
803	"""A pseudo table. The glyph order isn't in the font as a separate
804	table, but it's nice to present it as such in the TTX format.
805	"""
806
807	def __init__(self, tag=None):
808		pass
809
810	def toXML(self, writer, ttFont):
811		glyphOrder = ttFont.getGlyphOrder()
812		writer.comment("The 'id' attribute is only for humans; "
813				"it is ignored when parsed.")
814		writer.newline()
815		for i in range(len(glyphOrder)):
816			glyphName = glyphOrder[i]
817			writer.simpletag("GlyphID", id=i, name=glyphName)
818			writer.newline()
819
820	def fromXML(self, name, attrs, content, ttFont):
821		if not hasattr(self, "glyphOrder"):
822			self.glyphOrder = []
823			ttFont.setGlyphOrder(self.glyphOrder)
824		if name == "GlyphID":
825			self.glyphOrder.append(attrs["name"])
826
827
828def getTableModule(tag):
829	"""Fetch the packer/unpacker module for a table.
830	Return None when no module is found.
831	"""
832	from . import tables
833	pyTag = tagToIdentifier(tag)
834	try:
835		__import__("fontTools.ttLib.tables." + pyTag)
836	except ImportError as err:
837		# If pyTag is found in the ImportError message,
838		# means table is not implemented.  If it's not
839		# there, then some other module is missing, don't
840		# suppress the error.
841		if str(err).find(pyTag) >= 0:
842			return None
843		else:
844			raise err
845	else:
846		return getattr(tables, pyTag)
847
848
849# Registry for custom table packer/unpacker classes. Keys are table
850# tags, values are (moduleName, className) tuples.
851# See registerCustomTableClass() and getCustomTableClass()
852_customTableRegistry = {}
853
854
855def registerCustomTableClass(tag, moduleName, className=None):
856	"""Register a custom packer/unpacker class for a table.
857	The 'moduleName' must be an importable module. If no 'className'
858	is given, it is derived from the tag, for example it will be
859	table_C_U_S_T_ for a 'CUST' tag.
860
861	The registered table class should be a subclass of
862	fontTools.ttLib.tables.DefaultTable.DefaultTable
863	"""
864	if className is None:
865		className = "table_" + tagToIdentifier(tag)
866	_customTableRegistry[tag] = (moduleName, className)
867
868
869def unregisterCustomTableClass(tag):
870	"""Unregister the custom packer/unpacker class for a table."""
871	del _customTableRegistry[tag]
872
873
874def getCustomTableClass(tag):
875	"""Return the custom table class for tag, if one has been registered
876	with 'registerCustomTableClass()'. Else return None.
877	"""
878	if tag not in _customTableRegistry:
879		return None
880	import importlib
881	moduleName, className = _customTableRegistry[tag]
882	module = importlib.import_module(moduleName)
883	return getattr(module, className)
884
885
886def getTableClass(tag):
887	"""Fetch the packer/unpacker class for a table."""
888	tableClass = getCustomTableClass(tag)
889	if tableClass is not None:
890		return tableClass
891	module = getTableModule(tag)
892	if module is None:
893		from .tables.DefaultTable import DefaultTable
894		return DefaultTable
895	pyTag = tagToIdentifier(tag)
896	tableClass = getattr(module, "table_" + pyTag)
897	return tableClass
898
899
900def getClassTag(klass):
901	"""Fetch the table tag for a class object."""
902	name = klass.__name__
903	assert name[:6] == 'table_'
904	name = name[6:] # Chop 'table_'
905	return identifierToTag(name)
906
907
908def newTable(tag):
909	"""Return a new instance of a table."""
910	tableClass = getTableClass(tag)
911	return tableClass(tag)
912
913
914def _escapechar(c):
915	"""Helper function for tagToIdentifier()"""
916	import re
917	if re.match("[a-z0-9]", c):
918		return "_" + c
919	elif re.match("[A-Z]", c):
920		return c + "_"
921	else:
922		return hex(byteord(c))[2:]
923
924
925def tagToIdentifier(tag):
926	"""Convert a table tag to a valid (but UGLY) python identifier,
927	as well as a filename that's guaranteed to be unique even on a
928	caseless file system. Each character is mapped to two characters.
929	Lowercase letters get an underscore before the letter, uppercase
930	letters get an underscore after the letter. Trailing spaces are
931	trimmed. Illegal characters are escaped as two hex bytes. If the
932	result starts with a number (as the result of a hex escape), an
933	extra underscore is prepended. Examples:
934		'glyf' -> '_g_l_y_f'
935		'cvt ' -> '_c_v_t'
936		'OS/2' -> 'O_S_2f_2'
937	"""
938	import re
939	tag = Tag(tag)
940	if tag == "GlyphOrder":
941		return tag
942	assert len(tag) == 4, "tag should be 4 characters long"
943	while len(tag) > 1 and tag[-1] == ' ':
944		tag = tag[:-1]
945	ident = ""
946	for c in tag:
947		ident = ident + _escapechar(c)
948	if re.match("[0-9]", ident):
949		ident = "_" + ident
950	return ident
951
952
953def identifierToTag(ident):
954	"""the opposite of tagToIdentifier()"""
955	if ident == "GlyphOrder":
956		return ident
957	if len(ident) % 2 and ident[0] == "_":
958		ident = ident[1:]
959	assert not (len(ident) % 2)
960	tag = ""
961	for i in range(0, len(ident), 2):
962		if ident[i] == "_":
963			tag = tag + ident[i+1]
964		elif ident[i+1] == "_":
965			tag = tag + ident[i]
966		else:
967			# assume hex
968			tag = tag + chr(int(ident[i:i+2], 16))
969	# append trailing spaces
970	tag = tag + (4 - len(tag)) * ' '
971	return Tag(tag)
972
973
974def tagToXML(tag):
975	"""Similarly to tagToIdentifier(), this converts a TT tag
976	to a valid XML element name. Since XML element names are
977	case sensitive, this is a fairly simple/readable translation.
978	"""
979	import re
980	tag = Tag(tag)
981	if tag == "OS/2":
982		return "OS_2"
983	elif tag == "GlyphOrder":
984		return tag
985	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
986		return tag.strip()
987	else:
988		return tagToIdentifier(tag)
989
990
991def xmlToTag(tag):
992	"""The opposite of tagToXML()"""
993	if tag == "OS_2":
994		return Tag("OS/2")
995	if len(tag) == 8:
996		return identifierToTag(tag)
997	else:
998		return Tag(tag + " " * (4 - len(tag)))
999
1000
1001
1002# Table order as recommended in the OpenType specification 1.4
1003TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX",
1004				"hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf",
1005				"kern", "name", "post", "gasp", "PCLT"]
1006
1007OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post",
1008				"CFF "]
1009
1010def sortedTagList(tagList, tableOrder=None):
1011	"""Return a sorted copy of tagList, sorted according to the OpenType
1012	specification, or according to a custom tableOrder. If given and not
1013	None, tableOrder needs to be a list of tag names.
1014	"""
1015	tagList = sorted(tagList)
1016	if tableOrder is None:
1017		if "DSIG" in tagList:
1018			# DSIG should be last (XXX spec reference?)
1019			tagList.remove("DSIG")
1020			tagList.append("DSIG")
1021		if "CFF " in tagList:
1022			tableOrder = OTFTableOrder
1023		else:
1024			tableOrder = TTFTableOrder
1025	orderedTables = []
1026	for tag in tableOrder:
1027		if tag in tagList:
1028			orderedTables.append(tag)
1029			tagList.remove(tag)
1030	orderedTables.extend(tagList)
1031	return orderedTables
1032
1033
1034def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
1035	"""Rewrite a font file, ordering the tables as recommended by the
1036	OpenType specification 1.4.
1037	"""
1038	inFile.seek(0)
1039	outFile.seek(0)
1040	reader = SFNTReader(inFile, checkChecksums=checkChecksums)
1041	writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData)
1042	tables = list(reader.keys())
1043	for tag in sortedTagList(tables, tableOrder):
1044		writer[tag] = reader[tag]
1045	writer.close()
1046
1047
1048def maxPowerOfTwo(x):
1049	"""Return the highest exponent of two, so that
1050	(2 ** exponent) <= x.  Return 0 if x is 0.
1051	"""
1052	exponent = 0
1053	while x:
1054		x = x >> 1
1055		exponent = exponent + 1
1056	return max(exponent - 1, 0)
1057
1058
1059def getSearchRange(n, itemSize=16):
1060	"""Calculate searchRange, entrySelector, rangeShift.
1061	"""
1062	# itemSize defaults to 16, for backward compatibility
1063	# with upstream fonttools.
1064	exponent = maxPowerOfTwo(n)
1065	searchRange = (2 ** exponent) * itemSize
1066	entrySelector = exponent
1067	rangeShift = max(0, n * itemSize - searchRange)
1068	return searchRange, entrySelector, rangeShift
1069