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