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