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