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