__init__.py revision 79c1316d6561f3ae3196425008883c1872f1f814
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
44#
45# $Id: __init__.py,v 1.46 2005-01-24 10:06:45 jvr Exp $
46#
47
48import sys
49import os
50import string
51
52haveMacSupport = 0
53if sys.platform == "mac":
54	haveMacSupport = 1
55elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0):
56	# Python 2.2's Mac support is broken, so don't enable it there.
57	haveMacSupport = 1
58
59
60class TTLibError(Exception): pass
61
62
63class TTFont:
64
65	"""The main font object. It manages file input and output, and offers
66	a convenient way of accessing tables.
67	Tables will be only decompiled when neccesary, ie. when they're actually
68	accessed. This means that simple operations can be extremely fast.
69	"""
70
71	def __init__(self, file=None, res_name_or_index=None,
72			sfntVersion="\000\001\000\000", checkChecksums=0,
73			verbose=0, recalcBBoxes=1):
74
75		"""The constructor can be called with a few different arguments.
76		When reading a font from disk, 'file' should be either a pathname
77		pointing to a file, or a readable file object.
78
79		It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
80		resource name or an sfnt resource index number or zero. The latter
81		case will cause TTLib to autodetect whether the file is a flat file
82		or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
83		will be read!)
84
85		The 'checkChecksums' argument is used to specify how sfnt
86		checksums are treated upon reading a file from disk:
87			0: don't check (default)
88			1: check, print warnings if a wrong checksum is found
89			2: check, raise an exception if a wrong checksum is found.
90
91		The TTFont constructor can also be called without a 'file'
92		argument: this is the way to create a new empty font.
93		In this case you can optionally supply the 'sfntVersion' argument.
94
95		If the recalcBBoxes argument is false, a number of things will *not*
96		be recalculated upon save/compile:
97			1) glyph bounding boxes
98			2) maxp font bounding box
99			3) hhea min/max values
100		(1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-).
101		Additionally, upon importing an TTX file, this option cause glyphs
102		to be compiled right away. This should reduce memory consumption
103		greatly, and therefore should have some impact on the time needed
104		to parse/compile large fonts.
105		"""
106
107		import sfnt
108		self.verbose = verbose
109		self.recalcBBoxes = recalcBBoxes
110		self.tables = {}
111		self.reader = None
112		if not file:
113			self.sfntVersion = sfntVersion
114			return
115		if not hasattr(file, "read"):
116			# assume file is a string
117			if haveMacSupport and res_name_or_index is not None:
118				# on the mac, we deal with sfnt resources as well as flat files
119				import macUtils
120				if res_name_or_index == 0:
121					if macUtils.getSFNTResIndices(file):
122						# get the first available sfnt font.
123						file = macUtils.SFNTResourceReader(file, 1)
124					else:
125						file = open(file, "rb")
126				else:
127					file = macUtils.SFNTResourceReader(file, res_name_or_index)
128			else:
129				file = open(file, "rb")
130		else:
131			pass # assume "file" is a readable file object
132		self.reader = sfnt.SFNTReader(file, checkChecksums)
133		self.sfntVersion = self.reader.sfntVersion
134
135	def close(self):
136		"""If we still have a reader object, close it."""
137		if self.reader is not None:
138			self.reader.close()
139
140	def save(self, file, makeSuitcase=0, reorderTables=1):
141		"""Save the font to disk. Similarly to the constructor,
142		the 'file' argument can be either a pathname or a writable
143		file object.
144
145		On the Mac, if makeSuitcase is true, a suitcase (resource fork)
146		file will we made instead of a flat .ttf file.
147		"""
148		from fontTools.ttLib import sfnt
149		if not hasattr(file, "write"):
150			closeStream = 1
151			if os.name == "mac" and makeSuitcase:
152				import macUtils
153				file = macUtils.SFNTResourceWriter(file, self)
154			else:
155				file = open(file, "wb")
156				if os.name == "mac":
157					import macfs
158					fss = macfs.FSSpec(file.name)
159					fss.SetCreatorType('mdos', 'BINA')
160		else:
161			# assume "file" is a writable file object
162			closeStream = 0
163
164		tags = self.keys()
165		if "GlyphOrder" in tags:
166			tags.remove("GlyphOrder")
167		numTables = len(tags)
168		if reorderTables:
169			import tempfile
170			tmp = tempfile.TemporaryFile(prefix="ttx-fonttools")
171		else:
172			tmp = file
173		writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion)
174
175		done = []
176		for tag in tags:
177			self._writeTable(tag, writer, done)
178
179		writer.close()
180
181		if reorderTables:
182			tmp.flush()
183			tmp.seek(0)
184			reorderFontTables(tmp, file)
185			tmp.close()
186
187		if closeStream:
188			file.close()
189
190	def saveXML(self, fileOrPath, progress=None,
191			tables=None, skipTables=None, splitTables=0, disassembleInstructions=1):
192		"""Export the font as TTX (an XML-based text file), or as a series of text
193		files when splitTables is true. In the latter case, the 'fileOrPath'
194		argument should be a path to a directory.
195		The 'tables' argument must either be false (dump all tables) or a
196		list of tables to dump. The 'skipTables' argument may be a list of tables
197		to skip, but only when the 'tables' argument is false.
198		"""
199		from fontTools import version
200		import xmlWriter
201
202		self.disassembleInstructions = disassembleInstructions
203		if not tables:
204			tables = self.keys()
205			if "GlyphOrder" not in tables:
206				tables = ["GlyphOrder"] + tables
207			if skipTables:
208				for tag in skipTables:
209					if tag in tables:
210						tables.remove(tag)
211		numTables = len(tables)
212		if progress:
213			progress.set(0, numTables)
214			idlefunc = getattr(progress, "idle", None)
215		else:
216			idlefunc = None
217
218		writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc)
219		writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1],
220				ttLibVersion=version)
221		writer.newline()
222
223		if not splitTables:
224			writer.newline()
225		else:
226			# 'fileOrPath' must now be a path
227			path, ext = os.path.splitext(fileOrPath)
228			fileNameTemplate = path + ".%s" + ext
229
230		for i in range(numTables):
231			if progress:
232				progress.set(i)
233			tag = tables[i]
234			if splitTables:
235				tablePath = fileNameTemplate % tagToIdentifier(tag)
236				tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc)
237				tableWriter.begintag("ttFont", ttLibVersion=version)
238				tableWriter.newline()
239				tableWriter.newline()
240				writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
241				writer.newline()
242			else:
243				tableWriter = writer
244			self._tableToXML(tableWriter, tag, progress)
245			if splitTables:
246				tableWriter.endtag("ttFont")
247				tableWriter.newline()
248				tableWriter.close()
249		if progress:
250			progress.set((i + 1))
251		writer.endtag("ttFont")
252		writer.newline()
253		writer.close()
254		if self.verbose:
255			debugmsg("Done dumping TTX")
256
257	def _tableToXML(self, writer, tag, progress):
258		if self.has_key(tag):
259			table = self[tag]
260			report = "Dumping '%s' table..." % tag
261		else:
262			report = "No '%s' table found." % tag
263		if progress:
264			progress.setLabel(report)
265		elif self.verbose:
266			debugmsg(report)
267		else:
268			print report
269		if not self.has_key(tag):
270			return
271		xmlTag = tagToXML(tag)
272		if hasattr(table, "ERROR"):
273			writer.begintag(xmlTag, ERROR="decompilation error")
274		else:
275			writer.begintag(xmlTag)
276		writer.newline()
277		if tag in ("glyf", "CFF "):
278			table.toXML(writer, self, progress)
279		else:
280			table.toXML(writer, self)
281		writer.endtag(xmlTag)
282		writer.newline()
283		writer.newline()
284
285	def importXML(self, file, progress=None):
286		"""Import a TTX file (an XML-based text format), so as to recreate
287		a font object.
288		"""
289		if self.has_key("maxp") and self.has_key("post"):
290			# Make sure the glyph order is loaded, as it otherwise gets
291			# lost if the XML doesn't contain the glyph order, yet does
292			# contain the table which was originally used to extract the
293			# glyph names from (ie. 'post', 'cmap' or 'CFF ').
294			self.getGlyphOrder()
295		import xmlImport
296		xmlImport.importXML(self, file, progress)
297
298	def isLoaded(self, tag):
299		"""Return true if the table identified by 'tag' has been
300		decompiled and loaded into memory."""
301		return self.tables.has_key(tag)
302
303	def has_key(self, tag):
304		if self.isLoaded(tag):
305			return 1
306		elif self.reader and self.reader.has_key(tag):
307			return 1
308		elif tag == "GlyphOrder":
309			return 1
310		else:
311			return 0
312
313	__contains__ = has_key
314
315	def keys(self):
316		keys = self.tables.keys()
317		if self.reader:
318			for key in self.reader.keys():
319				if key not in keys:
320					keys.append(key)
321
322		if "GlyphOrder" in keys:
323			keys.remove("GlyphOrder")
324		keys = sortedTagList(keys)
325		return ["GlyphOrder"] + keys
326
327	def __len__(self):
328		return len(self.keys())
329
330	def __getitem__(self, tag):
331		try:
332			return self.tables[tag]
333		except KeyError:
334			if tag == "GlyphOrder":
335				table = GlyphOrder(tag)
336				self.tables[tag] = table
337				return table
338			if self.reader is not None:
339				import traceback
340				if self.verbose:
341					debugmsg("Reading '%s' table from disk" % tag)
342				data = self.reader[tag]
343				tableClass = getTableClass(tag)
344				table = tableClass(tag)
345				self.tables[tag] = table
346				if self.verbose:
347					debugmsg("Decompiling '%s' table" % tag)
348				try:
349					table.decompile(data, self)
350				except "_ _ F O O _ _": # dummy exception to disable exception catching
351					print "An exception occurred during the decompilation of the '%s' table" % tag
352					from tables.DefaultTable import DefaultTable
353					import StringIO
354					file = StringIO.StringIO()
355					traceback.print_exc(file=file)
356					table = DefaultTable(tag)
357					table.ERROR = file.getvalue()
358					self.tables[tag] = table
359					table.decompile(data, self)
360				return table
361			else:
362				raise KeyError, "'%s' table not found" % tag
363
364	def __setitem__(self, tag, table):
365		self.tables[tag] = table
366
367	def __delitem__(self, tag):
368		if not self.has_key(tag):
369			raise KeyError, "'%s' table not found" % tag
370		if self.tables.has_key(tag):
371			del self.tables[tag]
372		if self.reader and self.reader.has_key(tag):
373			del self.reader[tag]
374
375	def setGlyphOrder(self, glyphOrder):
376		self.glyphOrder = glyphOrder
377
378	def getGlyphOrder(self):
379		try:
380			return self.glyphOrder
381		except AttributeError:
382			pass
383		if self.has_key('CFF '):
384			cff = self['CFF ']
385			self.glyphOrder = cff.getGlyphOrder()
386		elif self.has_key('post'):
387			# TrueType font
388			glyphOrder = self['post'].getGlyphOrder()
389			if glyphOrder is None:
390				#
391				# No names found in the 'post' table.
392				# Try to create glyph names from the unicode cmap (if available)
393				# in combination with the Adobe Glyph List (AGL).
394				#
395				self._getGlyphNamesFromCmap()
396			else:
397				self.glyphOrder = glyphOrder
398		else:
399			self._getGlyphNamesFromCmap()
400		return self.glyphOrder
401
402	def _getGlyphNamesFromCmap(self):
403		#
404		# This is rather convoluted, but then again, it's an interesting problem:
405		# - we need to use the unicode values found in the cmap table to
406		#   build glyph names (eg. because there is only a minimal post table,
407		#   or none at all).
408		# - but the cmap parser also needs glyph names to work with...
409		# So here's what we do:
410		# - make up glyph names based on glyphID
411		# - load a temporary cmap table based on those names
412		# - extract the unicode values, build the "real" glyph names
413		# - unload the temporary cmap table
414		#
415		if self.isLoaded("cmap"):
416			# Bootstrapping: we're getting called by the cmap parser
417			# itself. This means self.tables['cmap'] contains a partially
418			# loaded cmap, making it impossible to get at a unicode
419			# subtable here. We remove the partially loaded cmap and
420			# restore it later.
421			# This only happens if the cmap table is loaded before any
422			# other table that does f.getGlyphOrder()  or f.getGlyphName().
423			cmapLoading = self.tables['cmap']
424			del self.tables['cmap']
425		else:
426			cmapLoading = None
427		# Make up glyph names based on glyphID, which will be used by the
428		# temporary cmap and by the real cmap in case we don't find a unicode
429		# cmap.
430		numGlyphs = int(self['maxp'].numGlyphs)
431		glyphOrder = [None] * numGlyphs
432		glyphOrder[0] = ".notdef"
433		for i in range(1, numGlyphs):
434			glyphOrder[i] = "glyph%.5d" % i
435		# Set the glyph order, so the cmap parser has something
436		# to work with (so we don't get called recursively).
437		self.glyphOrder = glyphOrder
438		# Get a (new) temporary cmap (based on the just invented names)
439		tempcmap = self['cmap'].getcmap(3, 1)
440		if tempcmap is not None:
441			# we have a unicode cmap
442			from fontTools import agl
443			cmap = tempcmap.cmap
444			# create a reverse cmap dict
445			reversecmap = {}
446			for unicode, name in cmap.items():
447				reversecmap[name] = unicode
448			allNames = {}
449			for i in range(numGlyphs):
450				tempName = glyphOrder[i]
451				if reversecmap.has_key(tempName):
452					unicode = reversecmap[tempName]
453					if agl.UV2AGL.has_key(unicode):
454						# get name from the Adobe Glyph List
455						glyphName = agl.UV2AGL[unicode]
456					else:
457						# create uni<CODE> name
458						glyphName = "uni" + string.upper(string.zfill(
459								hex(unicode)[2:], 4))
460					tempName = glyphName
461					n = 1
462					while allNames.has_key(tempName):
463						tempName = glyphName + "#" + `n`
464						n = n + 1
465					glyphOrder[i] = tempName
466					allNames[tempName] = 1
467			# Delete the temporary cmap table from the cache, so it can
468			# be parsed again with the right names.
469			del self.tables['cmap']
470		else:
471			pass # no unicode cmap available, stick with the invented names
472		self.glyphOrder = glyphOrder
473		if cmapLoading:
474			# restore partially loaded cmap, so it can continue loading
475			# using the proper names.
476			self.tables['cmap'] = cmapLoading
477
478	def getGlyphNames(self):
479		"""Get a list of glyph names, sorted alphabetically."""
480		glyphNames = self.getGlyphOrder()[:]
481		glyphNames.sort()
482		return glyphNames
483
484	def getGlyphNames2(self):
485		"""Get a list of glyph names, sorted alphabetically,
486		but not case sensitive.
487		"""
488		from fontTools.misc import textTools
489		return textTools.caselessSort(self.getGlyphOrder())
490
491	def getGlyphName(self, glyphID):
492		try:
493			return self.getGlyphOrder()[glyphID]
494		except IndexError:
495			# XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in
496			# the cmap table than there are glyphs. I don't think it's legal...
497			return "glyph%.5d" % glyphID
498
499	def getGlyphID(self, glyphName):
500		if not hasattr(self, "_reverseGlyphOrderDict"):
501			self._buildReverseGlyphOrderDict()
502		glyphOrder = self.getGlyphOrder()
503		d = self._reverseGlyphOrderDict
504		if not d.has_key(glyphName):
505			if glyphName in glyphOrder:
506				self._buildReverseGlyphOrderDict()
507				return self.getGlyphID(glyphName)
508			else:
509				raise KeyError, glyphName
510		glyphID = d[glyphName]
511		if glyphName <> glyphOrder[glyphID]:
512			self._buildReverseGlyphOrderDict()
513			return self.getGlyphID(glyphName)
514		return glyphID
515
516	def _buildReverseGlyphOrderDict(self):
517		self._reverseGlyphOrderDict = d = {}
518		glyphOrder = self.getGlyphOrder()
519		for glyphID in range(len(glyphOrder)):
520			d[glyphOrder[glyphID]] = glyphID
521
522	def _writeTable(self, tag, writer, done):
523		"""Internal helper function for self.save(). Keeps track of
524		inter-table dependencies.
525		"""
526		if tag in done:
527			return
528		tableClass = getTableClass(tag)
529		for masterTable in tableClass.dependencies:
530			if masterTable not in done:
531				if self.has_key(masterTable):
532					self._writeTable(masterTable, writer, done)
533				else:
534					done.append(masterTable)
535		tabledata = self.getTableData(tag)
536		if self.verbose:
537			debugmsg("writing '%s' table to disk" % tag)
538		writer[tag] = tabledata
539		done.append(tag)
540
541	def getTableData(self, tag):
542		"""Returns raw table data, whether compiled or directly read from disk.
543		"""
544		if self.isLoaded(tag):
545			if self.verbose:
546				debugmsg("compiling '%s' table" % tag)
547			return self.tables[tag].compile(self)
548		elif self.reader and self.reader.has_key(tag):
549			if self.verbose:
550				debugmsg("Reading '%s' table from disk" % tag)
551			return self.reader[tag]
552		else:
553			raise KeyError, tag
554
555	def getGlyphSet(self, preferCFF=1):
556		"""Return a generic GlyphSet, which is a dict-like object
557		mapping glyph names to glyph objects. The returned glyph objects
558		have a .draw() method that supports the Pen protocol, and will
559		have an attribute named 'width', but only *after* the .draw() method
560		has been called.
561
562		If the font is CFF-based, the outlines will be taken from the 'CFF '
563		table. Otherwise the outlines will be taken from the 'glyf' table.
564		If the font contains both a 'CFF ' and a 'glyf' table, you can use
565		the 'preferCFF' argument to specify which one should be taken.
566		"""
567		if preferCFF and self.has_key("CFF "):
568			return self["CFF "].cff.values()[0].CharStrings
569		if self.has_key("glyf"):
570			return _TTGlyphSet(self)
571		if self.has_key("CFF "):
572			return self["CFF "].cff.values()[0].CharStrings
573		raise TTLibError, "Font contains no outlines"
574
575
576class _TTGlyphSet:
577
578	"""Generic dict-like GlyphSet class, meant as a TrueType counterpart
579	to CFF's CharString dict. See TTFont.getGlyphSet().
580	"""
581
582	# This class is distinct from the 'glyf' table itself because we need
583	# access to the 'hmtx' table, which could cause a dependency problem
584	# there when reading from XML.
585
586	def __init__(self, ttFont):
587		self._ttFont = ttFont
588
589	def keys(self):
590		return self._ttFont["glyf"].keys()
591
592	def has_key(self, glyphName):
593		return self._ttFont["glyf"].has_key(glyphName)
594
595	__contains__ = has_key
596
597	def __getitem__(self, glyphName):
598		return _TTGlyph(glyphName, self._ttFont)
599
600
601class _TTGlyph:
602
603	"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
604	that it has a .draw() method that takes a pen object as its only
605	argument. Additionally there is a 'width' attribute.
606	"""
607
608	def __init__(self, glyphName, ttFont):
609		self._glyphName = glyphName
610		self._ttFont = ttFont
611		self.width, self.lsb = self._ttFont['hmtx'][self._glyphName]
612
613	def draw(self, pen):
614		"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
615		how that works.
616		"""
617		glyfTable = self._ttFont['glyf']
618		glyph = glyfTable[self._glyphName]
619		if hasattr(glyph, "xMin"):
620			offset = self.lsb - glyph.xMin
621		else:
622			offset = 0
623		if glyph.isComposite():
624			for component in glyph:
625				glyphName, transform = component.getComponentInfo()
626				pen.addComponent(glyphName, transform)
627		else:
628			coordinates, endPts, flags = glyph.getCoordinates(glyfTable)
629			if offset:
630				coordinates = coordinates + (offset, 0)
631			start = 0
632			for end in endPts:
633				end = end + 1
634				contour = coordinates[start:end].tolist()
635				cFlags = flags[start:end].tolist()
636				start = end
637				if 1 not in cFlags:
638					# There is not a single on-curve point on the curve,
639					# use pen.qCurveTo's special case by specifying None
640					# as the on-curve point.
641					contour.append(None)
642					pen.qCurveTo(*contour)
643				else:
644					# Shuffle the points so that contour the is guaranteed
645					# to *end* in an on-curve point, which we'll use for
646					# the moveTo.
647					firstOnCurve = cFlags.index(1) + 1
648					contour = contour[firstOnCurve:] + contour[:firstOnCurve]
649					cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
650					pen.moveTo(contour[-1])
651					while contour:
652						nextOnCurve = cFlags.index(1) + 1
653						if nextOnCurve == 1:
654							pen.lineTo(contour[0])
655						else:
656							pen.qCurveTo(*contour[:nextOnCurve])
657						contour = contour[nextOnCurve:]
658						cFlags = cFlags[nextOnCurve:]
659				pen.closePath()
660
661
662class GlyphOrder:
663
664	"""A pseudo table. The glyph order isn't in the font as a separate
665	table, but it's nice to present it as such in the TTX format.
666	"""
667
668	def __init__(self, tag):
669		pass
670
671	def toXML(self, writer, ttFont):
672		glyphOrder = ttFont.getGlyphOrder()
673		writer.comment("The 'id' attribute is only for humans; "
674				"it is ignored when parsed.")
675		writer.newline()
676		for i in range(len(glyphOrder)):
677			glyphName = glyphOrder[i]
678			writer.simpletag("GlyphID", id=i, name=glyphName)
679			writer.newline()
680
681	def fromXML(self, (name, attrs, content), ttFont):
682		if not hasattr(self, "glyphOrder"):
683			self.glyphOrder = []
684			ttFont.setGlyphOrder(self.glyphOrder)
685		if name == "GlyphID":
686			self.glyphOrder.append(attrs["name"])
687
688
689def _test_endianness():
690	"""Test the endianness of the machine. This is crucial to know
691	since TrueType data is always big endian, even on little endian
692	machines. There are quite a few situations where we explicitly
693	need to swap some bytes.
694	"""
695	import struct
696	data = struct.pack("h", 0x01)
697	if data == "\000\001":
698		return "big"
699	elif data == "\001\000":
700		return "little"
701	else:
702		assert 0, "endian confusion!"
703
704endian = _test_endianness()
705
706
707def getTableModule(tag):
708	"""Fetch the packer/unpacker module for a table.
709	Return None when no module is found.
710	"""
711	import tables
712	pyTag = tagToIdentifier(tag)
713	try:
714		__import__("fontTools.ttLib.tables." + pyTag)
715	except ImportError:
716		return None
717	else:
718		return getattr(tables, pyTag)
719
720
721def getTableClass(tag):
722	"""Fetch the packer/unpacker class for a table.
723	Return None when no class is found.
724	"""
725	module = getTableModule(tag)
726	if module is None:
727		from tables.DefaultTable import DefaultTable
728		return DefaultTable
729	pyTag = tagToIdentifier(tag)
730	tableClass = getattr(module, "table_" + pyTag)
731	return tableClass
732
733
734def newTable(tag):
735	"""Return a new instance of a table."""
736	tableClass = getTableClass(tag)
737	return tableClass(tag)
738
739
740def _escapechar(c):
741	"""Helper function for tagToIdentifier()"""
742	import re
743	if re.match("[a-z0-9]", c):
744		return "_" + c
745	elif re.match("[A-Z]", c):
746		return c + "_"
747	else:
748		return hex(ord(c))[2:]
749
750
751def tagToIdentifier(tag):
752	"""Convert a table tag to a valid (but UGLY) python identifier,
753	as well as a filename that's guaranteed to be unique even on a
754	caseless file system. Each character is mapped to two characters.
755	Lowercase letters get an underscore before the letter, uppercase
756	letters get an underscore after the letter. Trailing spaces are
757	trimmed. Illegal characters are escaped as two hex bytes. If the
758	result starts with a number (as the result of a hex escape), an
759	extra underscore is prepended. Examples:
760		'glyf' -> '_g_l_y_f'
761		'cvt ' -> '_c_v_t'
762		'OS/2' -> 'O_S_2f_2'
763	"""
764	import re
765	if tag == "GlyphOrder":
766		return tag
767	assert len(tag) == 4, "tag should be 4 characters long"
768	while len(tag) > 1 and tag[-1] == ' ':
769		tag = tag[:-1]
770	ident = ""
771	for c in tag:
772		ident = ident + _escapechar(c)
773	if re.match("[0-9]", ident):
774		ident = "_" + ident
775	return ident
776
777
778def identifierToTag(ident):
779	"""the opposite of tagToIdentifier()"""
780	if ident == "GlyphOrder":
781		return ident
782	if len(ident) % 2 and ident[0] == "_":
783		ident = ident[1:]
784	assert not (len(ident) % 2)
785	tag = ""
786	for i in range(0, len(ident), 2):
787		if ident[i] == "_":
788			tag = tag + ident[i+1]
789		elif ident[i+1] == "_":
790			tag = tag + ident[i]
791		else:
792			# assume hex
793			tag = tag + chr(int(ident[i:i+2], 16))
794	# append trailing spaces
795	tag = tag + (4 - len(tag)) * ' '
796	return tag
797
798
799def tagToXML(tag):
800	"""Similarly to tagToIdentifier(), this converts a TT tag
801	to a valid XML element name. Since XML element names are
802	case sensitive, this is a fairly simple/readable translation.
803	"""
804	import re
805	if tag == "OS/2":
806		return "OS_2"
807	elif tag == "GlyphOrder":
808		return tag
809	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
810		return string.strip(tag)
811	else:
812		return tagToIdentifier(tag)
813
814
815def xmlToTag(tag):
816	"""The opposite of tagToXML()"""
817	if tag == "OS_2":
818		return "OS/2"
819	if len(tag) == 8:
820		return identifierToTag(tag)
821	else:
822		return tag + " " * (4 - len(tag))
823	return tag
824
825
826def debugmsg(msg):
827	import time
828	print msg + time.strftime("  (%H:%M:%S)", time.localtime(time.time()))
829
830
831# Table order as recommended in the OpenType specification 1.4
832TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX",
833                  "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf",
834                  "kern", "name", "post", "gasp", "PCLT"]
835
836OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post",
837                  "CFF "]
838
839def sortedTagList(tagList, tableOrder=None):
840	"""Return a sorted copy of tagList, sorted according to the OpenType
841	specification, or according to a custom tableOrder. If given and not
842	None, tableOrder needs to be a list of tag names.
843	"""
844	tagList = list(tagList)
845	tagList.sort()
846	if tableOrder is None:
847		if "DSIG" in tagList:
848			# DSIG should be last (XXX spec reference?)
849			tagList.remove("DSIG")
850			tagList.append("DSIG")
851		if "CFF " in tagList:
852			tableOrder = OTFTableOrder
853		else:
854			tableOrder = TTFTableOrder
855	orderedTables = []
856	for tag in tableOrder:
857		if tag in tagList:
858			orderedTables.append(tag)
859			tagList.remove(tag)
860	orderedTables.extend(tagList)
861	return orderedTables
862
863
864def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=0):
865	"""Rewrite a font file, ordering the tables as recommended by the
866	OpenType specification 1.4.
867	"""
868	from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
869	reader = SFNTReader(inFile, checkChecksums=checkChecksums)
870	writer = SFNTWriter(outFile, reader.numTables, reader.sfntVersion)
871	tables = reader.keys()
872	for tag in sortedTagList(tables, tableOrder):
873		writer[tag] = reader[tag]
874	writer.close()
875