__init__.py revision 9f1e14bec9dad6d59300bbff6a5a9d5a8d0828f9
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.22 2002-05-05 11:29:33 jvr Exp $
46#
47
48import os
49import string
50import types
51
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 TTX 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		from fontTools.ttLib import sfnt
141		if type(file) == types.StringType:
142			closeStream = 1
143			if os.name == "mac" and makeSuitcase:
144				import macUtils
145				file = macUtils.SFNTResourceWriter(file, self)
146			else:
147				file = open(file, "wb")
148				if os.name == "mac":
149					import macfs
150					fss = macfs.FSSpec(file.name)
151					fss.SetCreatorType('mdos', 'BINA')
152		else:
153			# assume "file" is a writable file object
154			closeStream = 0
155
156		tags = self.keys()
157		numTables = len(tags)
158		writer = sfnt.SFNTWriter(file, numTables, self.sfntVersion)
159
160		done = []
161		for tag in tags:
162			self._writeTable(tag, writer, done)
163
164		writer.close(closeStream)
165
166	def saveXML(self, fileOrPath, progress=None,
167			tables=None, skipTables=None, splitTables=0, disassembleInstructions=1):
168		"""Export the font as TTX (an XML-based text file), or as a series of text
169		files when splitTables is true. In the latter case, the 'fileOrPath'
170		argument should be a path to a directory.
171		The 'tables' argument must either be false (dump all tables) or a
172		list of tables to dump. The 'skipTables' argument may be a list of tables
173		to skip, but only when the 'tables' argument is false.
174		"""
175		from fontTools import version
176		import xmlWriter
177
178		self.disassembleInstructions = disassembleInstructions
179		if not tables:
180			tables = self.keys()
181			if skipTables:
182				for tag in skipTables:
183					if tag in tables:
184						tables.remove(tag)
185		numTables = len(tables)
186		numGlyphs = self['maxp'].numGlyphs
187		if progress:
188			progress.set(0, numTables * numGlyphs)
189		if not splitTables:
190			writer = xmlWriter.XMLWriter(fileOrPath)
191			writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1],
192					ttLibVersion=version)
193			writer.newline()
194			writer.newline()
195		else:
196			# 'fileOrPath' must now be a path (pointing to a directory)
197			if not os.path.exists(fileOrPath):
198				os.mkdir(fileOrPath)
199			fileNameTemplate = os.path.join(fileOrPath,
200					os.path.basename(fileOrPath)) + ".%s.ttx"
201
202		for i in range(numTables):
203			tag = tables[i]
204			if splitTables:
205				writer = xmlWriter.XMLWriter(fileNameTemplate % tag2identifier(tag))
206				writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1],
207						ttLibVersion=version)
208				writer.newline()
209				writer.newline()
210			table = self[tag]
211			report = "Dumping '%s' table..." % tag
212			if progress:
213				progress.setlabel(report)
214			elif self.verbose:
215				debugmsg(report)
216			else:
217				print report
218			xmltag = tag2xmltag(tag)
219			if hasattr(table, "ERROR"):
220				writer.begintag(xmltag, ERROR="decompilation error")
221			else:
222				writer.begintag(xmltag)
223			writer.newline()
224			if tag == "glyf":
225				table.toXML(writer, self, progress)
226			elif tag == "CFF ":
227				table.toXML(writer, self, progress)
228			else:
229				table.toXML(writer, self)
230			writer.endtag(xmltag)
231			writer.newline()
232			writer.newline()
233			if splitTables:
234				writer.endtag("ttFont")
235				writer.newline()
236				writer.close()
237			if progress:
238				progress.set(i * numGlyphs, numTables * numGlyphs)
239		if not splitTables:
240			writer.endtag("ttFont")
241			writer.newline()
242			writer.close()
243		if self.verbose:
244			debugmsg("Done dumping TTX")
245
246	def importXML(self, file, progress=None):
247		"""Import a TTX file (an XML-based text format), so as to recreate
248		a font object.
249		"""
250		import xmlImport
251		xmlImport.importXML(self, file, progress)
252
253	def isLoaded(self, tag):
254		"""Return true if the table identified by 'tag' has been
255		decompiled and loaded into memory."""
256		return self.tables.has_key(tag)
257
258	def has_key(self, tag):
259		if self.isLoaded(tag):
260			return 1
261		elif self.reader and self.reader.has_key(tag):
262			return 1
263		else:
264			return 0
265
266	def keys(self):
267		keys = self.tables.keys()
268		if self.reader:
269			for key in self.reader.keys():
270				if key not in keys:
271					keys.append(key)
272		keys.sort()
273		return keys
274
275	def __len__(self):
276		return len(self.keys())
277
278	def __getitem__(self, tag):
279		try:
280			return self.tables[tag]
281		except KeyError:
282			if self.reader is not None:
283				import traceback
284				if self.verbose:
285					debugmsg("reading '%s' table from disk" % tag)
286				data = self.reader[tag]
287				tableclass = getTableClass(tag)
288				table = tableclass(tag)
289				self.tables[tag] = table
290				if self.verbose:
291					debugmsg("decompiling '%s' table" % tag)
292				try:
293					table.decompile(data, self)
294				except "_ _ F O O _ _": # dummy exception to disable exception catching
295					print "An exception occurred during the decompilation of the '%s' table" % tag
296					from tables.DefaultTable import DefaultTable
297					import StringIO
298					file = StringIO.StringIO()
299					traceback.print_exc(file=file)
300					table = DefaultTable(tag)
301					table.ERROR = file.getvalue()
302					self.tables[tag] = table
303					table.decompile(data, self)
304				return table
305			else:
306				raise KeyError, "'%s' table not found" % tag
307
308	def __setitem__(self, tag, table):
309		self.tables[tag] = table
310
311	def __delitem__(self, tag):
312		if not self.has_key(tag):
313			raise KeyError, "'%s' table not found" % tag
314		if self.tables.has_key(tag):
315			del self.tables[tag]
316		if self.reader and self.reader.has_key(tag):
317			del self.reader[tag]
318
319	def setGlyphOrder(self, glyphOrder):
320		self.glyphOrder = glyphOrder
321		if self.has_key('CFF '):
322			self['CFF '].setGlyphOrder(glyphOrder)
323		if self.has_key('glyf'):
324			self['glyf'].setGlyphOrder(glyphOrder)
325
326	def getGlyphOrder(self):
327		try:
328			return self.glyphOrder
329		except AttributeError:
330			pass
331		if self.has_key('CFF '):
332			# CFF OpenType font
333			self.glyphOrder = self['CFF '].getGlyphOrder()
334		elif self.has_key('post'):
335			# TrueType font
336			glyphOrder = self['post'].getGlyphOrder()
337			if glyphOrder is None:
338				#
339				# No names found in the 'post' table.
340				# Try to create glyph names from the unicode cmap (if available)
341				# in combination with the Adobe Glyph List (AGL).
342				#
343				self._getGlyphNamesFromCmap()
344			else:
345				self.glyphOrder = glyphOrder
346		else:
347			self._getGlyphNamesFromCmap()
348		return self.glyphOrder
349
350	def _getGlyphNamesFromCmap(self):
351		#
352		# This is rather convoluted, but then again, it's an interesting problem:
353		# - we need to use the unicode values found in the cmap table to
354		#   build glyph names (eg. because there is only a minimal post table,
355		#   or none at all).
356		# - but the cmap parser also needs glyph names to work with...
357		# So here's what we do:
358		# - make up glyph names based on glyphID
359		# - load a temporary cmap table based on those names
360		# - extract the unicode values, build the "real" glyph names
361		# - unload the temporary cmap table
362		#
363		if self.isLoaded("cmap"):
364			# Bootstrapping: we're getting called by the cmap parser
365			# itself. This means self.tables['cmap'] contains a partially
366			# loaded cmap, making it impossible to get at a unicode
367			# subtable here. We remove the partially loaded cmap and
368			# restore it later.
369			# This only happens if the cmap table is loaded before any
370			# other table that does f.getGlyphOrder()  or f.getGlyphName().
371			cmapLoading = self.tables['cmap']
372			del self.tables['cmap']
373		else:
374			cmapLoading = None
375		# Make up glyph names based on glyphID, which will be used by the
376		# temporary cmap and by the real cmap in case we don't find a unicode
377		# cmap.
378		numGlyphs = int(self['maxp'].numGlyphs)
379		glyphOrder = [None] * numGlyphs
380		glyphOrder[0] = ".notdef"
381		for i in range(1, numGlyphs):
382			glyphOrder[i] = "glyph%.5d" % i
383		# Set the glyph order, so the cmap parser has something
384		# to work with (so we don't get called recursively).
385		self.glyphOrder = glyphOrder
386		# Get a (new) temporary cmap (based on the just invented names)
387		tempcmap = self['cmap'].getcmap(3, 1)
388		if tempcmap is not None:
389			# we have a unicode cmap
390			from fontTools import agl
391			cmap = tempcmap.cmap
392			# create a reverse cmap dict
393			reversecmap = {}
394			for unicode, name in cmap.items():
395				reversecmap[name] = unicode
396			allNames = {}
397			for i in range(numGlyphs):
398				tempName = glyphOrder[i]
399				if reversecmap.has_key(tempName):
400					unicode = reversecmap[tempName]
401					if agl.UV2AGL.has_key(unicode):
402						# get name from the Adobe Glyph List
403						glyphName = agl.UV2AGL[unicode]
404					else:
405						# create uni<CODE> name
406						glyphName = "uni" + string.upper(string.zfill(
407								hex(unicode)[2:], 4))
408					tempName = glyphName
409					n = 1
410					while allNames.has_key(tempName):
411						tempName = glyphName + "#" + `n`
412						n = n + 1
413					glyphOrder[i] = tempName
414					allNames[tempName] = 1
415			# Delete the temporary cmap table from the cache, so it can
416			# be parsed again with the right names.
417			del self.tables['cmap']
418		else:
419			pass # no unicode cmap available, stick with the invented names
420		self.glyphOrder = glyphOrder
421		if cmapLoading:
422			# restore partially loaded cmap, so it can continue loading
423			# using the proper names.
424			self.tables['cmap'] = cmapLoading
425
426	def getGlyphNames(self):
427		"""Get a list of glyph names, sorted alphabetically."""
428		glyphNames = self.getGlyphOrder()[:]
429		glyphNames.sort()
430		return glyphNames
431
432	def getGlyphNames2(self):
433		"""Get a list of glyph names, sorted alphabetically,
434		but not case sensitive.
435		"""
436		from fontTools.misc import textTools
437		return textTools.caselessSort(self.getGlyphOrder())
438
439	def getGlyphName(self, glyphID):
440		return self.getGlyphOrder()[glyphID]
441
442	def getGlyphID(self, glyphName):
443		if not hasattr(self, "_reverseGlyphOrderDict"):
444			self._buildReverseGlyphOrderDict()
445		glyphOrder = self.getGlyphOrder()
446		d = self._reverseGlyphOrderDict
447		if not d.has_key(glyphName):
448			if glyphName in glyphOrder:
449				self._buildReverseGlyphOrderDict()
450				return self.getGlyphID(glyphName)
451			else:
452				raise KeyError, glyphName
453		glyphID = d[glyphName]
454		if glyphName <> glyphOrder[glyphID]:
455			self._buildReverseGlyphOrderDict()
456			return self.getGlyphID(glyphName)
457		return glyphID
458
459	def _buildReverseGlyphOrderDict(self):
460		self._reverseGlyphOrderDict = d = {}
461		glyphOrder = self.getGlyphOrder()
462		for glyphID in range(len(glyphOrder)):
463			d[glyphOrder[glyphID]] = glyphID
464
465	def _writeTable(self, tag, writer, done):
466		"""Internal helper function for self.save(). Keeps track of
467		inter-table dependencies.
468		"""
469		if tag in done:
470			return
471		tableclass = getTableClass(tag)
472		for masterTable in tableclass.dependencies:
473			if masterTable not in done:
474				if self.has_key(masterTable):
475					self._writeTable(masterTable, writer, done)
476				else:
477					done.append(masterTable)
478		tabledata = self.getTableData(tag)
479		if self.verbose:
480			debugmsg("writing '%s' table to disk" % tag)
481		writer[tag] = tabledata
482		done.append(tag)
483
484	def getTableData(self, tag):
485		"""Returns raw table data, whether compiled or directly read from disk.
486		"""
487		if self.isLoaded(tag):
488			if self.verbose:
489				debugmsg("compiling '%s' table" % tag)
490			return self.tables[tag].compile(self)
491		elif self.reader and self.reader.has_key(tag):
492			if self.verbose:
493				debugmsg("reading '%s' table from disk" % tag)
494			return self.reader[tag]
495		else:
496			raise KeyError, tag
497
498
499def _test_endianness():
500	"""Test the endianness of the machine. This is crucial to know
501	since TrueType data is always big endian, even on little endian
502	machines. There are quite a few situations where we explicitly
503	need to swap some bytes.
504	"""
505	import struct
506	data = struct.pack("h", 0x01)
507	if data == "\000\001":
508		return "big"
509	elif data == "\001\000":
510		return "little"
511	else:
512		assert 0, "endian confusion!"
513
514endian = _test_endianness()
515
516
517def getTableModule(tag):
518	"""Fetch the packer/unpacker module for a table.
519	Return None when no module is found.
520	"""
521	import imp
522	import tables
523	py_tag = tag2identifier(tag)
524	try:
525		f, path, kind = imp.find_module(py_tag, tables.__path__)
526		if f:
527			f.close()
528	except ImportError:
529		return None
530	else:
531		module = __import__("fontTools.ttLib.tables." + py_tag)
532		return getattr(tables, py_tag)
533
534
535def getTableClass(tag):
536	"""Fetch the packer/unpacker class for a table.
537	Return None when no class is found.
538	"""
539	module = getTableModule(tag)
540	if module is None:
541		from tables.DefaultTable import DefaultTable
542		return DefaultTable
543	py_tag = tag2identifier(tag)
544	tableclass = getattr(module, "table_" + py_tag)
545	return tableclass
546
547
548def getNewTable(tag):
549	"""Return a new instance of a table."""
550	tableclass = getTableClass(tag)
551	return tableclass(tag)
552
553
554def _escapechar(c):
555	"""Helper function for tag2identifier()"""
556	import re
557	if re.match("[a-z0-9]", c):
558		return "_" + c
559	elif re.match("[A-Z]", c):
560		return c + "_"
561	else:
562		return hex(ord(c))[2:]
563
564
565def tag2identifier(tag):
566	"""Convert a table tag to a valid (but UGLY) python identifier,
567	as well as a filename that's guaranteed to be unique even on a
568	caseless file system. Each character is mapped to two characters.
569	Lowercase letters get an underscore before the letter, uppercase
570	letters get an underscore after the letter. Trailing spaces are
571	trimmed. Illegal characters are escaped as two hex bytes. If the
572	result starts with a number (as the result of a hex escape), an
573	extra underscore is prepended. Examples:
574		'glyf' -> '_g_l_y_f'
575		'cvt ' -> '_c_v_t'
576		'OS/2' -> 'O_S_2f_2'
577	"""
578	import re
579	assert len(tag) == 4, "tag should be 4 characters long"
580	while len(tag) > 1 and tag[-1] == ' ':
581		tag = tag[:-1]
582	ident = ""
583	for c in tag:
584		ident = ident + _escapechar(c)
585	if re.match("[0-9]", ident):
586		ident = "_" + ident
587	return ident
588
589
590def identifier2tag(ident):
591	"""the opposite of tag2identifier()"""
592	if len(ident) % 2 and ident[0] == "_":
593		ident = ident[1:]
594	assert not (len(ident) % 2)
595	tag = ""
596	for i in range(0, len(ident), 2):
597		if ident[i] == "_":
598			tag = tag + ident[i+1]
599		elif ident[i+1] == "_":
600			tag = tag + ident[i]
601		else:
602			# assume hex
603			tag = tag + chr(string.atoi(ident[i:i+2], 16))
604	# append trailing spaces
605	tag = tag + (4 - len(tag)) * ' '
606	return tag
607
608
609def tag2xmltag(tag):
610	"""Similarly to tag2identifier(), this converts a TT tag
611	to a valid XML element name. Since XML element names are
612	case sensitive, this is a fairly simple/readable translation.
613	"""
614	import re
615	if tag == "OS/2":
616		return "OS_2"
617	if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
618		return string.strip(tag)
619	else:
620		return tag2identifier(tag)
621
622
623def xmltag2tag(tag):
624	"""The opposite of tag2xmltag()"""
625	if tag == "OS_2":
626		return "OS/2"
627	if len(tag) == 8:
628		return identifier2tag(tag)
629	else:
630		return tag + " " * (4 - len(tag))
631	return tag
632
633
634def debugmsg(msg):
635	import time
636	print msg + time.strftime("  (%H:%M:%S)", time.localtime(time.time()))
637
638
639