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