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