• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2"""
3glifLib.py -- Generic module for reading and writing the .glif format.
4
5More info about the .glif format (GLyphInterchangeFormat) can be found here:
6
7	http://unifiedfontobject.org
8
9The main class in this module is GlyphSet. It manages a set of .glif files
10in a folder. It offers two ways to read glyph data, and one way to write
11glyph data. See the class doc string for details.
12"""
13
14from __future__ import absolute_import, unicode_literals
15from warnings import warn
16from collections import OrderedDict
17import fs
18import fs.base
19import fs.errors
20import fs.osfs
21import fs.path
22from fontTools.misc.py23 import basestring, unicode, tobytes, tounicode
23from fontTools.misc import plistlib
24from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen
25from fontTools.ufoLib.errors import GlifLibError
26from fontTools.ufoLib.filenames import userNameToFileName
27from fontTools.ufoLib.validators import (
28	genericTypeValidator,
29	colorValidator,
30	guidelinesValidator,
31	anchorsValidator,
32	identifierValidator,
33	imageValidator,
34	glyphLibValidator,
35)
36from fontTools.misc import etree
37from fontTools.ufoLib import _UFOBaseIO
38from fontTools.ufoLib.utils import integerTypes, numberTypes
39
40
41__all__ = [
42	"GlyphSet",
43	"GlifLibError",
44	"readGlyphFromString", "writeGlyphToString",
45	"glyphNameToFileName"
46]
47
48
49# ---------
50# Constants
51# ---------
52
53CONTENTS_FILENAME = "contents.plist"
54LAYERINFO_FILENAME = "layerinfo.plist"
55supportedUFOFormatVersions = [1, 2, 3]
56supportedGLIFFormatVersions = [1, 2]
57
58
59# ------------
60# Simple Glyph
61# ------------
62
63class Glyph(object):
64
65	"""
66	Minimal glyph object. It has no glyph attributes until either
67	the draw() or the drawPoints() method has been called.
68	"""
69
70	def __init__(self, glyphName, glyphSet):
71		self.glyphName = glyphName
72		self.glyphSet = glyphSet
73
74	def draw(self, pen):
75		"""
76		Draw this glyph onto a *FontTools* Pen.
77		"""
78		pointPen = PointToSegmentPen(pen)
79		self.drawPoints(pointPen)
80
81	def drawPoints(self, pointPen):
82		"""
83		Draw this glyph onto a PointPen.
84		"""
85		self.glyphSet.readGlyph(self.glyphName, self, pointPen)
86
87
88# ---------
89# Glyph Set
90# ---------
91
92class GlyphSet(_UFOBaseIO):
93
94	"""
95	GlyphSet manages a set of .glif files inside one directory.
96
97	GlyphSet's constructor takes a path to an existing directory as it's
98	first argument. Reading glyph data can either be done through the
99	readGlyph() method, or by using GlyphSet's dictionary interface, where
100	the keys are glyph names and the values are (very) simple glyph objects.
101
102	To write a glyph to the glyph set, you use the writeGlyph() method.
103	The simple glyph objects returned through the dict interface do not
104	support writing, they are just a convenient way to get at the glyph data.
105	"""
106
107	glyphClass = Glyph
108
109	def __init__(
110		self,
111		path,
112		glyphNameToFileNameFunc=None,
113		ufoFormatVersion=3,
114		validateRead=True,
115		validateWrite=True,
116	):
117		"""
118		'path' should be a path (string) to an existing local directory, or
119		an instance of fs.base.FS class.
120
121		The optional 'glyphNameToFileNameFunc' argument must be a callback
122		function that takes two arguments: a glyph name and a list of all
123		existing filenames (if any exist). It should return a file name
124		(including the .glif extension). The glyphNameToFileName function
125		is called whenever a file name is created for a given glyph name.
126
127		``validateRead`` will validate read operations. Its default is ``True``.
128		``validateWrite`` will validate write operations. Its default is ``True``.
129		"""
130		if ufoFormatVersion not in supportedUFOFormatVersions:
131			raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
132		if isinstance(path, basestring):
133			try:
134				filesystem = fs.osfs.OSFS(path)
135			except fs.errors.CreateFailed:
136				raise GlifLibError("No glyphs directory '%s'" % path)
137			self._shouldClose = True
138		elif isinstance(path, fs.base.FS):
139			filesystem = path
140			try:
141				filesystem.check()
142			except fs.errors.FilesystemClosed:
143				raise GlifLibError("the filesystem '%s' is closed" % filesystem)
144			self._shouldClose = False
145		else:
146			raise TypeError(
147				"Expected a path string or fs object, found %s"
148				% type(path).__name__
149			)
150		try:
151			path = filesystem.getsyspath("/")
152		except fs.errors.NoSysPath:
153			# network or in-memory FS may not map to the local one
154			path = unicode(filesystem)
155		# 'dirName' is kept for backward compatibility only, but it's DEPRECATED
156		# as it's not guaranteed that it maps to an existing OSFS directory.
157		# Client could use the FS api via the `self.fs` attribute instead.
158		self.dirName = fs.path.parts(path)[-1]
159		self.fs = filesystem
160		# if glyphSet contains no 'contents.plist', we consider it empty
161		self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME)
162		self.ufoFormatVersion = ufoFormatVersion
163		if glyphNameToFileNameFunc is None:
164			glyphNameToFileNameFunc = glyphNameToFileName
165		self.glyphNameToFileName = glyphNameToFileNameFunc
166		self._validateRead = validateRead
167		self._validateWrite = validateWrite
168		self._existingFileNames = None
169		self._reverseContents = None
170
171		self.rebuildContents()
172
173	def rebuildContents(self, validateRead=None):
174		"""
175		Rebuild the contents dict by loading contents.plist.
176
177		``validateRead`` will validate the data, by default it is set to the
178		class's ``validateRead`` value, can be overridden.
179		"""
180		if validateRead is None:
181			validateRead = self._validateRead
182		contents = self._getPlist(CONTENTS_FILENAME, {})
183		# validate the contents
184		if validateRead:
185			invalidFormat = False
186			if not isinstance(contents, dict):
187				invalidFormat = True
188			else:
189				for name, fileName in contents.items():
190					if not isinstance(name, basestring):
191						invalidFormat = True
192					if not isinstance(fileName, basestring):
193						invalidFormat = True
194					elif not self.fs.exists(fileName):
195						raise GlifLibError(
196							"%s references a file that does not exist: %s"
197							% (CONTENTS_FILENAME, fileName)
198						)
199			if invalidFormat:
200				raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME)
201		self.contents = contents
202		self._existingFileNames = None
203		self._reverseContents = None
204
205	def getReverseContents(self):
206		"""
207		Return a reversed dict of self.contents, mapping file names to
208		glyph names. This is primarily an aid for custom glyph name to file
209		name schemes that want to make sure they don't generate duplicate
210		file names. The file names are converted to lowercase so we can
211		reliably check for duplicates that only differ in case, which is
212		important for case-insensitive file systems.
213		"""
214		if self._reverseContents is None:
215			d = {}
216			for k, v in self.contents.items():
217				d[v.lower()] = k
218			self._reverseContents = d
219		return self._reverseContents
220
221	def writeContents(self):
222		"""
223		Write the contents.plist file out to disk. Call this method when
224		you're done writing glyphs.
225		"""
226		self._writePlist(CONTENTS_FILENAME, self.contents)
227
228	# layer info
229
230	def readLayerInfo(self, info, validateRead=None):
231		"""
232		``validateRead`` will validate the data, by default it is set to the
233		class's ``validateRead`` value, can be overridden.
234		"""
235		if validateRead is None:
236			validateRead = self._validateRead
237		infoDict = self._getPlist(LAYERINFO_FILENAME, {})
238		if validateRead:
239			if not isinstance(infoDict, dict):
240				raise GlifLibError("layerinfo.plist is not properly formatted.")
241			infoDict = validateLayerInfoVersion3Data(infoDict)
242		# populate the object
243		for attr, value in infoDict.items():
244			try:
245				setattr(info, attr, value)
246			except AttributeError:
247				raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr)
248
249	def writeLayerInfo(self, info, validateWrite=None):
250		"""
251		``validateWrite`` will validate the data, by default it is set to the
252		class's ``validateWrite`` value, can be overridden.
253		"""
254		if validateWrite is None:
255			validateWrite = self._validateWrite
256		if self.ufoFormatVersion < 3:
257			raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion)
258		# gather data
259		infoData = {}
260		for attr in layerInfoVersion3ValueData.keys():
261			if hasattr(info, attr):
262				try:
263					value = getattr(info, attr)
264				except AttributeError:
265					raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
266				if value is None or (attr == 'lib' and not value):
267					continue
268				infoData[attr] = value
269		if infoData:
270			# validate
271			if validateWrite:
272				infoData = validateLayerInfoVersion3Data(infoData)
273			# write file
274			self._writePlist(LAYERINFO_FILENAME, infoData)
275		elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME):
276			# data empty, remove existing file
277			self.fs.remove(LAYERINFO_FILENAME)
278
279	def getGLIF(self, glyphName):
280		"""
281		Get the raw GLIF text for a given glyph name. This only works
282		for GLIF files that are already on disk.
283
284		This method is useful in situations when the raw XML needs to be
285		read from a glyph set for a particular glyph before fully parsing
286		it into an object structure via the readGlyph method.
287
288		Raises KeyError if 'glyphName' is not in contents.plist, or
289		GlifLibError if the file associated with can't be found.
290		"""
291		fileName = self.contents[glyphName]
292		try:
293			return self.fs.readbytes(fileName)
294		except fs.errors.ResourceNotFound:
295			raise GlifLibError(
296				"The file '%s' associated with glyph '%s' in contents.plist "
297				"does not exist on %s" % (fileName, glyphName, self.fs)
298			)
299
300	def getGLIFModificationTime(self, glyphName):
301		"""
302		Returns the modification time for the GLIF file with 'glyphName', as
303		a floating point number giving the number of seconds since the epoch.
304		Return None if the associated file does not exist or the underlying
305		filesystem does not support getting modified times.
306		Raises KeyError if the glyphName is not in contents.plist.
307		"""
308		fileName = self.contents[glyphName]
309		return self.getFileModificationTime(fileName)
310
311	# reading/writing API
312
313	def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=None):
314		"""
315		Read a .glif file for 'glyphName' from the glyph set. The
316		'glyphObject' argument can be any kind of object (even None);
317		the readGlyph() method will attempt to set the following
318		attributes on it:
319			"width"      the advance width of the glyph
320			"height"     the advance height of the glyph
321			"unicodes"   a list of unicode values for this glyph
322			"note"       a string
323			"lib"        a dictionary containing custom data
324			"image"      a dictionary containing image data
325			"guidelines" a list of guideline data dictionaries
326			"anchors"    a list of anchor data dictionaries
327
328		All attributes are optional, in two ways:
329			1) An attribute *won't* be set if the .glif file doesn't
330			   contain data for it. 'glyphObject' will have to deal
331			   with default values itself.
332			2) If setting the attribute fails with an AttributeError
333			   (for example if the 'glyphObject' attribute is read-
334			   only), readGlyph() will not propagate that exception,
335			   but ignore that attribute.
336
337		To retrieve outline information, you need to pass an object
338		conforming to the PointPen protocol as the 'pointPen' argument.
339		This argument may be None if you don't need the outline data.
340
341		readGlyph() will raise KeyError if the glyph is not present in
342		the glyph set.
343
344		``validate`` will validate the data, by default it is set to the
345		class's ``validateRead`` value, can be overridden.
346		"""
347		if validate is None:
348			validate = self._validateRead
349		text = self.getGLIF(glyphName)
350		tree = _glifTreeFromString(text)
351		if self.ufoFormatVersion < 3:
352			formatVersions = (1,)
353		else:
354			formatVersions = (1, 2)
355		_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
356
357	def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=None):
358		"""
359		Write a .glif file for 'glyphName' to the glyph set. The
360		'glyphObject' argument can be any kind of object (even None);
361		the writeGlyph() method will attempt to get the following
362		attributes from it:
363			"width"      the advance with of the glyph
364			"height"     the advance height of the glyph
365			"unicodes"   a list of unicode values for this glyph
366			"note"       a string
367			"lib"        a dictionary containing custom data
368			"image"      a dictionary containing image data
369			"guidelines" a list of guideline data dictionaries
370			"anchors"    a list of anchor data dictionaries
371
372		All attributes are optional: if 'glyphObject' doesn't
373		have the attribute, it will simply be skipped.
374
375		To write outline data to the .glif file, writeGlyph() needs
376		a function (any callable object actually) that will take one
377		argument: an object that conforms to the PointPen protocol.
378		The function will be called by writeGlyph(); it has to call the
379		proper PointPen methods to transfer the outline to the .glif file.
380
381		The GLIF format version will be chosen based on the ufoFormatVersion
382		passed during the creation of this object. If a particular format
383		version is desired, it can be passed with the formatVersion argument.
384
385		``validate`` will validate the data, by default it is set to the
386		class's ``validateWrite`` value, can be overridden.
387		"""
388		if formatVersion is None:
389			if self.ufoFormatVersion >= 3:
390				formatVersion = 2
391			else:
392				formatVersion = 1
393		if formatVersion not in supportedGLIFFormatVersions:
394			raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
395		if formatVersion == 2 and self.ufoFormatVersion < 3:
396			raise GlifLibError(
397				"Unsupported GLIF format version (%d) for UFO format version %d."
398				% (formatVersion, self.ufoFormatVersion)
399			)
400		if validate is None:
401			validate = self._validateWrite
402		fileName = self.contents.get(glyphName)
403		if fileName is None:
404			if self._existingFileNames is None:
405				self._existingFileNames = {}
406				for fileName in self.contents.values():
407					self._existingFileNames[fileName] = fileName.lower()
408			fileName = self.glyphNameToFileName(glyphName, self._existingFileNames)
409			self.contents[glyphName] = fileName
410			self._existingFileNames[fileName] = fileName.lower()
411			if self._reverseContents is not None:
412				self._reverseContents[fileName.lower()] = glyphName
413		data = _writeGlyphToBytes(
414			glyphName,
415			glyphObject,
416			drawPointsFunc,
417			formatVersion=formatVersion,
418			validate=validate,
419		)
420		if (
421			self._havePreviousFile
422			and self.fs.exists(fileName)
423			and data == self.fs.readbytes(fileName)
424		):
425			return
426		self.fs.writebytes(fileName, data)
427
428	def deleteGlyph(self, glyphName):
429		"""Permanently delete the glyph from the glyph set on disk. Will
430		raise KeyError if the glyph is not present in the glyph set.
431		"""
432		fileName = self.contents[glyphName]
433		self.fs.remove(fileName)
434		if self._existingFileNames is not None:
435			del self._existingFileNames[fileName]
436		if self._reverseContents is not None:
437			del self._reverseContents[self.contents[glyphName].lower()]
438		del self.contents[glyphName]
439
440	# dict-like support
441
442	def keys(self):
443		return list(self.contents.keys())
444
445	def has_key(self, glyphName):
446		return glyphName in self.contents
447
448	__contains__ = has_key
449
450	def __len__(self):
451		return len(self.contents)
452
453	def __getitem__(self, glyphName):
454		if glyphName not in self.contents:
455			raise KeyError(glyphName)
456		return self.glyphClass(glyphName, self)
457
458	# quickly fetch unicode values
459
460	def getUnicodes(self, glyphNames=None):
461		"""
462		Return a dictionary that maps glyph names to lists containing
463		the unicode value[s] for that glyph, if any. This parses the .glif
464		files partially, so it is a lot faster than parsing all files completely.
465		By default this checks all glyphs, but a subset can be passed with glyphNames.
466		"""
467		unicodes = {}
468		if glyphNames is None:
469			glyphNames = self.contents.keys()
470		for glyphName in glyphNames:
471			text = self.getGLIF(glyphName)
472			unicodes[glyphName] = _fetchUnicodes(text)
473		return unicodes
474
475	def getComponentReferences(self, glyphNames=None):
476		"""
477		Return a dictionary that maps glyph names to lists containing the
478		base glyph name of components in the glyph. This parses the .glif
479		files partially, so it is a lot faster than parsing all files completely.
480		By default this checks all glyphs, but a subset can be passed with glyphNames.
481		"""
482		components = {}
483		if glyphNames is None:
484			glyphNames = self.contents.keys()
485		for glyphName in glyphNames:
486			text = self.getGLIF(glyphName)
487			components[glyphName] = _fetchComponentBases(text)
488		return components
489
490	def getImageReferences(self, glyphNames=None):
491		"""
492		Return a dictionary that maps glyph names to the file name of the image
493		referenced by the glyph. This parses the .glif files partially, so it is a
494		lot faster than parsing all files completely.
495		By default this checks all glyphs, but a subset can be passed with glyphNames.
496		"""
497		images = {}
498		if glyphNames is None:
499			glyphNames = self.contents.keys()
500		for glyphName in glyphNames:
501			text = self.getGLIF(glyphName)
502			images[glyphName] = _fetchImageFileName(text)
503		return images
504
505	def close(self):
506		if self._shouldClose:
507			self.fs.close()
508
509	def __enter__(self):
510		return self
511
512	def __exit__(self, exc_type, exc_value, exc_tb):
513		self.close()
514
515
516# -----------------------
517# Glyph Name to File Name
518# -----------------------
519
520def glyphNameToFileName(glyphName, existingFileNames):
521	"""
522	Wrapper around the userNameToFileName function in filenames.py
523	"""
524	if existingFileNames is None:
525		existingFileNames = []
526	if not isinstance(glyphName, unicode):
527		try:
528			new = unicode(glyphName)
529			glyphName = new
530		except UnicodeDecodeError:
531			pass
532	return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif")
533
534# -----------------------
535# GLIF To and From String
536# -----------------------
537
538def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
539	"""
540	Read .glif data from a string into a glyph object.
541
542	The 'glyphObject' argument can be any kind of object (even None);
543	the readGlyphFromString() method will attempt to set the following
544	attributes on it:
545		"width"      the advance with of the glyph
546		"height"     the advance height of the glyph
547		"unicodes"   a list of unicode values for this glyph
548		"note"       a string
549		"lib"        a dictionary containing custom data
550		"image"      a dictionary containing image data
551		"guidelines" a list of guideline data dictionaries
552		"anchors"    a list of anchor data dictionaries
553
554	All attributes are optional, in two ways:
555		1) An attribute *won't* be set if the .glif file doesn't
556		   contain data for it. 'glyphObject' will have to deal
557		   with default values itself.
558		2) If setting the attribute fails with an AttributeError
559		   (for example if the 'glyphObject' attribute is read-
560		   only), readGlyphFromString() will not propagate that
561		   exception, but ignore that attribute.
562
563	To retrieve outline information, you need to pass an object
564	conforming to the PointPen protocol as the 'pointPen' argument.
565	This argument may be None if you don't need the outline data.
566
567	The formatVersions argument defined the GLIF format versions
568	that are allowed to be read.
569
570	``validate`` will validate the read data. It is set to ``True`` by default.
571	"""
572	tree = _glifTreeFromString(aString)
573	_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
574
575
576def _writeGlyphToBytes(
577		glyphName, glyphObject=None, drawPointsFunc=None, writer=None,
578		formatVersion=2, validate=True):
579	"""Return .glif data for a glyph as a UTF-8 encoded bytes string."""
580	# start
581	if validate and not isinstance(glyphName, basestring):
582		raise GlifLibError("The glyph name is not properly formatted.")
583	if validate and len(glyphName) == 0:
584		raise GlifLibError("The glyph name is empty.")
585	root = etree.Element("glyph", OrderedDict([("name", glyphName), ("format", repr(formatVersion))]))
586	identifiers = set()
587	# advance
588	_writeAdvance(glyphObject, root, validate)
589	# unicodes
590	if getattr(glyphObject, "unicodes", None):
591		_writeUnicodes(glyphObject, root, validate)
592	# note
593	if getattr(glyphObject, "note", None):
594		_writeNote(glyphObject, root, validate)
595	# image
596	if formatVersion >= 2 and getattr(glyphObject, "image", None):
597		_writeImage(glyphObject, root, validate)
598	# guidelines
599	if formatVersion >= 2 and getattr(glyphObject, "guidelines", None):
600		_writeGuidelines(glyphObject, root, identifiers, validate)
601	# anchors
602	anchors = getattr(glyphObject, "anchors", None)
603	if formatVersion >= 2 and anchors:
604		_writeAnchors(glyphObject, root, identifiers, validate)
605	# outline
606	if drawPointsFunc is not None:
607		outline = etree.SubElement(root, "outline")
608		pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
609		drawPointsFunc(pen)
610		if formatVersion == 1 and anchors:
611			_writeAnchorsFormat1(pen, anchors, validate)
612		# prevent lxml from writing self-closing tags
613		if not len(outline):
614			outline.text = "\n  "
615	# lib
616	if getattr(glyphObject, "lib", None):
617		_writeLib(glyphObject, root, validate)
618	# return the text
619	data = etree.tostring(
620		root, encoding="UTF-8", xml_declaration=True, pretty_print=True
621	)
622	return data
623
624
625def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=2, validate=True):
626	"""
627	Return .glif data for a glyph as a Unicode string (`unicode` in py2, `str`
628	in py3). The XML declaration's encoding is always set to "UTF-8".
629	The 'glyphObject' argument can be any kind of object (even None);
630	the writeGlyphToString() method will attempt to get the following
631	attributes from it:
632		"width"      the advance width of the glyph
633		"height"     the advance height of the glyph
634		"unicodes"   a list of unicode values for this glyph
635		"note"       a string
636		"lib"        a dictionary containing custom data
637		"image"      a dictionary containing image data
638		"guidelines" a list of guideline data dictionaries
639		"anchors"    a list of anchor data dictionaries
640
641	All attributes are optional: if 'glyphObject' doesn't
642	have the attribute, it will simply be skipped.
643
644	To write outline data to the .glif file, writeGlyphToString() needs
645	a function (any callable object actually) that will take one
646	argument: an object that conforms to the PointPen protocol.
647	The function will be called by writeGlyphToString(); it has to call the
648	proper PointPen methods to transfer the outline to the .glif file.
649
650	The GLIF format version can be specified with the formatVersion argument.
651
652	``validate`` will validate the written data. It is set to ``True`` by default.
653	"""
654	data = _writeGlyphToBytes(
655		glyphName,
656		glyphObject=glyphObject,
657		drawPointsFunc=drawPointsFunc,
658		formatVersion=formatVersion,
659		validate=validate,
660	)
661	return data.decode("utf-8")
662
663
664def _writeAdvance(glyphObject, element, validate):
665	width = getattr(glyphObject, "width", None)
666	if width is not None:
667		if validate and not isinstance(width, numberTypes):
668			raise GlifLibError("width attribute must be int or float")
669		if width == 0:
670			width = None
671	height = getattr(glyphObject, "height", None)
672	if height is not None:
673		if validate and not isinstance(height, numberTypes):
674			raise GlifLibError("height attribute must be int or float")
675		if height == 0:
676			height = None
677	if width is not None and height is not None:
678		etree.SubElement(element, "advance", OrderedDict([("height", repr(height)), ("width", repr(width))]))
679	elif width is not None:
680		etree.SubElement(element, "advance", dict(width=repr(width)))
681	elif height is not None:
682		etree.SubElement(element, "advance", dict(height=repr(height)))
683
684def _writeUnicodes(glyphObject, element, validate):
685	unicodes = getattr(glyphObject, "unicodes", None)
686	if validate and isinstance(unicodes, integerTypes):
687		unicodes = [unicodes]
688	seen = set()
689	for code in unicodes:
690		if validate and not isinstance(code, integerTypes):
691			raise GlifLibError("unicode values must be int")
692		if code in seen:
693			continue
694		seen.add(code)
695		hexCode = "%04X" % code
696		etree.SubElement(element, "unicode", dict(hex=hexCode))
697
698def _writeNote(glyphObject, element, validate):
699	note = getattr(glyphObject, "note", None)
700	if validate and not isinstance(note, basestring):
701		raise GlifLibError("note attribute must be str or unicode")
702	note = note.strip()
703	note = "\n" + note + "\n"
704	# ensure text is unicode, if it's bytes decode as ASCII
705	etree.SubElement(element, "note").text = tounicode(note)
706
707def _writeImage(glyphObject, element, validate):
708	image = getattr(glyphObject, "image", None)
709	if validate and not imageValidator(image):
710		raise GlifLibError("image attribute must be a dict or dict-like object with the proper structure.")
711	attrs = OrderedDict([("fileName", image["fileName"])])
712	for attr, default in _transformationInfo:
713		value = image.get(attr, default)
714		if value != default:
715			attrs[attr] = repr(value)
716	color = image.get("color")
717	if color is not None:
718		attrs["color"] = color
719	etree.SubElement(element, "image", attrs)
720
721def _writeGuidelines(glyphObject, element, identifiers, validate):
722	guidelines = getattr(glyphObject, "guidelines", [])
723	if validate and not guidelinesValidator(guidelines):
724		raise GlifLibError("guidelines attribute does not have the proper structure.")
725	for guideline in guidelines:
726		attrs = OrderedDict()
727		x = guideline.get("x")
728		if x is not None:
729			attrs["x"] = repr(x)
730		y = guideline.get("y")
731		if y is not None:
732			attrs["y"] = repr(y)
733		angle = guideline.get("angle")
734		if angle is not None:
735			attrs["angle"] = repr(angle)
736		name = guideline.get("name")
737		if name is not None:
738			attrs["name"] = name
739		color = guideline.get("color")
740		if color is not None:
741			attrs["color"] = color
742		identifier = guideline.get("identifier")
743		if identifier is not None:
744			if validate and identifier in identifiers:
745				raise GlifLibError("identifier used more than once: %s" % identifier)
746			attrs["identifier"] = identifier
747			identifiers.add(identifier)
748		etree.SubElement(element, "guideline", attrs)
749
750def _writeAnchorsFormat1(pen, anchors, validate):
751	if validate and not anchorsValidator(anchors):
752		raise GlifLibError("anchors attribute does not have the proper structure.")
753	for anchor in anchors:
754		attrs = {}
755		x = anchor["x"]
756		attrs["x"] = repr(x)
757		y = anchor["y"]
758		attrs["y"] = repr(y)
759		name = anchor.get("name")
760		if name is not None:
761			attrs["name"] = name
762		pen.beginPath()
763		pen.addPoint((x, y), segmentType="move", name=name)
764		pen.endPath()
765
766def _writeAnchors(glyphObject, element, identifiers, validate):
767	anchors = getattr(glyphObject, "anchors", [])
768	if validate and not anchorsValidator(anchors):
769		raise GlifLibError("anchors attribute does not have the proper structure.")
770	for anchor in anchors:
771		attrs = OrderedDict()
772		x = anchor["x"]
773		attrs["x"] = repr(x)
774		y = anchor["y"]
775		attrs["y"] = repr(y)
776		name = anchor.get("name")
777		if name is not None:
778			attrs["name"] = name
779		color = anchor.get("color")
780		if color is not None:
781			attrs["color"] = color
782		identifier = anchor.get("identifier")
783		if identifier is not None:
784			if validate and identifier in identifiers:
785				raise GlifLibError("identifier used more than once: %s" % identifier)
786			attrs["identifier"] = identifier
787			identifiers.add(identifier)
788		etree.SubElement(element, "anchor", attrs)
789
790def _writeLib(glyphObject, element, validate):
791	lib = getattr(glyphObject, "lib", None)
792	if not lib:
793		# don't write empty lib
794		return
795	if validate:
796		valid, message = glyphLibValidator(lib)
797		if not valid:
798			raise GlifLibError(message)
799	if not isinstance(lib, dict):
800		lib = dict(lib)
801	# plist inside GLIF begins with 2 levels of indentation
802	e = plistlib.totree(lib, indent_level=2)
803	etree.SubElement(element, "lib").append(e)
804
805# -----------------------
806# layerinfo.plist Support
807# -----------------------
808
809layerInfoVersion3ValueData = {
810	"color"			: dict(type=basestring, valueValidator=colorValidator),
811	"lib"			: dict(type=dict, valueValidator=genericTypeValidator)
812}
813
814def validateLayerInfoVersion3ValueForAttribute(attr, value):
815	"""
816	This performs very basic validation of the value for attribute
817	following the UFO 3 fontinfo.plist specification. The results
818	of this should not be interpretted as *correct* for the font
819	that they are part of. This merely indicates that the value
820	is of the proper type and, where the specification defines
821	a set range of possible values for an attribute, that the
822	value is in the accepted range.
823	"""
824	if attr not in layerInfoVersion3ValueData:
825		return False
826	dataValidationDict = layerInfoVersion3ValueData[attr]
827	valueType = dataValidationDict.get("type")
828	validator = dataValidationDict.get("valueValidator")
829	valueOptions = dataValidationDict.get("valueOptions")
830	# have specific options for the validator
831	if valueOptions is not None:
832		isValidValue = validator(value, valueOptions)
833	# no specific options
834	else:
835		if validator == genericTypeValidator:
836			isValidValue = validator(value, valueType)
837		else:
838			isValidValue = validator(value)
839	return isValidValue
840
841def validateLayerInfoVersion3Data(infoData):
842	"""
843	This performs very basic validation of the value for infoData
844	following the UFO 3 layerinfo.plist specification. The results
845	of this should not be interpretted as *correct* for the font
846	that they are part of. This merely indicates that the values
847	are of the proper type and, where the specification defines
848	a set range of possible values for an attribute, that the
849	value is in the accepted range.
850	"""
851	for attr, value in infoData.items():
852		if attr not in layerInfoVersion3ValueData:
853			raise GlifLibError("Unknown attribute %s." % attr)
854		isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value)
855		if not isValidValue:
856			raise GlifLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
857	return infoData
858
859# -----------------
860# GLIF Tree Support
861# -----------------
862
863def _glifTreeFromFile(aFile):
864	root = etree.parse(aFile).getroot()
865	if root.tag != "glyph":
866		raise GlifLibError("The GLIF is not properly formatted.")
867	if root.text and root.text.strip() != '':
868		raise GlifLibError("Invalid GLIF structure.")
869	return root
870
871
872def _glifTreeFromString(aString):
873	data = tobytes(aString, encoding="utf-8")
874	root = etree.fromstring(data)
875	if root.tag != "glyph":
876		raise GlifLibError("The GLIF is not properly formatted.")
877	if root.text and root.text.strip() != '':
878		raise GlifLibError("Invalid GLIF structure.")
879	return root
880
881def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
882	# check the format version
883	formatVersion = tree.get("format")
884	if validate and formatVersion is None:
885		raise GlifLibError("Unspecified format version in GLIF.")
886	try:
887		v = int(formatVersion)
888		formatVersion = v
889	except ValueError:
890		pass
891	if validate and formatVersion not in formatVersions:
892		raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion)
893	if formatVersion == 1:
894		_readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
895	elif formatVersion == 2:
896		_readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
897	else:
898		raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
899
900
901def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None):
902	# get the name
903	_readName(glyphObject, tree, validate)
904	# populate the sub elements
905	unicodes = []
906	haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False
907	for element in tree:
908		if element.tag == "outline":
909			if validate:
910				if haveSeenOutline:
911					raise GlifLibError("The outline element occurs more than once.")
912				if element.attrib:
913					raise GlifLibError("The outline element contains unknown attributes.")
914				if element.text and element.text.strip() != '':
915					raise GlifLibError("Invalid outline structure.")
916			haveSeenOutline = True
917			buildOutlineFormat1(glyphObject, pointPen, element, validate)
918		elif glyphObject is None:
919			continue
920		elif element.tag == "advance":
921			if validate and haveSeenAdvance:
922				raise GlifLibError("The advance element occurs more than once.")
923			haveSeenAdvance = True
924			_readAdvance(glyphObject, element)
925		elif element.tag == "unicode":
926			try:
927				v = element.get("hex")
928				v = int(v, 16)
929				if v not in unicodes:
930					unicodes.append(v)
931			except ValueError:
932				raise GlifLibError("Illegal value for hex attribute of unicode element.")
933		elif element.tag == "note":
934			if validate and haveSeenNote:
935				raise GlifLibError("The note element occurs more than once.")
936			haveSeenNote = True
937			_readNote(glyphObject, element)
938		elif element.tag == "lib":
939			if validate and haveSeenLib:
940				raise GlifLibError("The lib element occurs more than once.")
941			haveSeenLib = True
942			_readLib(glyphObject, element, validate)
943		else:
944			raise GlifLibError("Unknown element in GLIF: %s" % element)
945	# set the collected unicodes
946	if unicodes:
947		_relaxedSetattr(glyphObject, "unicodes", unicodes)
948
949def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=None):
950	# get the name
951	_readName(glyphObject, tree, validate)
952	# populate the sub elements
953	unicodes = []
954	guidelines = []
955	anchors = []
956	haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = False
957	identifiers = set()
958	for element in tree:
959		if element.tag == "outline":
960			if validate:
961				if haveSeenOutline:
962					raise GlifLibError("The outline element occurs more than once.")
963				if element.attrib:
964					raise GlifLibError("The outline element contains unknown attributes.")
965				if element.text and element.text.strip() != '':
966					raise GlifLibError("Invalid outline structure.")
967			haveSeenOutline = True
968			if pointPen is not None:
969				buildOutlineFormat2(glyphObject, pointPen, element, identifiers, validate)
970		elif glyphObject is None:
971			continue
972		elif element.tag == "advance":
973			if validate and haveSeenAdvance:
974				raise GlifLibError("The advance element occurs more than once.")
975			haveSeenAdvance = True
976			_readAdvance(glyphObject, element)
977		elif element.tag == "unicode":
978			try:
979				v = element.get("hex")
980				v = int(v, 16)
981				if v not in unicodes:
982					unicodes.append(v)
983			except ValueError:
984				raise GlifLibError("Illegal value for hex attribute of unicode element.")
985		elif element.tag == "guideline":
986			if validate and len(element):
987				raise GlifLibError("Unknown children in guideline element.")
988			attrib = dict(element.attrib)
989			for attr in ("x", "y", "angle"):
990				if attr in attrib:
991					attrib[attr] = _number(attrib[attr])
992			guidelines.append(attrib)
993		elif element.tag == "anchor":
994			if validate and len(element):
995				raise GlifLibError("Unknown children in anchor element.")
996			attrib = dict(element.attrib)
997			for attr in ("x", "y"):
998				if attr in element.attrib:
999					attrib[attr] = _number(attrib[attr])
1000			anchors.append(attrib)
1001		elif element.tag == "image":
1002			if validate:
1003				if haveSeenImage:
1004					raise GlifLibError("The image element occurs more than once.")
1005				if len(element):
1006					raise GlifLibError("Unknown children in image element.")
1007			haveSeenImage = True
1008			_readImage(glyphObject, element, validate)
1009		elif element.tag == "note":
1010			if validate and haveSeenNote:
1011				raise GlifLibError("The note element occurs more than once.")
1012			haveSeenNote = True
1013			_readNote(glyphObject, element)
1014		elif element.tag == "lib":
1015			if validate and haveSeenLib:
1016				raise GlifLibError("The lib element occurs more than once.")
1017			haveSeenLib = True
1018			_readLib(glyphObject, element, validate)
1019		else:
1020			raise GlifLibError("Unknown element in GLIF: %s" % element)
1021	# set the collected unicodes
1022	if unicodes:
1023		_relaxedSetattr(glyphObject, "unicodes", unicodes)
1024	# set the collected guidelines
1025	if guidelines:
1026		if validate and not guidelinesValidator(guidelines, identifiers):
1027			raise GlifLibError("The guidelines are improperly formatted.")
1028		_relaxedSetattr(glyphObject, "guidelines", guidelines)
1029	# set the collected anchors
1030	if anchors:
1031		if validate and not anchorsValidator(anchors, identifiers):
1032			raise GlifLibError("The anchors are improperly formatted.")
1033		_relaxedSetattr(glyphObject, "anchors", anchors)
1034
1035def _readName(glyphObject, root, validate):
1036	glyphName = root.get("name")
1037	if validate and not glyphName:
1038		raise GlifLibError("Empty glyph name in GLIF.")
1039	if glyphName and glyphObject is not None:
1040		_relaxedSetattr(glyphObject, "name", glyphName)
1041
1042def _readAdvance(glyphObject, advance):
1043	width = _number(advance.get("width", 0))
1044	_relaxedSetattr(glyphObject, "width", width)
1045	height = _number(advance.get("height", 0))
1046	_relaxedSetattr(glyphObject, "height", height)
1047
1048def _readNote(glyphObject, note):
1049	lines = note.text.split("\n")
1050	note = "\n".join(line.strip() for line in lines if line.strip())
1051	_relaxedSetattr(glyphObject, "note", note)
1052
1053def _readLib(glyphObject, lib, validate):
1054	assert len(lib) == 1
1055	child = lib[0]
1056	plist = plistlib.fromtree(child)
1057	if validate:
1058		valid, message = glyphLibValidator(plist)
1059		if not valid:
1060			raise GlifLibError(message)
1061	_relaxedSetattr(glyphObject, "lib", plist)
1062
1063def _readImage(glyphObject, image, validate):
1064	imageData = dict(image.attrib)
1065	for attr, default in _transformationInfo:
1066		value = imageData.get(attr, default)
1067		imageData[attr] = _number(value)
1068	if validate and not imageValidator(imageData):
1069		raise GlifLibError("The image element is not properly formatted.")
1070	_relaxedSetattr(glyphObject, "image", imageData)
1071
1072# ----------------
1073# GLIF to PointPen
1074# ----------------
1075
1076contourAttributesFormat2 = set(["identifier"])
1077componentAttributesFormat1 = set(["base", "xScale", "xyScale", "yxScale", "yScale", "xOffset", "yOffset"])
1078componentAttributesFormat2 = componentAttributesFormat1 | set(["identifier"])
1079pointAttributesFormat1 = set(["x", "y", "type", "smooth", "name"])
1080pointAttributesFormat2 = pointAttributesFormat1 | set(["identifier"])
1081pointSmoothOptions = set(("no", "yes"))
1082pointTypeOptions = set(["move", "line", "offcurve", "curve", "qcurve"])
1083
1084# format 1
1085
1086def buildOutlineFormat1(glyphObject, pen, outline, validate):
1087	anchors = []
1088	for element in outline:
1089		if element.tag == "contour":
1090			if len(element) == 1:
1091				point = element[0]
1092				if point.tag == "point":
1093					anchor = _buildAnchorFormat1(point, validate)
1094					if anchor is not None:
1095						anchors.append(anchor)
1096						continue
1097			if pen is not None:
1098				_buildOutlineContourFormat1(pen, element, validate)
1099		elif element.tag == "component":
1100			if pen is not None:
1101				_buildOutlineComponentFormat1(pen, element, validate)
1102		else:
1103			raise GlifLibError("Unknown element in outline element: %s" % element)
1104	if glyphObject is not None and anchors:
1105		if validate and not anchorsValidator(anchors):
1106			raise GlifLibError("GLIF 1 anchors are not properly formatted.")
1107		_relaxedSetattr(glyphObject, "anchors", anchors)
1108
1109def _buildAnchorFormat1(point, validate):
1110	if point.get("type") != "move":
1111		return None
1112	name = point.get("name")
1113	if name is None:
1114		return None
1115	x = point.get("x")
1116	y = point.get("y")
1117	if validate and x is None:
1118		raise GlifLibError("Required x attribute is missing in point element.")
1119	if validate and y is None:
1120		raise GlifLibError("Required y attribute is missing in point element.")
1121	x = _number(x)
1122	y = _number(y)
1123	anchor = dict(x=x, y=y, name=name)
1124	return anchor
1125
1126def _buildOutlineContourFormat1(pen, contour, validate):
1127	if validate and contour.attrib:
1128		raise GlifLibError("Unknown attributes in contour element.")
1129	pen.beginPath()
1130	if len(contour):
1131		massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat1, openContourOffCurveLeniency=True, validate=validate)
1132		_buildOutlinePointsFormat1(pen, massaged)
1133	pen.endPath()
1134
1135def _buildOutlinePointsFormat1(pen, contour):
1136	for point in contour:
1137		x = point["x"]
1138		y = point["y"]
1139		segmentType = point["segmentType"]
1140		smooth = point["smooth"]
1141		name = point["name"]
1142		pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
1143
1144def _buildOutlineComponentFormat1(pen, component, validate):
1145	if validate:
1146		if len(component):
1147			raise GlifLibError("Unknown child elements of component element.")
1148		for attr in component.attrib.keys():
1149			if attr not in componentAttributesFormat1:
1150				raise GlifLibError("Unknown attribute in component element: %s" % attr)
1151	baseGlyphName = component.get("base")
1152	if validate and baseGlyphName is None:
1153		raise GlifLibError("The base attribute is not defined in the component.")
1154	transformation = []
1155	for attr, default in _transformationInfo:
1156		value = component.get(attr)
1157		if value is None:
1158			value = default
1159		else:
1160			value = _number(value)
1161		transformation.append(value)
1162	pen.addComponent(baseGlyphName, tuple(transformation))
1163
1164# format 2
1165
1166def buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate):
1167	for element in outline:
1168		if element.tag == "contour":
1169			_buildOutlineContourFormat2(pen, element, identifiers, validate)
1170		elif element.tag == "component":
1171			_buildOutlineComponentFormat2(pen, element, identifiers, validate)
1172		else:
1173			raise GlifLibError("Unknown element in outline element: %s" % element.tag)
1174
1175def _buildOutlineContourFormat2(pen, contour, identifiers, validate):
1176	if validate:
1177		for attr in contour.attrib.keys():
1178			if attr not in contourAttributesFormat2:
1179				raise GlifLibError("Unknown attribute in contour element: %s" % attr)
1180	identifier = contour.get("identifier")
1181	if identifier is not None:
1182		if validate:
1183			if identifier in identifiers:
1184				raise GlifLibError("The identifier %s is used more than once." % identifier)
1185			if not identifierValidator(identifier):
1186				raise GlifLibError("The contour identifier %s is not valid." % identifier)
1187		identifiers.add(identifier)
1188	try:
1189		pen.beginPath(identifier=identifier)
1190	except TypeError:
1191		pen.beginPath()
1192		warn("The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.", DeprecationWarning)
1193	if len(contour):
1194		massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat2, validate=validate)
1195		_buildOutlinePointsFormat2(pen, massaged, identifiers, validate)
1196	pen.endPath()
1197
1198def _buildOutlinePointsFormat2(pen, contour, identifiers, validate):
1199	for point in contour:
1200		x = point["x"]
1201		y = point["y"]
1202		segmentType = point["segmentType"]
1203		smooth = point["smooth"]
1204		name = point["name"]
1205		identifier = point.get("identifier")
1206		if identifier is not None:
1207			if validate:
1208				if identifier in identifiers:
1209					raise GlifLibError("The identifier %s is used more than once." % identifier)
1210				if not identifierValidator(identifier):
1211					raise GlifLibError("The identifier %s is not valid." % identifier)
1212			identifiers.add(identifier)
1213		try:
1214			pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name, identifier=identifier)
1215		except TypeError:
1216			pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
1217			warn("The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.", DeprecationWarning)
1218
1219def _buildOutlineComponentFormat2(pen, component, identifiers, validate):
1220	if validate:
1221		if len(component):
1222			raise GlifLibError("Unknown child elements of component element.")
1223		for attr in component.attrib.keys():
1224			if attr not in componentAttributesFormat2:
1225				raise GlifLibError("Unknown attribute in component element: %s" % attr)
1226	baseGlyphName = component.get("base")
1227	if validate and baseGlyphName is None:
1228		raise GlifLibError("The base attribute is not defined in the component.")
1229	transformation = []
1230	for attr, default in _transformationInfo:
1231		value = component.get(attr)
1232		if value is None:
1233			value = default
1234		else:
1235			value = _number(value)
1236		transformation.append(value)
1237	identifier = component.get("identifier")
1238	if identifier is not None:
1239		if validate:
1240			if identifier in identifiers:
1241				raise GlifLibError("The identifier %s is used more than once." % identifier)
1242			if validate and not identifierValidator(identifier):
1243				raise GlifLibError("The identifier %s is not valid." % identifier)
1244		identifiers.add(identifier)
1245	try:
1246		pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier)
1247	except TypeError:
1248		pen.addComponent(baseGlyphName, tuple(transformation))
1249		warn("The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.", DeprecationWarning)
1250
1251# all formats
1252
1253def _validateAndMassagePointStructures(contour, pointAttributes, openContourOffCurveLeniency=False, validate=True):
1254	if not len(contour):
1255		return
1256	# store some data for later validation
1257	lastOnCurvePoint = None
1258	haveOffCurvePoint = False
1259	# validate and massage the individual point elements
1260	massaged = []
1261	for index, element in enumerate(contour):
1262		# not <point>
1263		if element.tag != "point":
1264			raise GlifLibError("Unknown child element (%s) of contour element." % element.tag)
1265		point = dict(element.attrib)
1266		massaged.append(point)
1267		if validate:
1268			# unknown attributes
1269			for attr in point.keys():
1270				if attr not in pointAttributes:
1271					raise GlifLibError("Unknown attribute in point element: %s" % attr)
1272			# search for unknown children
1273			if len(element):
1274				raise GlifLibError("Unknown child elements in point element.")
1275		# x and y are required
1276		for attr in ("x", "y"):
1277			value = element.get(attr)
1278			if validate and value is None:
1279				raise GlifLibError("Required %s attribute is missing in point element." % attr)
1280			point[attr] = _number(value)
1281		# segment type
1282		pointType = point.pop("type", "offcurve")
1283		if validate and pointType not in pointTypeOptions:
1284			raise GlifLibError("Unknown point type: %s" % pointType)
1285		if pointType == "offcurve":
1286			pointType = None
1287		point["segmentType"] = pointType
1288		if pointType is None:
1289			haveOffCurvePoint = True
1290		else:
1291			lastOnCurvePoint = index
1292		# move can only occur as the first point
1293		if validate and pointType == "move" and index != 0:
1294			raise GlifLibError("A move point occurs after the first point in the contour.")
1295		# smooth is optional
1296		smooth = point.get("smooth", "no")
1297		if validate and smooth is not None:
1298			if smooth not in pointSmoothOptions:
1299				raise GlifLibError("Unknown point smooth value: %s" % smooth)
1300		smooth = smooth == "yes"
1301		point["smooth"] = smooth
1302		# smooth can only be applied to curve and qcurve
1303		if validate and smooth and pointType is None:
1304			raise GlifLibError("smooth attribute set in an offcurve point.")
1305		# name is optional
1306		if "name" not in element.attrib:
1307			point["name"] = None
1308	if openContourOffCurveLeniency:
1309		# remove offcurves that precede a move. this is technically illegal,
1310		# but we let it slide because there are fonts out there in the wild like this.
1311		if massaged[0]["segmentType"] == "move":
1312			count = 0
1313			for point in reversed(massaged):
1314				if point["segmentType"] is None:
1315					count += 1
1316				else:
1317					break
1318			if count:
1319				massaged = massaged[:-count]
1320	# validate the off-curves in the segments
1321	if validate and haveOffCurvePoint and lastOnCurvePoint is not None:
1322		# we only care about how many offCurves there are before an onCurve
1323		# filter out the trailing offCurves
1324		offCurvesCount = len(massaged) - 1 - lastOnCurvePoint
1325		for point in massaged:
1326			segmentType = point["segmentType"]
1327			if segmentType is None:
1328				offCurvesCount += 1
1329			else:
1330				if offCurvesCount:
1331					# move and line can't be preceded by off-curves
1332					if segmentType == "move":
1333						# this will have been filtered out already
1334						raise GlifLibError("move can not have an offcurve.")
1335					elif segmentType == "line":
1336						raise GlifLibError("line can not have an offcurve.")
1337					elif segmentType == "curve":
1338						if offCurvesCount > 2:
1339							raise GlifLibError("Too many offcurves defined for curve.")
1340					elif segmentType == "qcurve":
1341						pass
1342					else:
1343						# unknown segment type. it'll be caught later.
1344						pass
1345				offCurvesCount = 0
1346	return massaged
1347
1348# ---------------------
1349# Misc Helper Functions
1350# ---------------------
1351
1352def _relaxedSetattr(object, attr, value):
1353	try:
1354		setattr(object, attr, value)
1355	except AttributeError:
1356		pass
1357
1358def _number(s):
1359	"""
1360	Given a numeric string, return an integer or a float, whichever
1361	the string indicates. _number("1") will return the integer 1,
1362	_number("1.0") will return the float 1.0.
1363
1364	>>> _number("1")
1365	1
1366	>>> _number("1.0")
1367	1.0
1368	>>> _number("a")  # doctest: +IGNORE_EXCEPTION_DETAIL
1369	Traceback (most recent call last):
1370	    ...
1371	GlifLibError: Could not convert a to an int or float.
1372	"""
1373	try:
1374		n = int(s)
1375		return n
1376	except ValueError:
1377		pass
1378	try:
1379		n = float(s)
1380		return n
1381	except ValueError:
1382		raise GlifLibError("Could not convert %s to an int or float." % s)
1383
1384# --------------------
1385# Rapid Value Fetching
1386# --------------------
1387
1388# base
1389
1390class _DoneParsing(Exception): pass
1391
1392class _BaseParser(object):
1393
1394	def __init__(self):
1395		self._elementStack = []
1396
1397	def parse(self, text):
1398		from xml.parsers.expat import ParserCreate
1399		parser = ParserCreate()
1400		parser.StartElementHandler = self.startElementHandler
1401		parser.EndElementHandler = self.endElementHandler
1402		parser.Parse(text)
1403
1404	def startElementHandler(self, name, attrs):
1405		self._elementStack.append(name)
1406
1407	def endElementHandler(self, name):
1408		other = self._elementStack.pop(-1)
1409		assert other == name
1410
1411
1412# unicodes
1413
1414def _fetchUnicodes(glif):
1415	"""
1416	Get a list of unicodes listed in glif.
1417	"""
1418	parser = _FetchUnicodesParser()
1419	parser.parse(glif)
1420	return parser.unicodes
1421
1422class _FetchUnicodesParser(_BaseParser):
1423
1424	def __init__(self):
1425		self.unicodes = []
1426		super(_FetchUnicodesParser, self).__init__()
1427
1428	def startElementHandler(self, name, attrs):
1429		if name == "unicode" and self._elementStack and self._elementStack[-1] == "glyph":
1430			value = attrs.get("hex")
1431			if value is not None:
1432				try:
1433					value = int(value, 16)
1434					if value not in self.unicodes:
1435						self.unicodes.append(value)
1436				except ValueError:
1437					pass
1438		super(_FetchUnicodesParser, self).startElementHandler(name, attrs)
1439
1440# image
1441
1442def _fetchImageFileName(glif):
1443	"""
1444	The image file name (if any) from glif.
1445	"""
1446	parser = _FetchImageFileNameParser()
1447	try:
1448		parser.parse(glif)
1449	except _DoneParsing:
1450		pass
1451	return parser.fileName
1452
1453class _FetchImageFileNameParser(_BaseParser):
1454
1455	def __init__(self):
1456		self.fileName = None
1457		super(_FetchImageFileNameParser, self).__init__()
1458
1459	def startElementHandler(self, name, attrs):
1460		if name == "image" and self._elementStack and self._elementStack[-1] == "glyph":
1461			self.fileName = attrs.get("fileName")
1462			raise _DoneParsing
1463		super(_FetchImageFileNameParser, self).startElementHandler(name, attrs)
1464
1465# component references
1466
1467def _fetchComponentBases(glif):
1468	"""
1469	Get a list of component base glyphs listed in glif.
1470	"""
1471	parser = _FetchComponentBasesParser()
1472	try:
1473		parser.parse(glif)
1474	except _DoneParsing:
1475		pass
1476	return list(parser.bases)
1477
1478class _FetchComponentBasesParser(_BaseParser):
1479
1480	def __init__(self):
1481		self.bases = []
1482		super(_FetchComponentBasesParser, self).__init__()
1483
1484	def startElementHandler(self, name, attrs):
1485		if name == "component" and self._elementStack and self._elementStack[-1] == "outline":
1486			base = attrs.get("base")
1487			if base is not None:
1488				self.bases.append(base)
1489		super(_FetchComponentBasesParser, self).startElementHandler(name, attrs)
1490
1491	def endElementHandler(self, name):
1492		if name == "outline":
1493			raise _DoneParsing
1494		super(_FetchComponentBasesParser, self).endElementHandler(name)
1495
1496# --------------
1497# GLIF Point Pen
1498# --------------
1499
1500_transformationInfo = [
1501	# field name, default value
1502	("xScale",    1),
1503	("xyScale",   0),
1504	("yxScale",   0),
1505	("yScale",    1),
1506	("xOffset",   0),
1507	("yOffset",   0),
1508]
1509
1510class GLIFPointPen(AbstractPointPen):
1511
1512	"""
1513	Helper class using the PointPen protocol to write the <outline>
1514	part of .glif files.
1515	"""
1516
1517	def __init__(self, element, formatVersion=2, identifiers=None, validate=True):
1518		if identifiers is None:
1519			identifiers = set()
1520		self.formatVersion = formatVersion
1521		self.identifiers = identifiers
1522		self.outline = element
1523		self.contour = None
1524		self.prevOffCurveCount = 0
1525		self.prevPointTypes = []
1526		self.validate = validate
1527
1528	def beginPath(self, identifier=None, **kwargs):
1529		attrs = OrderedDict()
1530		if identifier is not None and self.formatVersion >= 2:
1531			if self.validate:
1532				if identifier in self.identifiers:
1533					raise GlifLibError("identifier used more than once: %s" % identifier)
1534				if not identifierValidator(identifier):
1535					raise GlifLibError("identifier not formatted properly: %s" % identifier)
1536			attrs["identifier"] = identifier
1537			self.identifiers.add(identifier)
1538		self.contour = etree.SubElement(self.outline, "contour", attrs)
1539		self.prevOffCurveCount = 0
1540
1541	def endPath(self):
1542		if self.prevPointTypes and self.prevPointTypes[0] == "move":
1543			if self.validate and self.prevPointTypes[-1] == "offcurve":
1544				raise GlifLibError("open contour has loose offcurve point")
1545		# prevent lxml from writing self-closing tags
1546		if not len(self.contour):
1547			self.contour.text = "\n  "
1548		self.contour = None
1549		self.prevPointType = None
1550		self.prevOffCurveCount = 0
1551		self.prevPointTypes = []
1552
1553	def addPoint(self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs):
1554		attrs = OrderedDict()
1555		# coordinates
1556		if pt is not None:
1557			if self.validate:
1558				for coord in pt:
1559					if not isinstance(coord, numberTypes):
1560						raise GlifLibError("coordinates must be int or float")
1561			attrs["x"] = repr(pt[0])
1562			attrs["y"] = repr(pt[1])
1563		# segment type
1564		if segmentType == "offcurve":
1565			segmentType = None
1566		if self.validate:
1567			if segmentType == "move" and self.prevPointTypes:
1568				raise GlifLibError("move occurs after a point has already been added to the contour.")
1569			if segmentType in ("move", "line") and self.prevPointTypes and self.prevPointTypes[-1] == "offcurve":
1570				raise GlifLibError("offcurve occurs before %s point." % segmentType)
1571			if segmentType == "curve" and self.prevOffCurveCount > 2:
1572				raise GlifLibError("too many offcurve points before curve point.")
1573		if segmentType is not None:
1574			attrs["type"] = segmentType
1575		else:
1576			segmentType = "offcurve"
1577		if segmentType == "offcurve":
1578			self.prevOffCurveCount += 1
1579		else:
1580			self.prevOffCurveCount = 0
1581		self.prevPointTypes.append(segmentType)
1582		# smooth
1583		if smooth:
1584			if self.validate and segmentType == "offcurve":
1585				raise GlifLibError("can't set smooth in an offcurve point.")
1586			attrs["smooth"] = "yes"
1587		# name
1588		if name is not None:
1589			attrs["name"] = name
1590		# identifier
1591		if identifier is not None and self.formatVersion >= 2:
1592			if self.validate:
1593				if identifier in self.identifiers:
1594					raise GlifLibError("identifier used more than once: %s" % identifier)
1595				if not identifierValidator(identifier):
1596					raise GlifLibError("identifier not formatted properly: %s" % identifier)
1597			attrs["identifier"] = identifier
1598			self.identifiers.add(identifier)
1599		etree.SubElement(self.contour, "point", attrs)
1600
1601	def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
1602		attrs = OrderedDict([("base", glyphName)])
1603		for (attr, default), value in zip(_transformationInfo, transformation):
1604			if self.validate and not isinstance(value, numberTypes):
1605				raise GlifLibError("transformation values must be int or float")
1606			if value != default:
1607				attrs[attr] = repr(value)
1608		if identifier is not None and self.formatVersion >= 2:
1609			if self.validate:
1610				if identifier in self.identifiers:
1611					raise GlifLibError("identifier used more than once: %s" % identifier)
1612				if self.validate and not identifierValidator(identifier):
1613					raise GlifLibError("identifier not formatted properly: %s" % identifier)
1614			attrs["identifier"] = identifier
1615			self.identifiers.add(identifier)
1616		etree.SubElement(self.outline, "component", attrs)
1617
1618if __name__ == "__main__":
1619	import doctest
1620	doctest.testmod()
1621