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