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