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