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