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