• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""fontTools.ttLib -- a package for dealing with TrueType fonts.
2
3This package offers translators to convert TrueType fonts to Python
4objects and vice versa, and additionally from Python to TTX (an XML-based
5text format) and vice versa.
6
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
19>>> tt.saveXML("afont.ttx")
20Dumping '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()
37>>> tt2.importXML("afont.ttx")
38>>> tt2['maxp'].numGlyphs
39242
40>>>
41
42"""
43
44from __future__ import print_function, division, absolute_import
45from fontTools.misc.py23 import *
46import os
47import sys
48
49haveMacSupport = 0
50if sys.platform == "mac":
51	haveMacSupport = 1
52elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0):
53	# Python 2.2's Mac support is broken, so don't enable it there.
54	haveMacSupport = 1
55
56
57class TTLibError(Exception): pass
58
59
60class TTFont(object):
61
62	"""The main font object. It manages file input and output, and offers
63	a convenient way of accessing tables.
64	Tables will be only decompiled when necessary, ie. when they're actually
65	accessed. This means that simple operations can be extremely fast.
66	"""
67
68	def __init__(self, file=None, res_name_or_index=None,
69			sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False,
70			verbose=False, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
71			recalcTimestamp=True, fontNumber=-1, lazy=False, quiet=False):
72
73		"""The constructor can be called with a few different arguments.
74		When reading a font from disk, 'file' should be either a pathname
75		pointing to a file, or a readable file object.
76
77		It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
78		resource name or an sfnt resource index number or zero. The latter
79		case will cause TTLib to autodetect whether the file is a flat file
80		or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
81		will be read!)
82
83		The 'checkChecksums' argument is used to specify how sfnt
84		checksums are treated upon reading a file from disk:
85			0: don't check (default)
86			1: check, print warnings if a wrong checksum is found
87			2: check, raise an exception if a wrong checksum is found.
88
89		The TTFont constructor can also be called without a 'file'
90		argument: this is the way to create a new empty font.
91		In this case you can optionally supply the 'sfntVersion' argument,
92		and a 'flavor' which can be None, or 'woff'.
93
94		If the recalcBBoxes argument is false, a number of things will *not*
95		be recalculated upon save/compile:
96			1) glyph bounding boxes
97			2) maxp font bounding box
98			3) hhea min/max values
99		(1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-).
100		Additionally, upon importing an TTX file, this option cause glyphs
101		to be compiled right away. This should reduce memory consumption
102		greatly, and therefore should have some impact on the time needed
103		to parse/compile large fonts.
104
105		If the recalcTimestamp argument is false, the modified timestamp in the
106		'head' table will *not* be recalculated upon save/compile.
107
108		If the allowVID argument is set to true, then virtual GID's are
109		supported. Asking for a glyph ID with a glyph name or GID that is not in
110		the font will return a virtual GID.   This is valid for GSUB and cmap
111		tables. For SING glyphlets, the cmap table is used to specify Unicode
112		values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested
113		and does not exist in the font, or the glyphname has the form glyphN
114		and does not exist in the font, then N is used as the virtual GID.
115		Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new
116		virtual GIDs, the next is one less than the previous.
117
118		If ignoreDecompileErrors is set to True, exceptions raised in
119		individual tables during decompilation will be ignored, falling
120		back to the DefaultTable implementation, which simply keeps the
121		binary data.
122
123		If lazy is set to True, many data structures are loaded lazily, upon
124		access only.
125		"""
126
127		from fontTools.ttLib import sfnt
128		self.verbose = verbose
129		self.quiet = quiet
130		self.lazy = lazy
131		self.recalcBBoxes = recalcBBoxes
132		self.recalcTimestamp = recalcTimestamp
133		self.tables = {}
134		self.reader = None
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
141		self.ignoreDecompileErrors = ignoreDecompileErrors
142
143		if not file:
144			self.sfntVersion = sfntVersion
145			self.flavor = flavor
146			self.flavorData = None
147			return
148		if not hasattr(file, "read"):
149			# assume file is a string
150			if haveMacSupport and res_name_or_index is not None:
151				# on the mac, we deal with sfnt resources as well as flat files
152				from . import macUtils
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
165		self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber)
166		self.sfntVersion = self.reader.sfntVersion
167		self.flavor = self.reader.flavor
168		self.flavorData = self.reader.flavorData
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
175	def save(self, file, makeSuitcase=False, reorderTables=True):
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
180		On the Mac, if makeSuitcase is true, a suitcase (resource fork)
181		file will we made instead of a flat .ttf file.
182		"""
183		from fontTools.ttLib import sfnt
184		if not hasattr(file, "write"):
185			closeStream = 1
186			if os.name == "mac" and makeSuitcase:
187				from . import macUtils
188				file = macUtils.SFNTResourceWriter(file, self)
189			else:
190				file = open(file, "wb")
191				if os.name == "mac":
192					from fontTools.misc.macCreator import setMacCreatorAndType
193					setMacCreatorAndType(file.name, 'mdos', 'BINA')
194		else:
195			# assume "file" is a writable file object
196			closeStream = 0
197
198		tags = list(self.keys())
199		if "GlyphOrder" in tags:
200			tags.remove("GlyphOrder")
201		numTables = len(tags)
202		if reorderTables:
203			import tempfile
204			tmp = tempfile.TemporaryFile(prefix="ttx-fonttools")
205		else:
206			tmp = file
207		writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData)
208
209		done = []
210		for tag in tags:
211			self._writeTable(tag, writer, done)
212
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()
223
224	def saveXML(self, fileOrPath, progress=None, quiet=False,
225			tables=None, skipTables=None, splitTables=False, disassembleInstructions=True,
226			bitmapGlyphDataFormat='raw'):
227		"""Export the font as TTX (an XML-based text file), or as a series of text
228		files when splitTables is true. In the latter case, the 'fileOrPath'
229		argument should be a path to a directory.
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.
233		"""
234		from fontTools import version
235		from fontTools.misc import xmlWriter
236
237		self.disassembleInstructions = disassembleInstructions
238		self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
239		if not tables:
240			tables = list(self.keys())
241			if "GlyphOrder" not in tables:
242				tables = ["GlyphOrder"] + tables
243			if skipTables:
244				for tag in skipTables:
245					if tag in tables:
246						tables.remove(tag)
247		numTables = len(tables)
248		if progress:
249			progress.set(0, numTables)
250			idlefunc = getattr(progress, "idle", None)
251		else:
252			idlefunc = None
253
254		writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc)
255		writer.begintag("ttFont", sfntVersion=repr(self.sfntVersion)[1:-1],
256				ttLibVersion=version)
257		writer.newline()
258
259		if not splitTables:
260			writer.newline()
261		else:
262			# 'fileOrPath' must now be a path
263			path, ext = os.path.splitext(fileOrPath)
264			fileNameTemplate = path + ".%s" + ext
265
266		for i in range(numTables):
267			if progress:
268				progress.set(i)
269			tag = tables[i]
270			if splitTables:
271				tablePath = fileNameTemplate % tagToIdentifier(tag)
272				tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc)
273				tableWriter.begintag("ttFont", ttLibVersion=version)
274				tableWriter.newline()
275				tableWriter.newline()
276				writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
277				writer.newline()
278			else:
279				tableWriter = writer
280			self._tableToXML(tableWriter, tag, progress, quiet)
281			if splitTables:
282				tableWriter.endtag("ttFont")
283				tableWriter.newline()
284				tableWriter.close()
285		if progress:
286			progress.set((i + 1))
287		writer.endtag("ttFont")
288		writer.newline()
289		writer.close()
290		if self.verbose:
291			debugmsg("Done dumping TTX")
292
293	def _tableToXML(self, writer, tag, progress, quiet):
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		if progress:
300			progress.setLabel(report)
301		elif self.verbose:
302			debugmsg(report)
303		else:
304			if not quiet:
305				print(report)
306		if tag not in self:
307			return
308		xmlTag = tagToXML(tag)
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
322	def importXML(self, file, progress=None, quiet=False):
323		"""Import a TTX file (an XML-based text format), so as to recreate
324		a font object.
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(file, self, progress, quiet)
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		try:
373			return self.tables[tag]
374		except KeyError:
375			if tag == "GlyphOrder":
376				table = GlyphOrder(tag)
377				self.tables[tag] = table
378				return table
379			if self.reader is not None:
380				import traceback
381				if self.verbose:
382					debugmsg("Reading '%s' table from disk" % tag)
383				data = self.reader[tag]
384				tableClass = getTableClass(tag)
385				table = tableClass(tag)
386				self.tables[tag] = table
387				if self.verbose:
388					debugmsg("Decompiling '%s' table" % tag)
389				try:
390					table.decompile(data, self)
391				except:
392					if not self.ignoreDecompileErrors:
393						raise
394					# fall back to DefaultTable, retaining the binary table data
395					print("An exception occurred during the decompilation of the '%s' table" % tag)
396					from .tables.DefaultTable import DefaultTable
397					file = StringIO()
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)
403				return table
404			else:
405				raise KeyError("'%s' table not found" % tag)
406
407	def __setitem__(self, tag, table):
408		self.tables[Tag(tag)] = table
409
410	def __delitem__(self, tag):
411		if tag not in self:
412			raise KeyError("'%s' table not found" % tag)
413		if tag in self.tables:
414			del self.tables[tag]
415		if self.reader and tag in self.reader:
416			del self.reader[tag]
417
418	def get(self, tag, default=None):
419		try:
420			return self[tag]
421		except KeyError:
422			return default
423
424	def setGlyphOrder(self, glyphOrder):
425		self.glyphOrder = glyphOrder
426
427	def getGlyphOrder(self):
428		try:
429			return self.glyphOrder
430		except AttributeError:
431			pass
432		if 'CFF ' in self:
433			cff = self['CFF ']
434			self.glyphOrder = cff.getGlyphOrder()
435		elif 'post' in self:
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				#
444				self._getGlyphNamesFromCmap()
445			else:
446				self.glyphOrder = glyphOrder
447		else:
448			self._getGlyphNamesFromCmap()
449		return self.glyphOrder
450
451	def _getGlyphNamesFromCmap(self):
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.
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
485		# to work with (so we don't get called recursively).
486		self.glyphOrder = glyphOrder
487		# Get a (new) temporary cmap (based on the just invented names)
488		tempcmap = self['cmap'].getcmap(3, 1)
489		if tempcmap is not None:
490			# we have a unicode cmap
491			from fontTools import agl
492			cmap = tempcmap.cmap
493			# create a reverse cmap dict
494			reversecmap = {}
495			for unicode, name in list(cmap.items()):
496				reversecmap[name] = unicode
497			allNames = {}
498			for i in range(numGlyphs):
499				tempName = glyphOrder[i]
500				if tempName in reversecmap:
501					unicode = reversecmap[tempName]
502					if unicode in agl.UV2AGL:
503						# get name from the Adobe Glyph List
504						glyphName = agl.UV2AGL[unicode]
505					else:
506						# create uni<CODE> name
507						glyphName = "uni%04X" % unicode
508					tempName = glyphName
509					n = 1
510					while tempName in allNames:
511						tempName = glyphName + "#" + repr(n)
512						n = n + 1
513					glyphOrder[i] = tempName
514					allNames[tempName] = 1
515			# Delete the temporary cmap table from the cache, so it can
516			# be parsed again with the right names.
517			del self.tables['cmap']
518		else:
519			pass # no unicode cmap available, stick with the invented names
520		self.glyphOrder = glyphOrder
521		if cmapLoading:
522			# restore partially loaded cmap, so it can continue loading
523			# using the proper names.
524			self.tables['cmap'] = cmapLoading
525
526	def getGlyphNames(self):
527		"""Get a list of glyph names, sorted alphabetically."""
528		glyphNames = sorted(self.getGlyphOrder()[:])
529		return glyphNames
530
531	def getGlyphNames2(self):
532		"""Get a list of glyph names, sorted alphabetically,
533		but not case sensitive.
534		"""
535		from fontTools.misc import textTools
536		return textTools.caselessSort(self.getGlyphOrder())
537
538	def getGlyphName(self, glyphID, requireReal=False):
539		try:
540			return self.getGlyphOrder()[glyphID]
541		except IndexError:
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
557	def getGlyphID(self, glyphName, requireReal=False):
558		if not hasattr(self, "_reverseGlyphOrderDict"):
559			self._buildReverseGlyphOrderDict()
560		glyphOrder = self.getGlyphOrder()
561		d = self._reverseGlyphOrderDict
562		if glyphName not in d:
563			if glyphName in glyphOrder:
564				self._buildReverseGlyphOrderDict()
565				return self.getGlyphID(glyphName)
566			else:
567				if requireReal:
568					raise KeyError(glyphName)
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)
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
587						if glyphID is None:
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
594		glyphID = d[glyphName]
595		if glyphName != glyphOrder[glyphID]:
596			self._buildReverseGlyphOrderDict()
597			return self.getGlyphID(glyphName)
598		return glyphID
599
600	def getReverseGlyphMap(self, rebuild=False):
601		if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
602			self._buildReverseGlyphOrderDict()
603		return self._reverseGlyphOrderDict
604
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
617		tableClass = getTableClass(tag)
618		for masterTable in tableClass.dependencies:
619			if masterTable not in done:
620				if masterTable in self:
621					self._writeTable(masterTable, writer, done)
622				else:
623					done.append(masterTable)
624		tabledata = self.getTableData(tag)
625		if self.verbose:
626			debugmsg("writing '%s' table to disk" % tag)
627		writer[tag] = tabledata
628		done.append(tag)
629
630	def getTableData(self, tag):
631		"""Returns raw table data, whether compiled or directly read from disk.
632		"""
633		tag = Tag(tag)
634		if self.isLoaded(tag):
635			if self.verbose:
636				debugmsg("compiling '%s' table" % tag)
637			return self.tables[tag].compile(self)
638		elif self.reader and tag in self.reader:
639			if self.verbose:
640				debugmsg("Reading '%s' table from disk" % tag)
641			return self.reader[tag]
642		else:
643			raise KeyError(tag)
644
645	def getGlyphSet(self, preferCFF=True):
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		"""
657		if preferCFF and "CFF " in self:
658			return list(self["CFF "].cff.values())[0].CharStrings
659		if "glyf" in self:
660			return _TTGlyphSet(self)
661		if "CFF " in self:
662			return list(self["CFF "].cff.values())[0].CharStrings
663		raise TTLibError("Font contains no outlines")
664
665
666class _TTGlyphSet(object):
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):
680		return list(self._ttFont["glyf"].keys())
681
682	def has_key(self, glyphName):
683		return glyphName in self._ttFont["glyf"]
684
685	__contains__ = has_key
686
687	def __getitem__(self, glyphName):
688		return _TTGlyph(glyphName, self._ttFont)
689
690	def get(self, glyphName, default=None):
691		try:
692			return self[glyphName]
693		except KeyError:
694			return default
695
696
697class _TTGlyph(object):
698
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.
702	"""
703
704	def __init__(self, glyphName, ttFont):
705		self._glyphName = glyphName
706		self._ttFont = ttFont
707		self.width, self.lsb = self._ttFont['hmtx'][self._glyphName]
708
709	def draw(self, pen):
710		"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
711		how that works.
712		"""
713		glyfTable = self._ttFont['glyf']
714		glyph = glyfTable[self._glyphName]
715		if hasattr(glyph, "xMin"):
716			offset = self.lsb - glyph.xMin
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:
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.
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()
756
757
758class GlyphOrder(object):
759
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.
762	"""
763
764	def __init__(self, tag=None):
765		pass
766
767	def toXML(self, writer, ttFont):
768		glyphOrder = ttFont.getGlyphOrder()
769		writer.comment("The 'id' attribute is only for humans; "
770				"it is ignored when parsed.")
771		writer.newline()
772		for i in range(len(glyphOrder)):
773			glyphName = glyphOrder[i]
774			writer.simpletag("GlyphID", id=i, name=glyphName)
775			writer.newline()
776
777	def fromXML(self, name, attrs, content, ttFont):
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
785def getTableModule(tag):
786	"""Fetch the packer/unpacker module for a table.
787	Return None when no module is found.
788	"""
789	from . import tables
790	pyTag = tagToIdentifier(tag)
791	try:
792		__import__("fontTools.ttLib.tables." + pyTag)
793	except ImportError as err:
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
802	else:
803		return getattr(tables, pyTag)
804
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:
812		from .tables.DefaultTable import DefaultTable
813		return DefaultTable
814	pyTag = tagToIdentifier(tag)
815	tableClass = getattr(module, "table_" + pyTag)
816	return tableClass
817
818
819def 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
828def newTable(tag):
829	"""Return a new instance of a table."""
830	tableClass = getTableClass(tag)
831	return tableClass(tag)
832
833
834def _escapechar(c):
835	"""Helper function for tagToIdentifier()"""
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:
842		return hex(byteord(c))[2:]
843
844
845def tagToIdentifier(tag):
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
859	tag = Tag(tag)
860	if tag == "GlyphOrder":
861		return tag
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
873def identifierToTag(ident):
874	"""the opposite of tagToIdentifier()"""
875	if ident == "GlyphOrder":
876		return ident
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
888			tag = tag + chr(int(ident[i:i+2], 16))
889	# append trailing spaces
890	tag = tag + (4 - len(tag)) * ' '
891	return Tag(tag)
892
893
894def tagToXML(tag):
895	"""Similarly to tagToIdentifier(), this converts a TT tag
896	to a valid XML element name. Since XML element names are
897	case sensitive, this is a fairly simple/readable translation.
898	"""
899	import re
900	tag = Tag(tag)
901	if tag == "OS/2":
902		return "OS_2"
903	elif tag == "GlyphOrder":
904		return tag
905	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
906		return tag.strip()
907	else:
908		return tagToIdentifier(tag)
909
910
911def xmlToTag(tag):
912	"""The opposite of tagToXML()"""
913	if tag == "OS_2":
914		return Tag("OS/2")
915	if len(tag) == 8:
916		return identifierToTag(tag)
917	else:
918		return Tag(tag + " " * (4 - len(tag)))
919
920
921def debugmsg(msg):
922	import time
923	print(msg + time.strftime("  (%H:%M:%S)", time.localtime(time.time())))
924
925
926# 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"]
930
931OTFTableOrder = ["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	"""
939	tagList = sorted(tagList)
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
947		else:
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
956
957
958def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
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)
964	writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData)
965	tables = list(reader.keys())
966	for tag in sortedTagList(tables, tableOrder):
967		writer[tag] = reader[tag]
968	writer.close()
969
970
971def maxPowerOfTwo(x):
972	"""Return the highest exponent of two, so that
973	(2 ** exponent) <= x.  Return 0 if x is 0.
974	"""
975	exponent = 0
976	while x:
977		x = x >> 1
978		exponent = exponent + 1
979	return max(exponent - 1, 0)
980
981
982def getSearchRange(n, itemSize):
983	"""Calculate searchRange, entrySelector, rangeShift.
984	"""
985	# This stuff needs to be stored in the file, because?
986	exponent = maxPowerOfTwo(n)
987	searchRange = (2 ** exponent) * itemSize
988	entrySelector = exponent
989	rangeShift = max(0, n * itemSize - searchRange)
990	return searchRange, entrySelector, rangeShift
991