__init__.py revision 8bc8cf850df75c660e15d34fac8ac70f18a4da4a
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__ = "$Id: __init__.py,v 1.3 1999-12-17 12:54:19 Just Exp $"
45__release__ = "1.0a6"
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		import xmlWriter
152		writer = xmlWriter.XMLWriter(file)
153		writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1],
154				ttLibVersion=__release__)
155		writer.newline()
156		writer.newline()
157		if not tables:
158			tables = self.keys()
159		numTables = len(tables)
160		numGlyphs = self['maxp'].numGlyphs
161		if progress:
162			progress.set(0, numTables * numGlyphs)
163		for i in range(numTables):
164			tag = tables[i]
165			table = self[tag]
166			report = "Dumping '%s' table..." % tag
167			if progress:
168				progress.setlabel(report)
169			elif self.verbose:
170				debugmsg(report)
171			else:
172				print report
173			xmltag = tag2xmltag(tag)
174			writer.begintag(xmltag)
175			writer.newline()
176			if tag == "glyf":
177				table.toXML(writer, self, progress)
178			elif tag == "CFF ":
179				table.toXML(writer, self, progress)
180			else:
181				table.toXML(writer, self)
182			writer.endtag(xmltag)
183			writer.newline()
184			writer.newline()
185			if progress:
186				progress.set(i * numGlyphs, numTables * numGlyphs)
187		writer.endtag("ttFont")
188		writer.newline()
189		writer.close()
190		if self.verbose:
191			debugmsg("Done dumping XML")
192
193	def importXML(self, file, progress=None):
194		"""Import an XML-based text file, so as to recreate
195		a font object.
196		"""
197		if self.tables:
198			raise error, "Can't import XML into existing font."
199		import xmlImport
200		from xml.parsers.xmlproc import xmlproc
201		builder = xmlImport.XMLApplication(self, progress)
202		if progress:
203			progress.set(0, os.stat(file)[stat.ST_SIZE] / 100 or 1)
204		proc = xmlImport.UnicodeProcessor()
205		proc.set_application(builder)
206		proc.set_error_handler(xmlImport.XMLErrorHandler(proc))
207		dir, filename = os.path.split(file)
208		if dir:
209			olddir = os.getcwd()
210			os.chdir(dir)
211		try:
212			proc.parse_resource(filename)
213			root = builder.root
214		finally:
215			if dir:
216				os.chdir(olddir)
217			# remove circular references
218			proc.deref()
219			del builder.progress
220
221	def isLoaded(self, tag):
222		"""Return true if the table identified by 'tag' has been
223		decompiled and loaded into memory."""
224		return self.tables.has_key(tag)
225
226	def has_key(self, tag):
227		"""Pretend we're a dictionary."""
228		if self.isLoaded(tag):
229			return 1
230		elif self.reader and self.reader.has_key(tag):
231			return 1
232		else:
233			return 0
234
235	def keys(self):
236		"""Pretend we're a dictionary."""
237		keys = self.tables.keys()
238		if self.reader:
239			for key in self.reader.keys():
240				if key not in keys:
241					keys.append(key)
242		keys.sort()
243		return keys
244
245	def __len__(self):
246		"""Pretend we're a dictionary."""
247		return len(self.keys())
248
249	def __getitem__(self, tag):
250		"""Pretend we're a dictionary."""
251		try:
252			return self.tables[tag]
253		except KeyError:
254			if self.reader is not None:
255				if self.verbose:
256					debugmsg("reading '%s' table from disk" % tag)
257				data = self.reader[tag]
258				tableclass = getTableClass(tag)
259				table = tableclass(tag)
260				self.tables[tag] = table
261				if self.verbose:
262					debugmsg("decompiling '%s' table" % tag)
263				table.decompile(data, self)
264				return table
265			else:
266				raise KeyError, "'%s' table not found" % tag
267
268	def __setitem__(self, tag, table):
269		"""Pretend we're a dictionary."""
270		self.tables[tag] = table
271
272	def __delitem__(self, tag):
273		"""Pretend we're a dictionary."""
274		del self.tables[tag]
275
276	def setGlyphOrder(self, glyphOrder):
277		self.glyphOrder = glyphOrder
278		if self.has_key('CFF '):
279			self['CFF '].setGlyphOrder(glyphOrder)
280		if self.has_key('glyf'):
281			self['glyf'].setGlyphOrder(glyphOrder)
282
283	def getGlyphOrder(self):
284		if not hasattr(self, "glyphOrder"):
285			if self.has_key('CFF '):
286				# CFF OpenType font
287				self.glyphOrder = self['CFF '].getGlyphOrder()
288			else:
289				# TrueType font
290				glyphOrder = self['post'].getGlyphOrder()
291				if glyphOrder is None:
292					#
293					# No names found in the 'post' table.
294					# Try to create glyph names from the unicode cmap (if available)
295					# in combination with the Adobe Glyph List (AGL).
296					#
297					self._getGlyphNamesFromCmap()
298				else:
299					self.glyphOrder = glyphOrder
300			# XXX what if a font contains 'glyf'/'post' table *and* CFF?
301		return self.glyphOrder
302
303	def _getGlyphNamesFromCmap(self):
304		# Make up glyph names based on glyphID, which will be used
305		# in case we don't find a unicode cmap.
306		numGlyphs = int(self['maxp'].numGlyphs)
307		glyphOrder = [None] * numGlyphs
308		glyphOrder[0] = ".notdef"
309		for i in range(1, numGlyphs):
310			glyphOrder[i] = "glyph%.5d" % i
311		# Set the glyph order, so the cmap parser has something
312		# to work with
313		self.glyphOrder = glyphOrder
314		# Get the temporary cmap (based on the just invented names)
315		tempcmap = self['cmap'].getcmap(3, 1)
316		if tempcmap is not None:
317			# we have a unicode cmap
318			import agl, string
319			cmap = tempcmap.cmap
320			# create a reverse cmap dict
321			reversecmap = {}
322			for unicode, name in cmap.items():
323				reversecmap[name] = unicode
324			assert len(reversecmap) == len(cmap)
325			for i in range(numGlyphs):
326				tempName = glyphOrder[i]
327				if reversecmap.has_key(tempName):
328					unicode = reversecmap[tempName]
329					if agl.UV2AGL.has_key(unicode):
330						# get name from the Adobe Glyph List
331						glyphOrder[i] = agl.UV2AGL[unicode]
332					else:
333						# create uni<CODE> name
334						glyphOrder[i] = "uni" + string.upper(string.zfill(hex(unicode)[2:], 4))
335			# Delete the cmap table from the cache, so it can be
336			# parsed again with the right names.
337			del self.tables['cmap']
338		else:
339			pass # no unicode cmap available, stick with the invented names
340		self.glyphOrder = glyphOrder
341
342	def getGlyphNames(self):
343		"""Get a list of glyph names, sorted alphabetically."""
344		glyphNames = self.getGlyphOrder()[:]
345		glyphNames.sort()
346		return glyphNames
347
348	def getGlyphNames2(self):
349		"""Get a list of glyph names, sorted alphabetically,
350		but not case sensitive.
351		"""
352		from fontTools.misc import textTools
353		return textTools.caselessSort(self.getGlyphOrder())
354
355	def getGlyphName(self, glyphID):
356		return self.getGlyphOrder()[glyphID]
357
358	def getGlyphID(self, glyphName):
359		if not hasattr(self, "_reverseGlyphOrderDict"):
360			self._buildReverseGlyphOrderDict()
361		glyphOrder = self.getGlyphOrder()
362		d = self._reverseGlyphOrderDict
363		if not d.has_key(glyphName):
364			if glyphName in glyphOrder:
365				self._buildReverseGlyphOrderDict()
366				return self.getGlyphID(glyphName)
367			else:
368				raise KeyError, glyphName
369		glyphID = d[glyphName]
370		if glyphName <> glyphOrder[glyphID]:
371			self._buildReverseGlyphOrderDict()
372			return self.getGlyphID(glyphName)
373		return glyphID
374
375	def _buildReverseGlyphOrderDict(self):
376		self._reverseGlyphOrderDict = d = {}
377		glyphOrder = self.getGlyphOrder()
378		for glyphID in range(len(glyphOrder)):
379			d[glyphOrder[glyphID]] = glyphID
380
381	def _writeTable(self, tag, writer, done):
382		"""Internal helper function for self.save(). Keeps track of
383		inter-table dependencies.
384		"""
385		if tag in done:
386			return
387		tableclass = getTableClass(tag)
388		for masterTable in tableclass.dependencies:
389			if masterTable not in done:
390				if self.has_key(masterTable):
391					self._writeTable(masterTable, writer, done)
392				else:
393					done.append(masterTable)
394		tabledata = self._getTableData(tag)
395		if self.verbose:
396			debugmsg("writing '%s' table to disk" % tag)
397		writer[tag] = tabledata
398		done.append(tag)
399
400	def _getTableData(self, tag):
401		"""Internal helper function. Returns raw table data,
402		whether compiled or directly read from disk.
403		"""
404		if self.isLoaded(tag):
405			if self.verbose:
406				debugmsg("compiling '%s' table" % tag)
407			return self.tables[tag].compile(self)
408		elif self.reader and self.reader.has_key(tag):
409			if self.verbose:
410				debugmsg("reading '%s' table from disk" % tag)
411			return self.reader[tag]
412		else:
413			raise KeyError, tag
414
415
416def _test_endianness():
417	"""Test the endianness of the machine. This is crucial to know
418	since TrueType data is always big endian, even on little endian
419	machines. There are quite a few situations where we explicitly
420	need to swap some bytes.
421	"""
422	import struct
423	data = struct.pack("h", 0x01)
424	if data == "\000\001":
425		return "big"
426	elif data == "\001\000":
427		return "little"
428	else:
429		assert 0, "endian confusion!"
430
431endian = _test_endianness()
432
433
434def getTableModule(tag):
435	"""Fetch the packer/unpacker module for a table.
436	Return None when no module is found.
437	"""
438	import imp
439	import tables
440	py_tag = tag2identifier(tag)
441	try:
442		f, path, kind = imp.find_module(py_tag, tables.__path__)
443		if f:
444			f.close()
445	except ImportError:
446		return None
447	else:
448		module = __import__("fontTools.ttLib.tables." + py_tag)
449		return getattr(tables, py_tag)
450
451
452def getTableClass(tag):
453	"""Fetch the packer/unpacker class for a table.
454	Return None when no class is found.
455	"""
456	module = getTableModule(tag)
457	if module is None:
458		from tables.DefaultTable import DefaultTable
459		return DefaultTable
460	py_tag = tag2identifier(tag)
461	tableclass = getattr(module, "table_" + py_tag)
462	return tableclass
463
464
465def newtable(tag):
466	"""Return a new instance of a table."""
467	tableclass = getTableClass(tag)
468	return tableclass(tag)
469
470
471def _escapechar(c):
472	"""Helper function for tag2identifier()"""
473	import re
474	if re.match("[a-z0-9]", c):
475		return "_" + c
476	elif re.match("[A-Z]", c):
477		return c + "_"
478	else:
479		return hex(ord(c))[2:]
480
481
482def tag2identifier(tag):
483	"""Convert a table tag to a valid (but UGLY) python identifier,
484	as well as a filename that's guaranteed to be unique even on a
485	caseless file system. Each character is mapped to two characters.
486	Lowercase letters get an underscore before the letter, uppercase
487	letters get an underscore after the letter. Trailing spaces are
488	trimmed. Illegal characters are escaped as two hex bytes. If the
489	result starts with a number (as the result of a hex escape), an
490	extra underscore is prepended. Examples:
491		'glyf' -> '_g_l_y_f'
492		'cvt ' -> '_c_v_t'
493		'OS/2' -> 'O_S_2f_2'
494	"""
495	import re
496	assert len(tag) == 4, "tag should be 4 characters long"
497	while len(tag) > 1 and tag[-1] == ' ':
498		tag = tag[:-1]
499	ident = ""
500	for c in tag:
501		ident = ident + _escapechar(c)
502	if re.match("[0-9]", ident):
503		ident = "_" + ident
504	return ident
505
506
507def identifier2tag(ident):
508	"""the opposite of tag2identifier()"""
509	import string
510	if len(ident) % 2 and ident[0] == "_":
511		ident = ident[1:]
512	assert not (len(ident) % 2)
513	tag = ""
514	for i in range(0, len(ident), 2):
515		if ident[i] == "_":
516			tag = tag + ident[i+1]
517		elif ident[i+1] == "_":
518			tag = tag + ident[i]
519		else:
520			# assume hex
521			tag = tag + chr(string.atoi(ident[i:i+2], 16))
522	# append trailing spaces
523	tag = tag + (4 - len(tag)) * ' '
524	return tag
525
526
527def tag2xmltag(tag):
528	"""Similarly to tag2identifier(), this converts a TT tag
529	to a valid XML element name. Since XML element names are
530	case sensitive, this is a fairly simple/readable translation.
531	"""
532	import string, re
533	if tag == "OS/2":
534		return "OS_2"
535	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
536		return string.strip(tag)
537	else:
538		return tag2identifier(tag)
539
540
541def xmltag2tag(tag):
542	"""The opposite of tag2xmltag()"""
543	if tag == "OS_2":
544		return "OS/2"
545	if len(tag) == 8:
546		return identifier2tag(tag)
547	else:
548		return tag + " " * (4 - len(tag))
549	return tag
550
551
552def debugmsg(msg):
553	import time
554	print msg + time.strftime("  (%H:%M:%S)", time.localtime(time.time()))
555
556
557