__init__.py revision 7842e56b97ce677b83bdab09cda48bc2d89ac75a
1"""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 XML and vice versa.
5
6Example interactive session:
7
8Python 1.5.2c1 (#43, Mar  9 1999, 13:06:43)  [CW PPC w/GUSI w/MSL]
9Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
10>>> from fontTools import ttLib
11>>> tt = ttLib.TTFont("afont.ttf")
12>>> tt['maxp'].numGlyphs
13242
14>>> tt['OS/2'].achVendID
15'B&H\000'
16>>> tt['head'].unitsPerEm
172048
18>>> tt.saveXML("afont.xml")
19Dumping 'LTSH' table...
20Dumping 'OS/2' table...
21Dumping 'VDMX' table...
22Dumping 'cmap' table...
23Dumping 'cvt ' table...
24Dumping 'fpgm' table...
25Dumping 'glyf' table...
26Dumping 'hdmx' table...
27Dumping 'head' table...
28Dumping 'hhea' table...
29Dumping 'hmtx' table...
30Dumping 'loca' table...
31Dumping 'maxp' table...
32Dumping 'name' table...
33Dumping 'post' table...
34Dumping 'prep' table...
35>>> tt2 = ttLib.TTFont()
36>>> tt2.importXML("afont.xml")
37>>> tt2['maxp'].numGlyphs
38242
39>>>
40
41"""
42
43__author__ = "Just van Rossum, just@letterror.com"
44__version__ = "1.0a5"
45
46
47import os
48import stat
49import types
50
51class TTLibError(Exception): pass
52
53
54class TTFont:
55
56	"""The main font object. It manages file input and output, and offers
57	a convenient way of accessing tables.
58	Tables will be only decompiled when neccesary, ie. when they're actually
59	accessed. This means that simple operations can be extremely fast.
60	"""
61
62	def __init__(self, file=None, res_name_or_index=None,
63			sfntVersion="\000\001\000\000", checkchecksums=0, verbose=0):
64
65		"""The constructor can be called with a few different arguments.
66		When reading a font from disk, 'file' should be either a pathname
67		pointing to a file, or a readable file object.
68
69		It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
70		resource name or an sfnt resource index number or zero. The latter
71		case will cause TTLib to autodetect whether the file is a flat file
72		or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
73		will be read!)
74
75		The 'checkchecksums' argument is used to specify how sfnt
76		checksums are treated upon reading a file from disk:
77			0: don't check (default)
78			1: check, print warnings if a wrong checksum is found (default)
79			2: check, raise an exception if a wrong checksum is found.
80
81		The TTFont constructor can also be called without a 'file'
82		argument: this is the way to create a new empty font.
83		In this case you can optionally supply the 'sfntVersion' argument.
84		"""
85
86		import sfnt
87		self.verbose = verbose
88		self.tables = {}
89		self.reader = None
90		if not file:
91			self.sfntVersion = sfntVersion
92			return
93		if type(file) == types.StringType:
94			if os.name == "mac" and res_name_or_index is not None:
95				# on the mac, we deal with sfnt resources as well as flat files
96				import macUtils
97				if res_name_or_index == 0:
98					if macUtils.getSFNTResIndices(file):
99						# get the first available sfnt font.
100						file = macUtils.SFNTResourceReader(file, 1)
101					else:
102						file = open(file, "rb")
103				else:
104					file = macUtils.SFNTResourceReader(file, res_name_or_index)
105			else:
106				file = open(file, "rb")
107		else:
108			pass # assume "file" is a readable file object
109		self.reader = sfnt.SFNTReader(file, checkchecksums)
110		self.sfntVersion = self.reader.sfntVersion
111
112	def close(self):
113		"""If we still have a reader object, close it."""
114		if self.reader is not None:
115			self.reader.close()
116
117	def save(self, file, make_suitcase=0):
118		"""Save the font to disk. Similarly to the constructor,
119		the 'file' argument can be either a pathname or a writable
120		file object.
121
122		On the Mac, if make_suitcase is non-zero, a suitcase file will
123		we made instead of a flat .ttf file.
124		"""
125		import sfnt
126		if type(file) == types.StringType:
127			if os.name == "mac" and make_suitcase:
128				import macUtils
129				file = macUtils.SFNTResourceWriter(file, self)
130			else:
131				file = open(file, "wb")
132				if os.name == "mac":
133					import macfs
134					fss = macfs.FSSpec(file.name)
135					fss.SetCreatorType('mdos', 'BINA')
136		else:
137			pass # assume "file" is a writable file object
138
139		tags = self.keys()
140		numTables = len(tags)
141		writer = sfnt.SFNTWriter(file, numTables, self.sfntVersion)
142
143		done = []
144		for tag in tags:
145			self._writeTable(tag, writer, done)
146
147		writer.close()
148
149	def saveXML(self, file, progress=None, tables=None):
150		"""Export the font as an XML-based text file.
151		"""
152		import xmlWriter
153		writer = xmlWriter.XMLWriter(file)
154		writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1],
155				ttlibVersion=__version__)
156		writer.newline()
157		writer.newline()
158		if not tables:
159			tables = self.keys()
160		numTables = len(tables)
161		numGlyphs = self['maxp'].numGlyphs
162		if progress:
163			progress.set(0, numTables * numGlyphs)
164		for i in range(numTables):
165			tag = tables[i]
166			table = self[tag]
167			report = "Dumping '%s' table..." % tag
168			if progress:
169				progress.setlabel(report)
170			elif self.verbose:
171				debugmsg(report)
172			else:
173				print report
174			xmltag = tag2xmltag(tag)
175			writer.begintag(xmltag)
176			writer.newline()
177			if tag == "glyf":
178				table.toXML(writer, self, progress)
179			elif tag == "CFF ":
180				table.toXML(writer, self, progress)
181			else:
182				table.toXML(writer, self)
183			writer.endtag(xmltag)
184			writer.newline()
185			writer.newline()
186			if progress:
187				progress.set(i * numGlyphs, numTables * numGlyphs)
188		writer.endtag("ttFont")
189		writer.newline()
190		writer.close()
191		if self.verbose:
192			debugmsg("Done dumping XML")
193
194	def importXML(self, file, progress=None):
195		"""Import an XML-based text file, so as to recreate
196		a font object.
197		"""
198		if self.tables:
199			raise error, "Can't import XML into existing font."
200		import xmlImport
201		from xml.parsers.xmlproc import xmlproc
202		builder = xmlImport.XMLApplication(self, progress)
203		if progress:
204			progress.set(0, os.stat(file)[stat.ST_SIZE] / 100 or 1)
205		proc = xmlImport.UnicodeProcessor()
206		proc.set_application(builder)
207		proc.set_error_handler(xmlImport.XMLErrorHandler(proc))
208		dir, filename = os.path.split(file)
209		if dir:
210			olddir = os.getcwd()
211			os.chdir(dir)
212		try:
213			proc.parse_resource(filename)
214			root = builder.root
215		finally:
216			if dir:
217				os.chdir(olddir)
218			# remove circular references
219			proc.deref()
220			del builder.progress
221
222	def isLoaded(self, tag):
223		"""Return true if the table identified by 'tag' has been
224		decompiled and loaded into memory."""
225		return self.tables.has_key(tag)
226
227	def has_key(self, tag):
228		"""Pretend we're a dictionary."""
229		if self.isLoaded(tag):
230			return 1
231		elif self.reader and self.reader.has_key(tag):
232			return 1
233		else:
234			return 0
235
236	def keys(self):
237		"""Pretend we're a dictionary."""
238		keys = self.tables.keys()
239		if self.reader:
240			for key in self.reader.keys():
241				if key not in keys:
242					keys.append(key)
243		keys.sort()
244		return keys
245
246	def __len__(self):
247		"""Pretend we're a dictionary."""
248		return len(self.keys())
249
250	def __getitem__(self, tag):
251		"""Pretend we're a dictionary."""
252		try:
253			return self.tables[tag]
254		except KeyError:
255			if self.reader is not None:
256				if self.verbose:
257					debugmsg("reading '%s' table from disk" % tag)
258				data = self.reader[tag]
259				tableclass = getTableClass(tag)
260				table = tableclass(tag)
261				self.tables[tag] = table
262				if self.verbose:
263					debugmsg("decompiling '%s' table" % tag)
264				table.decompile(data, self)
265				return table
266			else:
267				raise KeyError, "'%s' table not found" % tag
268
269	def __setitem__(self, tag, table):
270		"""Pretend we're a dictionary."""
271		self.tables[tag] = table
272
273	def __delitem__(self, tag):
274		"""Pretend we're a dictionary."""
275		del self.tables[tag]
276
277	def setGlyphOrder(self, glyphOrder):
278		self.glyphOrder = glyphOrder
279		if self.has_key('CFF '):
280			self['CFF '].setGlyphOrder(glyphOrder)
281		if self.has_key('glyf'):
282			self['glyf'].setGlyphOrder(glyphOrder)
283
284	def getGlyphOrder(self):
285		if not hasattr(self, "glyphOrder"):
286			if self.has_key('CFF '):
287				# CFF OpenType font
288				self.glyphOrder = self['CFF '].getGlyphOrder()
289			else:
290				# TrueType font
291				glyphOrder = self['post'].getGlyphOrder()
292				if glyphOrder is None:
293					#
294					# No names found in the 'post' table.
295					# Try to create glyph names from the unicode cmap (if available)
296					# in combination with the Adobe Glyph List (AGL).
297					#
298					self._getGlyphNamesFromCmap()
299				else:
300					self.glyphOrder = glyphOrder
301			# XXX what if a font contains 'glyf'/'post' table *and* CFF?
302		return self.glyphOrder
303
304	def _getGlyphNamesFromCmap(self):
305		# Make up glyph names based on glyphID, which will be used
306		# in case we don't find a unicode cmap.
307		numGlyphs = int(self['maxp'].numGlyphs)
308		glyphOrder = [None] * numGlyphs
309		glyphOrder[0] = ".notdef"
310		for i in range(1, numGlyphs):
311			glyphOrder[i] = "glyph%.5d" % i
312		# Set the glyph order, so the cmap parser has something
313		# to work with
314		self.glyphOrder = glyphOrder
315		# Get the temporary cmap (based on the just invented names)
316		tempcmap = self['cmap'].getcmap(3, 1)
317		if tempcmap is not None:
318			# we have a unicode cmap
319			import agl, string
320			cmap = tempcmap.cmap
321			# create a reverse cmap dict
322			reversecmap = {}
323			for unicode, name in cmap.items():
324				reversecmap[name] = unicode
325			assert len(reversecmap) == len(cmap)
326			for i in range(numGlyphs):
327				tempName = glyphOrder[i]
328				if reversecmap.has_key(tempName):
329					unicode = reversecmap[tempName]
330					if agl.UV2AGL.has_key(unicode):
331						# get name from the Adobe Glyph List
332						glyphOrder[i] = agl.UV2AGL[unicode]
333					else:
334						# create uni<CODE> name
335						glyphOrder[i] = "uni" + string.upper(string.zfill(hex(unicode)[2:], 4))
336			# Delete the cmap table from the cache, so it can be
337			# parsed again with the right names.
338			del self.tables['cmap']
339		else:
340			pass # no unicode cmap available, stick with the invented names
341		self.glyphOrder = glyphOrder
342
343	def getGlyphNames(self):
344		"""Get a list of glyph names, sorted alphabetically."""
345		glyphNames = self.getGlyphOrder()[:]
346		glyphNames.sort()
347		return glyphNames
348
349	def getGlyphNames2(self):
350		"""Get a list of glyph names, sorted alphabetically, but not case sensitive."""
351		from fontTools.misc import textTools
352		return textTools.caselessSort(self.getGlyphOrder())
353
354	def getGlyphName(self, glyphID):
355		return self.getGlyphOrder()[glyphID]
356
357	def getGlyphID(self, glyphName):
358		if not hasattr(self, "_reverseGlyphOrderDict"):
359			self._buildReverseGlyphOrderDict()
360		glyphOrder = self.getGlyphOrder()
361		d = self._reverseGlyphOrderDict
362		if not d.has_key(glyphName):
363			if glyphName in glyphOrder:
364				self._buildReverseGlyphOrderDict()
365				return self.getGlyphID(glyphName)
366			else:
367				raise KeyError, glyphName
368		glyphID = d[glyphName]
369		if glyphName <> glyphOrder[glyphID]:
370			self._buildReverseGlyphOrderDict()
371			return self.getGlyphID(glyphName)
372		return glyphID
373
374	def _buildReverseGlyphOrderDict(self):
375		self._reverseGlyphOrderDict = d = {}
376		glyphOrder = self.getGlyphOrder()
377		for glyphID in range(len(glyphOrder)):
378			d[glyphOrder[glyphID]] = glyphID
379
380	def _writeTable(self, tag, writer, done):
381		"""Internal helper function for self.save(). Keeps track of
382		inter-table dependencies.
383		"""
384		if tag in done:
385			return
386		tableclass = getTableClass(tag)
387		for masterTable in tableclass.dependencies:
388			if masterTable not in done:
389				if self.has_key(masterTable):
390					self._writeTable(masterTable, writer, done)
391				else:
392					done.append(masterTable)
393		tabledata = self._getTableData(tag)
394		if self.verbose:
395			debugmsg("writing '%s' table to disk" % tag)
396		writer[tag] = tabledata
397		done.append(tag)
398
399	def _getTableData(self, tag):
400		"""Internal helper function. Returns raw table data,
401		whether compiled or directly read from disk.
402		"""
403		if self.isLoaded(tag):
404			if self.verbose:
405				debugmsg("compiling '%s' table" % tag)
406			return self.tables[tag].compile(self)
407		elif self.reader and self.reader.has_key(tag):
408			if self.verbose:
409				debugmsg("reading '%s' table from disk" % tag)
410			return self.reader[tag]
411		else:
412			raise KeyError, tag
413
414
415def _test_endianness():
416	"""Test the endianness of the machine. This is crucial to know
417	since TrueType data is always big endian, even on little endian
418	machines. There are quite a few situations where we explicitly
419	need to swap some bytes.
420	"""
421	import struct
422	data = struct.pack("h", 0x01)
423	if data == "\000\001":
424		return "big"
425	elif data == "\001\000":
426		return "little"
427	else:
428		assert 0, "endian confusion!"
429
430endian = _test_endianness()
431
432
433def getTableModule(tag):
434	"""Fetch the packer/unpacker module for a table.
435	Return None when no module is found.
436	"""
437	import imp
438	import tables
439	py_tag = tag2identifier(tag)
440	try:
441		f, path, kind = imp.find_module(py_tag, tables.__path__)
442		if f:
443			f.close()
444	except ImportError:
445		return None
446	else:
447		module = __import__("fontTools.ttLib.tables." + py_tag)
448		return getattr(tables, py_tag)
449
450
451def getTableClass(tag):
452	"""Fetch the packer/unpacker class for a table.
453	Return None when no class is found.
454	"""
455	module = getTableModule(tag)
456	if module is None:
457		from tables.DefaultTable import DefaultTable
458		return DefaultTable
459	py_tag = tag2identifier(tag)
460	tableclass = getattr(module, "table_" + py_tag)
461	return tableclass
462
463
464def newtable(tag):
465	"""Return a new instance of a table."""
466	tableclass = getTableClass(tag)
467	return tableclass(tag)
468
469
470def _escapechar(c):
471	"""Helper function for tag2identifier()"""
472	import re
473	if re.match("[a-z0-9]", c):
474		return "_" + c
475	elif re.match("[A-Z]", c):
476		return c + "_"
477	else:
478		return hex(ord(c))[2:]
479
480
481def tag2identifier(tag):
482	"""Convert a table tag to a valid (but UGLY) python identifier,
483	as well as a filename that's guaranteed to be unique even on a
484	caseless file system. Each character is mapped to two characters.
485	Lowercase letters get an underscore before the letter, uppercase
486	letters get an underscore after the letter. Trailing spaces are
487	trimmed. Illegal characters are escaped as two hex bytes. If the
488	result starts with a number (as the result of a hex escape), an
489	extra underscore is prepended. Examples:
490		'glyf' -> '_g_l_y_f'
491		'cvt ' -> '_c_v_t'
492		'OS/2' -> 'O_S_2f_2'
493	"""
494	import re
495	assert len(tag) == 4, "tag should be 4 characters long"
496	while len(tag) > 1 and tag[-1] == ' ':
497		tag = tag[:-1]
498	ident = ""
499	for c in tag:
500		ident = ident + _escapechar(c)
501	if re.match("[0-9]", ident):
502		ident = "_" + ident
503	return ident
504
505
506def identifier2tag(ident):
507	"""the opposite of tag2identifier()"""
508	import string
509	if len(ident) % 2 and ident[0] == "_":
510		ident = ident[1:]
511	assert not (len(ident) % 2)
512	tag = ""
513	for i in range(0, len(ident), 2):
514		if ident[i] == "_":
515			tag = tag + ident[i+1]
516		elif ident[i+1] == "_":
517			tag = tag + ident[i]
518		else:
519			# assume hex
520			tag = tag + chr(string.atoi(ident[i:i+2], 16))
521	# append trailing spaces
522	tag = tag + (4 - len(tag)) * ' '
523	return tag
524
525
526def tag2xmltag(tag):
527	"""Similarly to tag2identifier(), this converts a TT tag
528	to a valid XML element name. Since XML element names are
529	case sensitive, this is a fairly simple/readable translation.
530	"""
531	import string, re
532	if tag == "OS/2":
533		return "OS_2"
534	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
535		return string.strip(tag)
536	else:
537		return tag2identifier(tag)
538
539
540def xmltag2tag(tag):
541	"""The opposite of tag2xmltag()"""
542	if tag == "OS_2":
543		return "OS/2"
544	if len(tag) == 8:
545		return identifier2tag(tag)
546	else:
547		return tag + " " * (4 - len(tag))
548	return tag
549
550
551def debugmsg(msg):
552	import time
553	print msg + time.strftime("  (%H:%M:%S)", time.localtime(time.time()))
554
555
556