__init__.py revision 2e4cc02ca31c43eafb6f752e44dbca9b004a3a2f
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.47 2005-03-08 09:50:56 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	def get(self, glyphName, default=None):
601		try:
602			return self[glyphName]
603		except KeyError:
604			return default
605
606
607class _TTGlyph:
608
609	"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
610	that it has a .draw() method that takes a pen object as its only
611	argument. Additionally there is a 'width' attribute.
612	"""
613
614	def __init__(self, glyphName, ttFont):
615		self._glyphName = glyphName
616		self._ttFont = ttFont
617		self.width, self.lsb = self._ttFont['hmtx'][self._glyphName]
618
619	def draw(self, pen):
620		"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
621		how that works.
622		"""
623		glyfTable = self._ttFont['glyf']
624		glyph = glyfTable[self._glyphName]
625		if hasattr(glyph, "xMin"):
626			offset = self.lsb - glyph.xMin
627		else:
628			offset = 0
629		if glyph.isComposite():
630			for component in glyph:
631				glyphName, transform = component.getComponentInfo()
632				pen.addComponent(glyphName, transform)
633		else:
634			coordinates, endPts, flags = glyph.getCoordinates(glyfTable)
635			if offset:
636				coordinates = coordinates + (offset, 0)
637			start = 0
638			for end in endPts:
639				end = end + 1
640				contour = coordinates[start:end].tolist()
641				cFlags = flags[start:end].tolist()
642				start = end
643				if 1 not in cFlags:
644					# There is not a single on-curve point on the curve,
645					# use pen.qCurveTo's special case by specifying None
646					# as the on-curve point.
647					contour.append(None)
648					pen.qCurveTo(*contour)
649				else:
650					# Shuffle the points so that contour the is guaranteed
651					# to *end* in an on-curve point, which we'll use for
652					# the moveTo.
653					firstOnCurve = cFlags.index(1) + 1
654					contour = contour[firstOnCurve:] + contour[:firstOnCurve]
655					cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
656					pen.moveTo(contour[-1])
657					while contour:
658						nextOnCurve = cFlags.index(1) + 1
659						if nextOnCurve == 1:
660							pen.lineTo(contour[0])
661						else:
662							pen.qCurveTo(*contour[:nextOnCurve])
663						contour = contour[nextOnCurve:]
664						cFlags = cFlags[nextOnCurve:]
665				pen.closePath()
666
667
668class GlyphOrder:
669
670	"""A pseudo table. The glyph order isn't in the font as a separate
671	table, but it's nice to present it as such in the TTX format.
672	"""
673
674	def __init__(self, tag):
675		pass
676
677	def toXML(self, writer, ttFont):
678		glyphOrder = ttFont.getGlyphOrder()
679		writer.comment("The 'id' attribute is only for humans; "
680				"it is ignored when parsed.")
681		writer.newline()
682		for i in range(len(glyphOrder)):
683			glyphName = glyphOrder[i]
684			writer.simpletag("GlyphID", id=i, name=glyphName)
685			writer.newline()
686
687	def fromXML(self, (name, attrs, content), ttFont):
688		if not hasattr(self, "glyphOrder"):
689			self.glyphOrder = []
690			ttFont.setGlyphOrder(self.glyphOrder)
691		if name == "GlyphID":
692			self.glyphOrder.append(attrs["name"])
693
694
695def _test_endianness():
696	"""Test the endianness of the machine. This is crucial to know
697	since TrueType data is always big endian, even on little endian
698	machines. There are quite a few situations where we explicitly
699	need to swap some bytes.
700	"""
701	import struct
702	data = struct.pack("h", 0x01)
703	if data == "\000\001":
704		return "big"
705	elif data == "\001\000":
706		return "little"
707	else:
708		assert 0, "endian confusion!"
709
710endian = _test_endianness()
711
712
713def getTableModule(tag):
714	"""Fetch the packer/unpacker module for a table.
715	Return None when no module is found.
716	"""
717	import tables
718	pyTag = tagToIdentifier(tag)
719	try:
720		__import__("fontTools.ttLib.tables." + pyTag)
721	except ImportError:
722		return None
723	else:
724		return getattr(tables, pyTag)
725
726
727def getTableClass(tag):
728	"""Fetch the packer/unpacker class for a table.
729	Return None when no class is found.
730	"""
731	module = getTableModule(tag)
732	if module is None:
733		from tables.DefaultTable import DefaultTable
734		return DefaultTable
735	pyTag = tagToIdentifier(tag)
736	tableClass = getattr(module, "table_" + pyTag)
737	return tableClass
738
739
740def newTable(tag):
741	"""Return a new instance of a table."""
742	tableClass = getTableClass(tag)
743	return tableClass(tag)
744
745
746def _escapechar(c):
747	"""Helper function for tagToIdentifier()"""
748	import re
749	if re.match("[a-z0-9]", c):
750		return "_" + c
751	elif re.match("[A-Z]", c):
752		return c + "_"
753	else:
754		return hex(ord(c))[2:]
755
756
757def tagToIdentifier(tag):
758	"""Convert a table tag to a valid (but UGLY) python identifier,
759	as well as a filename that's guaranteed to be unique even on a
760	caseless file system. Each character is mapped to two characters.
761	Lowercase letters get an underscore before the letter, uppercase
762	letters get an underscore after the letter. Trailing spaces are
763	trimmed. Illegal characters are escaped as two hex bytes. If the
764	result starts with a number (as the result of a hex escape), an
765	extra underscore is prepended. Examples:
766		'glyf' -> '_g_l_y_f'
767		'cvt ' -> '_c_v_t'
768		'OS/2' -> 'O_S_2f_2'
769	"""
770	import re
771	if tag == "GlyphOrder":
772		return tag
773	assert len(tag) == 4, "tag should be 4 characters long"
774	while len(tag) > 1 and tag[-1] == ' ':
775		tag = tag[:-1]
776	ident = ""
777	for c in tag:
778		ident = ident + _escapechar(c)
779	if re.match("[0-9]", ident):
780		ident = "_" + ident
781	return ident
782
783
784def identifierToTag(ident):
785	"""the opposite of tagToIdentifier()"""
786	if ident == "GlyphOrder":
787		return ident
788	if len(ident) % 2 and ident[0] == "_":
789		ident = ident[1:]
790	assert not (len(ident) % 2)
791	tag = ""
792	for i in range(0, len(ident), 2):
793		if ident[i] == "_":
794			tag = tag + ident[i+1]
795		elif ident[i+1] == "_":
796			tag = tag + ident[i]
797		else:
798			# assume hex
799			tag = tag + chr(int(ident[i:i+2], 16))
800	# append trailing spaces
801	tag = tag + (4 - len(tag)) * ' '
802	return tag
803
804
805def tagToXML(tag):
806	"""Similarly to tagToIdentifier(), this converts a TT tag
807	to a valid XML element name. Since XML element names are
808	case sensitive, this is a fairly simple/readable translation.
809	"""
810	import re
811	if tag == "OS/2":
812		return "OS_2"
813	elif tag == "GlyphOrder":
814		return tag
815	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
816		return string.strip(tag)
817	else:
818		return tagToIdentifier(tag)
819
820
821def xmlToTag(tag):
822	"""The opposite of tagToXML()"""
823	if tag == "OS_2":
824		return "OS/2"
825	if len(tag) == 8:
826		return identifierToTag(tag)
827	else:
828		return tag + " " * (4 - len(tag))
829	return tag
830
831
832def debugmsg(msg):
833	import time
834	print msg + time.strftime("  (%H:%M:%S)", time.localtime(time.time()))
835
836
837# Table order as recommended in the OpenType specification 1.4
838TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX",
839                  "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf",
840                  "kern", "name", "post", "gasp", "PCLT"]
841
842OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post",
843                  "CFF "]
844
845def sortedTagList(tagList, tableOrder=None):
846	"""Return a sorted copy of tagList, sorted according to the OpenType
847	specification, or according to a custom tableOrder. If given and not
848	None, tableOrder needs to be a list of tag names.
849	"""
850	tagList = list(tagList)
851	tagList.sort()
852	if tableOrder is None:
853		if "DSIG" in tagList:
854			# DSIG should be last (XXX spec reference?)
855			tagList.remove("DSIG")
856			tagList.append("DSIG")
857		if "CFF " in tagList:
858			tableOrder = OTFTableOrder
859		else:
860			tableOrder = TTFTableOrder
861	orderedTables = []
862	for tag in tableOrder:
863		if tag in tagList:
864			orderedTables.append(tag)
865			tagList.remove(tag)
866	orderedTables.extend(tagList)
867	return orderedTables
868
869
870def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=0):
871	"""Rewrite a font file, ordering the tables as recommended by the
872	OpenType specification 1.4.
873	"""
874	from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
875	reader = SFNTReader(inFile, checkChecksums=checkChecksums)
876	writer = SFNTWriter(outFile, reader.numTables, reader.sfntVersion)
877	tables = reader.keys()
878	for tag in sortedTagList(tables, tableOrder):
879		writer[tag] = reader[tag]
880	writer.close()
881