Spaces:
Paused
Paused
| """ | |
| glifLib.py -- Generic module for reading and writing the .glif format. | |
| More info about the .glif format (GLyphInterchangeFormat) can be found here: | |
| http://unifiedfontobject.org | |
| The main class in this module is GlyphSet. It manages a set of .glif files | |
| in a folder. It offers two ways to read glyph data, and one way to write | |
| glyph data. See the class doc string for details. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import enum | |
| from warnings import warn | |
| from collections import OrderedDict | |
| import fs | |
| import fs.base | |
| import fs.errors | |
| import fs.osfs | |
| import fs.path | |
| from fontTools.misc.textTools import tobytes | |
| from fontTools.misc import plistlib | |
| from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen | |
| from fontTools.ufoLib.errors import GlifLibError | |
| from fontTools.ufoLib.filenames import userNameToFileName | |
| from fontTools.ufoLib.validators import ( | |
| genericTypeValidator, | |
| colorValidator, | |
| guidelinesValidator, | |
| anchorsValidator, | |
| identifierValidator, | |
| imageValidator, | |
| glyphLibValidator, | |
| ) | |
| from fontTools.misc import etree | |
| from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion | |
| from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin | |
| __all__ = [ | |
| "GlyphSet", | |
| "GlifLibError", | |
| "readGlyphFromString", | |
| "writeGlyphToString", | |
| "glyphNameToFileName", | |
| ] | |
| logger = logging.getLogger(__name__) | |
| # --------- | |
| # Constants | |
| # --------- | |
| CONTENTS_FILENAME = "contents.plist" | |
| LAYERINFO_FILENAME = "layerinfo.plist" | |
| class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum): | |
| FORMAT_1_0 = (1, 0) | |
| FORMAT_2_0 = (2, 0) | |
| def default(cls, ufoFormatVersion=None): | |
| if ufoFormatVersion is not None: | |
| return max(cls.supported_versions(ufoFormatVersion)) | |
| return super().default() | |
| def supported_versions(cls, ufoFormatVersion=None): | |
| if ufoFormatVersion is None: | |
| # if ufo format unspecified, return all the supported GLIF formats | |
| return super().supported_versions() | |
| # else only return the GLIF formats supported by the given UFO format | |
| versions = {cls.FORMAT_1_0} | |
| if ufoFormatVersion >= UFOFormatVersion.FORMAT_3_0: | |
| versions.add(cls.FORMAT_2_0) | |
| return frozenset(versions) | |
| # workaround for py3.11, see https://github.com/fonttools/fonttools/pull/2655 | |
| GLIFFormatVersion.__str__ = _VersionTupleEnumMixin.__str__ | |
| # ------------ | |
| # Simple Glyph | |
| # ------------ | |
| class Glyph: | |
| """ | |
| Minimal glyph object. It has no glyph attributes until either | |
| the draw() or the drawPoints() method has been called. | |
| """ | |
| def __init__(self, glyphName, glyphSet): | |
| self.glyphName = glyphName | |
| self.glyphSet = glyphSet | |
| def draw(self, pen, outputImpliedClosingLine=False): | |
| """ | |
| Draw this glyph onto a *FontTools* Pen. | |
| """ | |
| pointPen = PointToSegmentPen( | |
| pen, outputImpliedClosingLine=outputImpliedClosingLine | |
| ) | |
| self.drawPoints(pointPen) | |
| def drawPoints(self, pointPen): | |
| """ | |
| Draw this glyph onto a PointPen. | |
| """ | |
| self.glyphSet.readGlyph(self.glyphName, self, pointPen) | |
| # --------- | |
| # Glyph Set | |
| # --------- | |
| class GlyphSet(_UFOBaseIO): | |
| """ | |
| GlyphSet manages a set of .glif files inside one directory. | |
| GlyphSet's constructor takes a path to an existing directory as it's | |
| first argument. Reading glyph data can either be done through the | |
| readGlyph() method, or by using GlyphSet's dictionary interface, where | |
| the keys are glyph names and the values are (very) simple glyph objects. | |
| To write a glyph to the glyph set, you use the writeGlyph() method. | |
| The simple glyph objects returned through the dict interface do not | |
| support writing, they are just a convenient way to get at the glyph data. | |
| """ | |
| glyphClass = Glyph | |
| def __init__( | |
| self, | |
| path, | |
| glyphNameToFileNameFunc=None, | |
| ufoFormatVersion=None, | |
| validateRead=True, | |
| validateWrite=True, | |
| expectContentsFile=False, | |
| ): | |
| """ | |
| 'path' should be a path (string) to an existing local directory, or | |
| an instance of fs.base.FS class. | |
| The optional 'glyphNameToFileNameFunc' argument must be a callback | |
| function that takes two arguments: a glyph name and a list of all | |
| existing filenames (if any exist). It should return a file name | |
| (including the .glif extension). The glyphNameToFileName function | |
| is called whenever a file name is created for a given glyph name. | |
| ``validateRead`` will validate read operations. Its default is ``True``. | |
| ``validateWrite`` will validate write operations. Its default is ``True``. | |
| ``expectContentsFile`` will raise a GlifLibError if a contents.plist file is | |
| not found on the glyph set file system. This should be set to ``True`` if you | |
| are reading an existing UFO and ``False`` if you create a fresh glyph set. | |
| """ | |
| try: | |
| ufoFormatVersion = UFOFormatVersion(ufoFormatVersion) | |
| except ValueError as e: | |
| from fontTools.ufoLib.errors import UnsupportedUFOFormat | |
| raise UnsupportedUFOFormat( | |
| f"Unsupported UFO format: {ufoFormatVersion!r}" | |
| ) from e | |
| if hasattr(path, "__fspath__"): # support os.PathLike objects | |
| path = path.__fspath__() | |
| if isinstance(path, str): | |
| try: | |
| filesystem = fs.osfs.OSFS(path) | |
| except fs.errors.CreateFailed: | |
| raise GlifLibError("No glyphs directory '%s'" % path) | |
| self._shouldClose = True | |
| elif isinstance(path, fs.base.FS): | |
| filesystem = path | |
| try: | |
| filesystem.check() | |
| except fs.errors.FilesystemClosed: | |
| raise GlifLibError("the filesystem '%s' is closed" % filesystem) | |
| self._shouldClose = False | |
| else: | |
| raise TypeError( | |
| "Expected a path string or fs object, found %s" % type(path).__name__ | |
| ) | |
| try: | |
| path = filesystem.getsyspath("/") | |
| except fs.errors.NoSysPath: | |
| # network or in-memory FS may not map to the local one | |
| path = str(filesystem) | |
| # 'dirName' is kept for backward compatibility only, but it's DEPRECATED | |
| # as it's not guaranteed that it maps to an existing OSFS directory. | |
| # Client could use the FS api via the `self.fs` attribute instead. | |
| self.dirName = fs.path.parts(path)[-1] | |
| self.fs = filesystem | |
| # if glyphSet contains no 'contents.plist', we consider it empty | |
| self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) | |
| if expectContentsFile and not self._havePreviousFile: | |
| raise GlifLibError(f"{CONTENTS_FILENAME} is missing.") | |
| # attribute kept for backward compatibility | |
| self.ufoFormatVersion = ufoFormatVersion.major | |
| self.ufoFormatVersionTuple = ufoFormatVersion | |
| if glyphNameToFileNameFunc is None: | |
| glyphNameToFileNameFunc = glyphNameToFileName | |
| self.glyphNameToFileName = glyphNameToFileNameFunc | |
| self._validateRead = validateRead | |
| self._validateWrite = validateWrite | |
| self._existingFileNames: set[str] | None = None | |
| self._reverseContents = None | |
| self.rebuildContents() | |
| def rebuildContents(self, validateRead=None): | |
| """ | |
| Rebuild the contents dict by loading contents.plist. | |
| ``validateRead`` will validate the data, by default it is set to the | |
| class's ``validateRead`` value, can be overridden. | |
| """ | |
| if validateRead is None: | |
| validateRead = self._validateRead | |
| contents = self._getPlist(CONTENTS_FILENAME, {}) | |
| # validate the contents | |
| if validateRead: | |
| invalidFormat = False | |
| if not isinstance(contents, dict): | |
| invalidFormat = True | |
| else: | |
| for name, fileName in contents.items(): | |
| if not isinstance(name, str): | |
| invalidFormat = True | |
| if not isinstance(fileName, str): | |
| invalidFormat = True | |
| elif not self.fs.exists(fileName): | |
| raise GlifLibError( | |
| "%s references a file that does not exist: %s" | |
| % (CONTENTS_FILENAME, fileName) | |
| ) | |
| if invalidFormat: | |
| raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME) | |
| self.contents = contents | |
| self._existingFileNames = None | |
| self._reverseContents = None | |
| def getReverseContents(self): | |
| """ | |
| Return a reversed dict of self.contents, mapping file names to | |
| glyph names. This is primarily an aid for custom glyph name to file | |
| name schemes that want to make sure they don't generate duplicate | |
| file names. The file names are converted to lowercase so we can | |
| reliably check for duplicates that only differ in case, which is | |
| important for case-insensitive file systems. | |
| """ | |
| if self._reverseContents is None: | |
| d = {} | |
| for k, v in self.contents.items(): | |
| d[v.lower()] = k | |
| self._reverseContents = d | |
| return self._reverseContents | |
| def writeContents(self): | |
| """ | |
| Write the contents.plist file out to disk. Call this method when | |
| you're done writing glyphs. | |
| """ | |
| self._writePlist(CONTENTS_FILENAME, self.contents) | |
| # layer info | |
| def readLayerInfo(self, info, validateRead=None): | |
| """ | |
| ``validateRead`` will validate the data, by default it is set to the | |
| class's ``validateRead`` value, can be overridden. | |
| """ | |
| if validateRead is None: | |
| validateRead = self._validateRead | |
| infoDict = self._getPlist(LAYERINFO_FILENAME, {}) | |
| if validateRead: | |
| if not isinstance(infoDict, dict): | |
| raise GlifLibError("layerinfo.plist is not properly formatted.") | |
| infoDict = validateLayerInfoVersion3Data(infoDict) | |
| # populate the object | |
| for attr, value in infoDict.items(): | |
| try: | |
| setattr(info, attr, value) | |
| except AttributeError: | |
| raise GlifLibError( | |
| "The supplied layer info object does not support setting a necessary attribute (%s)." | |
| % attr | |
| ) | |
| def writeLayerInfo(self, info, validateWrite=None): | |
| """ | |
| ``validateWrite`` will validate the data, by default it is set to the | |
| class's ``validateWrite`` value, can be overridden. | |
| """ | |
| if validateWrite is None: | |
| validateWrite = self._validateWrite | |
| if self.ufoFormatVersionTuple.major < 3: | |
| raise GlifLibError( | |
| "layerinfo.plist is not allowed in UFO %d." | |
| % self.ufoFormatVersionTuple.major | |
| ) | |
| # gather data | |
| infoData = {} | |
| for attr in layerInfoVersion3ValueData.keys(): | |
| if hasattr(info, attr): | |
| try: | |
| value = getattr(info, attr) | |
| except AttributeError: | |
| raise GlifLibError( | |
| "The supplied info object does not support getting a necessary attribute (%s)." | |
| % attr | |
| ) | |
| if value is None or (attr == "lib" and not value): | |
| continue | |
| infoData[attr] = value | |
| if infoData: | |
| # validate | |
| if validateWrite: | |
| infoData = validateLayerInfoVersion3Data(infoData) | |
| # write file | |
| self._writePlist(LAYERINFO_FILENAME, infoData) | |
| elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME): | |
| # data empty, remove existing file | |
| self.fs.remove(LAYERINFO_FILENAME) | |
| def getGLIF(self, glyphName): | |
| """ | |
| Get the raw GLIF text for a given glyph name. This only works | |
| for GLIF files that are already on disk. | |
| This method is useful in situations when the raw XML needs to be | |
| read from a glyph set for a particular glyph before fully parsing | |
| it into an object structure via the readGlyph method. | |
| Raises KeyError if 'glyphName' is not in contents.plist, or | |
| GlifLibError if the file associated with can't be found. | |
| """ | |
| fileName = self.contents[glyphName] | |
| try: | |
| return self.fs.readbytes(fileName) | |
| except fs.errors.ResourceNotFound: | |
| raise GlifLibError( | |
| "The file '%s' associated with glyph '%s' in contents.plist " | |
| "does not exist on %s" % (fileName, glyphName, self.fs) | |
| ) | |
| def getGLIFModificationTime(self, glyphName): | |
| """ | |
| Returns the modification time for the GLIF file with 'glyphName', as | |
| a floating point number giving the number of seconds since the epoch. | |
| Return None if the associated file does not exist or the underlying | |
| filesystem does not support getting modified times. | |
| Raises KeyError if the glyphName is not in contents.plist. | |
| """ | |
| fileName = self.contents[glyphName] | |
| return self.getFileModificationTime(fileName) | |
| # reading/writing API | |
| def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=None): | |
| """ | |
| Read a .glif file for 'glyphName' from the glyph set. The | |
| 'glyphObject' argument can be any kind of object (even None); | |
| the readGlyph() method will attempt to set the following | |
| attributes on it: | |
| width | |
| the advance width of the glyph | |
| height | |
| the advance height of the glyph | |
| unicodes | |
| a list of unicode values for this glyph | |
| note | |
| a string | |
| lib | |
| a dictionary containing custom data | |
| image | |
| a dictionary containing image data | |
| guidelines | |
| a list of guideline data dictionaries | |
| anchors | |
| a list of anchor data dictionaries | |
| All attributes are optional, in two ways: | |
| 1) An attribute *won't* be set if the .glif file doesn't | |
| contain data for it. 'glyphObject' will have to deal | |
| with default values itself. | |
| 2) If setting the attribute fails with an AttributeError | |
| (for example if the 'glyphObject' attribute is read- | |
| only), readGlyph() will not propagate that exception, | |
| but ignore that attribute. | |
| To retrieve outline information, you need to pass an object | |
| conforming to the PointPen protocol as the 'pointPen' argument. | |
| This argument may be None if you don't need the outline data. | |
| readGlyph() will raise KeyError if the glyph is not present in | |
| the glyph set. | |
| ``validate`` will validate the data, by default it is set to the | |
| class's ``validateRead`` value, can be overridden. | |
| """ | |
| if validate is None: | |
| validate = self._validateRead | |
| text = self.getGLIF(glyphName) | |
| try: | |
| tree = _glifTreeFromString(text) | |
| formatVersions = GLIFFormatVersion.supported_versions( | |
| self.ufoFormatVersionTuple | |
| ) | |
| _readGlyphFromTree( | |
| tree, | |
| glyphObject, | |
| pointPen, | |
| formatVersions=formatVersions, | |
| validate=validate, | |
| ) | |
| except GlifLibError as glifLibError: | |
| # Re-raise with a note that gives extra context, describing where | |
| # the error occurred. | |
| fileName = self.contents[glyphName] | |
| try: | |
| glifLocation = f"'{self.fs.getsyspath(fileName)}'" | |
| except fs.errors.NoSysPath: | |
| # Network or in-memory FS may not map to a local path, so use | |
| # the best string representation we have. | |
| glifLocation = f"'{fileName}' from '{str(self.fs)}'" | |
| glifLibError._add_note( | |
| f"The issue is in glyph '{glyphName}', located in {glifLocation}." | |
| ) | |
| raise | |
| def writeGlyph( | |
| self, | |
| glyphName, | |
| glyphObject=None, | |
| drawPointsFunc=None, | |
| formatVersion=None, | |
| validate=None, | |
| ): | |
| """ | |
| Write a .glif file for 'glyphName' to the glyph set. The | |
| 'glyphObject' argument can be any kind of object (even None); | |
| the writeGlyph() method will attempt to get the following | |
| attributes from it: | |
| width | |
| the advance width of the glyph | |
| height | |
| the advance height of the glyph | |
| unicodes | |
| a list of unicode values for this glyph | |
| note | |
| a string | |
| lib | |
| a dictionary containing custom data | |
| image | |
| a dictionary containing image data | |
| guidelines | |
| a list of guideline data dictionaries | |
| anchors | |
| a list of anchor data dictionaries | |
| All attributes are optional: if 'glyphObject' doesn't | |
| have the attribute, it will simply be skipped. | |
| To write outline data to the .glif file, writeGlyph() needs | |
| a function (any callable object actually) that will take one | |
| argument: an object that conforms to the PointPen protocol. | |
| The function will be called by writeGlyph(); it has to call the | |
| proper PointPen methods to transfer the outline to the .glif file. | |
| The GLIF format version will be chosen based on the ufoFormatVersion | |
| passed during the creation of this object. If a particular format | |
| version is desired, it can be passed with the formatVersion argument. | |
| The formatVersion argument accepts either a tuple of integers for | |
| (major, minor), or a single integer for the major digit only (with | |
| minor digit implied as 0). | |
| An UnsupportedGLIFFormat exception is raised if the requested GLIF | |
| formatVersion is not supported. | |
| ``validate`` will validate the data, by default it is set to the | |
| class's ``validateWrite`` value, can be overridden. | |
| """ | |
| if formatVersion is None: | |
| formatVersion = GLIFFormatVersion.default(self.ufoFormatVersionTuple) | |
| else: | |
| try: | |
| formatVersion = GLIFFormatVersion(formatVersion) | |
| except ValueError as e: | |
| from fontTools.ufoLib.errors import UnsupportedGLIFFormat | |
| raise UnsupportedGLIFFormat( | |
| f"Unsupported GLIF format version: {formatVersion!r}" | |
| ) from e | |
| if formatVersion not in GLIFFormatVersion.supported_versions( | |
| self.ufoFormatVersionTuple | |
| ): | |
| from fontTools.ufoLib.errors import UnsupportedGLIFFormat | |
| raise UnsupportedGLIFFormat( | |
| f"Unsupported GLIF format version ({formatVersion!s}) " | |
| f"for UFO format version {self.ufoFormatVersionTuple!s}." | |
| ) | |
| if validate is None: | |
| validate = self._validateWrite | |
| fileName = self.contents.get(glyphName) | |
| if fileName is None: | |
| if self._existingFileNames is None: | |
| self._existingFileNames = { | |
| fileName.lower() for fileName in self.contents.values() | |
| } | |
| fileName = self.glyphNameToFileName(glyphName, self._existingFileNames) | |
| self.contents[glyphName] = fileName | |
| self._existingFileNames.add(fileName.lower()) | |
| if self._reverseContents is not None: | |
| self._reverseContents[fileName.lower()] = glyphName | |
| data = _writeGlyphToBytes( | |
| glyphName, | |
| glyphObject, | |
| drawPointsFunc, | |
| formatVersion=formatVersion, | |
| validate=validate, | |
| ) | |
| if ( | |
| self._havePreviousFile | |
| and self.fs.exists(fileName) | |
| and data == self.fs.readbytes(fileName) | |
| ): | |
| return | |
| self.fs.writebytes(fileName, data) | |
| def deleteGlyph(self, glyphName): | |
| """Permanently delete the glyph from the glyph set on disk. Will | |
| raise KeyError if the glyph is not present in the glyph set. | |
| """ | |
| fileName = self.contents[glyphName] | |
| self.fs.remove(fileName) | |
| if self._existingFileNames is not None: | |
| self._existingFileNames.remove(fileName.lower()) | |
| if self._reverseContents is not None: | |
| del self._reverseContents[fileName.lower()] | |
| del self.contents[glyphName] | |
| # dict-like support | |
| def keys(self): | |
| return list(self.contents.keys()) | |
| def has_key(self, glyphName): | |
| return glyphName in self.contents | |
| __contains__ = has_key | |
| def __len__(self): | |
| return len(self.contents) | |
| def __getitem__(self, glyphName): | |
| if glyphName not in self.contents: | |
| raise KeyError(glyphName) | |
| return self.glyphClass(glyphName, self) | |
| # quickly fetch unicode values | |
| def getUnicodes(self, glyphNames=None): | |
| """ | |
| Return a dictionary that maps glyph names to lists containing | |
| the unicode value[s] for that glyph, if any. This parses the .glif | |
| files partially, so it is a lot faster than parsing all files completely. | |
| By default this checks all glyphs, but a subset can be passed with glyphNames. | |
| """ | |
| unicodes = {} | |
| if glyphNames is None: | |
| glyphNames = self.contents.keys() | |
| for glyphName in glyphNames: | |
| text = self.getGLIF(glyphName) | |
| unicodes[glyphName] = _fetchUnicodes(text) | |
| return unicodes | |
| def getComponentReferences(self, glyphNames=None): | |
| """ | |
| Return a dictionary that maps glyph names to lists containing the | |
| base glyph name of components in the glyph. This parses the .glif | |
| files partially, so it is a lot faster than parsing all files completely. | |
| By default this checks all glyphs, but a subset can be passed with glyphNames. | |
| """ | |
| components = {} | |
| if glyphNames is None: | |
| glyphNames = self.contents.keys() | |
| for glyphName in glyphNames: | |
| text = self.getGLIF(glyphName) | |
| components[glyphName] = _fetchComponentBases(text) | |
| return components | |
| def getImageReferences(self, glyphNames=None): | |
| """ | |
| Return a dictionary that maps glyph names to the file name of the image | |
| referenced by the glyph. This parses the .glif files partially, so it is a | |
| lot faster than parsing all files completely. | |
| By default this checks all glyphs, but a subset can be passed with glyphNames. | |
| """ | |
| images = {} | |
| if glyphNames is None: | |
| glyphNames = self.contents.keys() | |
| for glyphName in glyphNames: | |
| text = self.getGLIF(glyphName) | |
| images[glyphName] = _fetchImageFileName(text) | |
| return images | |
| def close(self): | |
| if self._shouldClose: | |
| self.fs.close() | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc_value, exc_tb): | |
| self.close() | |
| # ----------------------- | |
| # Glyph Name to File Name | |
| # ----------------------- | |
| def glyphNameToFileName(glyphName, existingFileNames): | |
| """ | |
| Wrapper around the userNameToFileName function in filenames.py | |
| Note that existingFileNames should be a set for large glyphsets | |
| or performance will suffer. | |
| """ | |
| if existingFileNames is None: | |
| existingFileNames = set() | |
| return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif") | |
| # ----------------------- | |
| # GLIF To and From String | |
| # ----------------------- | |
| def readGlyphFromString( | |
| aString, | |
| glyphObject=None, | |
| pointPen=None, | |
| formatVersions=None, | |
| validate=True, | |
| ): | |
| """ | |
| Read .glif data from a string into a glyph object. | |
| The 'glyphObject' argument can be any kind of object (even None); | |
| the readGlyphFromString() method will attempt to set the following | |
| attributes on it: | |
| width | |
| the advance width of the glyph | |
| height | |
| the advance height of the glyph | |
| unicodes | |
| a list of unicode values for this glyph | |
| note | |
| a string | |
| lib | |
| a dictionary containing custom data | |
| image | |
| a dictionary containing image data | |
| guidelines | |
| a list of guideline data dictionaries | |
| anchors | |
| a list of anchor data dictionaries | |
| All attributes are optional, in two ways: | |
| 1) An attribute *won't* be set if the .glif file doesn't | |
| contain data for it. 'glyphObject' will have to deal | |
| with default values itself. | |
| 2) If setting the attribute fails with an AttributeError | |
| (for example if the 'glyphObject' attribute is read- | |
| only), readGlyphFromString() will not propagate that | |
| exception, but ignore that attribute. | |
| To retrieve outline information, you need to pass an object | |
| conforming to the PointPen protocol as the 'pointPen' argument. | |
| This argument may be None if you don't need the outline data. | |
| The formatVersions optional argument define the GLIF format versions | |
| that are allowed to be read. | |
| The type is Optional[Iterable[Tuple[int, int], int]]. It can contain | |
| either integers (for the major versions to be allowed, with minor | |
| digits defaulting to 0), or tuples of integers to specify both | |
| (major, minor) versions. | |
| By default when formatVersions is None all the GLIF format versions | |
| currently defined are allowed to be read. | |
| ``validate`` will validate the read data. It is set to ``True`` by default. | |
| """ | |
| tree = _glifTreeFromString(aString) | |
| if formatVersions is None: | |
| validFormatVersions = GLIFFormatVersion.supported_versions() | |
| else: | |
| validFormatVersions, invalidFormatVersions = set(), set() | |
| for v in formatVersions: | |
| try: | |
| formatVersion = GLIFFormatVersion(v) | |
| except ValueError: | |
| invalidFormatVersions.add(v) | |
| else: | |
| validFormatVersions.add(formatVersion) | |
| if not validFormatVersions: | |
| raise ValueError( | |
| "None of the requested GLIF formatVersions are supported: " | |
| f"{formatVersions!r}" | |
| ) | |
| _readGlyphFromTree( | |
| tree, | |
| glyphObject, | |
| pointPen, | |
| formatVersions=validFormatVersions, | |
| validate=validate, | |
| ) | |
| def _writeGlyphToBytes( | |
| glyphName, | |
| glyphObject=None, | |
| drawPointsFunc=None, | |
| writer=None, | |
| formatVersion=None, | |
| validate=True, | |
| ): | |
| """Return .glif data for a glyph as a UTF-8 encoded bytes string.""" | |
| try: | |
| formatVersion = GLIFFormatVersion(formatVersion) | |
| except ValueError: | |
| from fontTools.ufoLib.errors import UnsupportedGLIFFormat | |
| raise UnsupportedGLIFFormat( | |
| "Unsupported GLIF format version: {formatVersion!r}" | |
| ) | |
| # start | |
| if validate and not isinstance(glyphName, str): | |
| raise GlifLibError("The glyph name is not properly formatted.") | |
| if validate and len(glyphName) == 0: | |
| raise GlifLibError("The glyph name is empty.") | |
| glyphAttrs = OrderedDict( | |
| [("name", glyphName), ("format", repr(formatVersion.major))] | |
| ) | |
| if formatVersion.minor != 0: | |
| glyphAttrs["formatMinor"] = repr(formatVersion.minor) | |
| root = etree.Element("glyph", glyphAttrs) | |
| identifiers = set() | |
| # advance | |
| _writeAdvance(glyphObject, root, validate) | |
| # unicodes | |
| if getattr(glyphObject, "unicodes", None): | |
| _writeUnicodes(glyphObject, root, validate) | |
| # note | |
| if getattr(glyphObject, "note", None): | |
| _writeNote(glyphObject, root, validate) | |
| # image | |
| if formatVersion.major >= 2 and getattr(glyphObject, "image", None): | |
| _writeImage(glyphObject, root, validate) | |
| # guidelines | |
| if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None): | |
| _writeGuidelines(glyphObject, root, identifiers, validate) | |
| # anchors | |
| anchors = getattr(glyphObject, "anchors", None) | |
| if formatVersion.major >= 2 and anchors: | |
| _writeAnchors(glyphObject, root, identifiers, validate) | |
| # outline | |
| if drawPointsFunc is not None: | |
| outline = etree.SubElement(root, "outline") | |
| pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate) | |
| drawPointsFunc(pen) | |
| if formatVersion.major == 1 and anchors: | |
| _writeAnchorsFormat1(pen, anchors, validate) | |
| # prevent lxml from writing self-closing tags | |
| if not len(outline): | |
| outline.text = "\n " | |
| # lib | |
| if getattr(glyphObject, "lib", None): | |
| _writeLib(glyphObject, root, validate) | |
| # return the text | |
| data = etree.tostring( | |
| root, encoding="UTF-8", xml_declaration=True, pretty_print=True | |
| ) | |
| return data | |
| def writeGlyphToString( | |
| glyphName, | |
| glyphObject=None, | |
| drawPointsFunc=None, | |
| formatVersion=None, | |
| validate=True, | |
| ): | |
| """ | |
| Return .glif data for a glyph as a string. The XML declaration's | |
| encoding is always set to "UTF-8". | |
| The 'glyphObject' argument can be any kind of object (even None); | |
| the writeGlyphToString() method will attempt to get the following | |
| attributes from it: | |
| width | |
| the advance width of the glyph | |
| height | |
| the advance height of the glyph | |
| unicodes | |
| a list of unicode values for this glyph | |
| note | |
| a string | |
| lib | |
| a dictionary containing custom data | |
| image | |
| a dictionary containing image data | |
| guidelines | |
| a list of guideline data dictionaries | |
| anchors | |
| a list of anchor data dictionaries | |
| All attributes are optional: if 'glyphObject' doesn't | |
| have the attribute, it will simply be skipped. | |
| To write outline data to the .glif file, writeGlyphToString() needs | |
| a function (any callable object actually) that will take one | |
| argument: an object that conforms to the PointPen protocol. | |
| The function will be called by writeGlyphToString(); it has to call the | |
| proper PointPen methods to transfer the outline to the .glif file. | |
| The GLIF format version can be specified with the formatVersion argument. | |
| This accepts either a tuple of integers for (major, minor), or a single | |
| integer for the major digit only (with minor digit implied as 0). | |
| By default when formatVesion is None the latest GLIF format version will | |
| be used; currently it's 2.0, which is equivalent to formatVersion=(2, 0). | |
| An UnsupportedGLIFFormat exception is raised if the requested UFO | |
| formatVersion is not supported. | |
| ``validate`` will validate the written data. It is set to ``True`` by default. | |
| """ | |
| data = _writeGlyphToBytes( | |
| glyphName, | |
| glyphObject=glyphObject, | |
| drawPointsFunc=drawPointsFunc, | |
| formatVersion=formatVersion, | |
| validate=validate, | |
| ) | |
| return data.decode("utf-8") | |
| def _writeAdvance(glyphObject, element, validate): | |
| width = getattr(glyphObject, "width", None) | |
| if width is not None: | |
| if validate and not isinstance(width, numberTypes): | |
| raise GlifLibError("width attribute must be int or float") | |
| if width == 0: | |
| width = None | |
| height = getattr(glyphObject, "height", None) | |
| if height is not None: | |
| if validate and not isinstance(height, numberTypes): | |
| raise GlifLibError("height attribute must be int or float") | |
| if height == 0: | |
| height = None | |
| if width is not None and height is not None: | |
| etree.SubElement( | |
| element, | |
| "advance", | |
| OrderedDict([("height", repr(height)), ("width", repr(width))]), | |
| ) | |
| elif width is not None: | |
| etree.SubElement(element, "advance", dict(width=repr(width))) | |
| elif height is not None: | |
| etree.SubElement(element, "advance", dict(height=repr(height))) | |
| def _writeUnicodes(glyphObject, element, validate): | |
| unicodes = getattr(glyphObject, "unicodes", None) | |
| if validate and isinstance(unicodes, int): | |
| unicodes = [unicodes] | |
| seen = set() | |
| for code in unicodes: | |
| if validate and not isinstance(code, int): | |
| raise GlifLibError("unicode values must be int") | |
| if code in seen: | |
| continue | |
| seen.add(code) | |
| hexCode = "%04X" % code | |
| etree.SubElement(element, "unicode", dict(hex=hexCode)) | |
| def _writeNote(glyphObject, element, validate): | |
| note = getattr(glyphObject, "note", None) | |
| if validate and not isinstance(note, str): | |
| raise GlifLibError("note attribute must be str") | |
| note = note.strip() | |
| note = "\n" + note + "\n" | |
| etree.SubElement(element, "note").text = note | |
| def _writeImage(glyphObject, element, validate): | |
| image = getattr(glyphObject, "image", None) | |
| if validate and not imageValidator(image): | |
| raise GlifLibError( | |
| "image attribute must be a dict or dict-like object with the proper structure." | |
| ) | |
| attrs = OrderedDict([("fileName", image["fileName"])]) | |
| for attr, default in _transformationInfo: | |
| value = image.get(attr, default) | |
| if value != default: | |
| attrs[attr] = repr(value) | |
| color = image.get("color") | |
| if color is not None: | |
| attrs["color"] = color | |
| etree.SubElement(element, "image", attrs) | |
| def _writeGuidelines(glyphObject, element, identifiers, validate): | |
| guidelines = getattr(glyphObject, "guidelines", []) | |
| if validate and not guidelinesValidator(guidelines): | |
| raise GlifLibError("guidelines attribute does not have the proper structure.") | |
| for guideline in guidelines: | |
| attrs = OrderedDict() | |
| x = guideline.get("x") | |
| if x is not None: | |
| attrs["x"] = repr(x) | |
| y = guideline.get("y") | |
| if y is not None: | |
| attrs["y"] = repr(y) | |
| angle = guideline.get("angle") | |
| if angle is not None: | |
| attrs["angle"] = repr(angle) | |
| name = guideline.get("name") | |
| if name is not None: | |
| attrs["name"] = name | |
| color = guideline.get("color") | |
| if color is not None: | |
| attrs["color"] = color | |
| identifier = guideline.get("identifier") | |
| if identifier is not None: | |
| if validate and identifier in identifiers: | |
| raise GlifLibError("identifier used more than once: %s" % identifier) | |
| attrs["identifier"] = identifier | |
| identifiers.add(identifier) | |
| etree.SubElement(element, "guideline", attrs) | |
| def _writeAnchorsFormat1(pen, anchors, validate): | |
| if validate and not anchorsValidator(anchors): | |
| raise GlifLibError("anchors attribute does not have the proper structure.") | |
| for anchor in anchors: | |
| attrs = {} | |
| x = anchor["x"] | |
| attrs["x"] = repr(x) | |
| y = anchor["y"] | |
| attrs["y"] = repr(y) | |
| name = anchor.get("name") | |
| if name is not None: | |
| attrs["name"] = name | |
| pen.beginPath() | |
| pen.addPoint((x, y), segmentType="move", name=name) | |
| pen.endPath() | |
| def _writeAnchors(glyphObject, element, identifiers, validate): | |
| anchors = getattr(glyphObject, "anchors", []) | |
| if validate and not anchorsValidator(anchors): | |
| raise GlifLibError("anchors attribute does not have the proper structure.") | |
| for anchor in anchors: | |
| attrs = OrderedDict() | |
| x = anchor["x"] | |
| attrs["x"] = repr(x) | |
| y = anchor["y"] | |
| attrs["y"] = repr(y) | |
| name = anchor.get("name") | |
| if name is not None: | |
| attrs["name"] = name | |
| color = anchor.get("color") | |
| if color is not None: | |
| attrs["color"] = color | |
| identifier = anchor.get("identifier") | |
| if identifier is not None: | |
| if validate and identifier in identifiers: | |
| raise GlifLibError("identifier used more than once: %s" % identifier) | |
| attrs["identifier"] = identifier | |
| identifiers.add(identifier) | |
| etree.SubElement(element, "anchor", attrs) | |
| def _writeLib(glyphObject, element, validate): | |
| lib = getattr(glyphObject, "lib", None) | |
| if not lib: | |
| # don't write empty lib | |
| return | |
| if validate: | |
| valid, message = glyphLibValidator(lib) | |
| if not valid: | |
| raise GlifLibError(message) | |
| if not isinstance(lib, dict): | |
| lib = dict(lib) | |
| # plist inside GLIF begins with 2 levels of indentation | |
| e = plistlib.totree(lib, indent_level=2) | |
| etree.SubElement(element, "lib").append(e) | |
| # ----------------------- | |
| # layerinfo.plist Support | |
| # ----------------------- | |
| layerInfoVersion3ValueData = { | |
| "color": dict(type=str, valueValidator=colorValidator), | |
| "lib": dict(type=dict, valueValidator=genericTypeValidator), | |
| } | |
| def validateLayerInfoVersion3ValueForAttribute(attr, value): | |
| """ | |
| This performs very basic validation of the value for attribute | |
| following the UFO 3 fontinfo.plist specification. The results | |
| of this should not be interpretted as *correct* for the font | |
| that they are part of. This merely indicates that the value | |
| is of the proper type and, where the specification defines | |
| a set range of possible values for an attribute, that the | |
| value is in the accepted range. | |
| """ | |
| if attr not in layerInfoVersion3ValueData: | |
| return False | |
| dataValidationDict = layerInfoVersion3ValueData[attr] | |
| valueType = dataValidationDict.get("type") | |
| validator = dataValidationDict.get("valueValidator") | |
| valueOptions = dataValidationDict.get("valueOptions") | |
| # have specific options for the validator | |
| if valueOptions is not None: | |
| isValidValue = validator(value, valueOptions) | |
| # no specific options | |
| else: | |
| if validator == genericTypeValidator: | |
| isValidValue = validator(value, valueType) | |
| else: | |
| isValidValue = validator(value) | |
| return isValidValue | |
| def validateLayerInfoVersion3Data(infoData): | |
| """ | |
| This performs very basic validation of the value for infoData | |
| following the UFO 3 layerinfo.plist specification. The results | |
| of this should not be interpretted as *correct* for the font | |
| that they are part of. This merely indicates that the values | |
| are of the proper type and, where the specification defines | |
| a set range of possible values for an attribute, that the | |
| value is in the accepted range. | |
| """ | |
| for attr, value in infoData.items(): | |
| if attr not in layerInfoVersion3ValueData: | |
| raise GlifLibError("Unknown attribute %s." % attr) | |
| isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value) | |
| if not isValidValue: | |
| raise GlifLibError(f"Invalid value for attribute {attr} ({value!r}).") | |
| return infoData | |
| # ----------------- | |
| # GLIF Tree Support | |
| # ----------------- | |
| def _glifTreeFromFile(aFile): | |
| if etree._have_lxml: | |
| tree = etree.parse(aFile, parser=etree.XMLParser(remove_comments=True)) | |
| else: | |
| tree = etree.parse(aFile) | |
| root = tree.getroot() | |
| if root.tag != "glyph": | |
| raise GlifLibError("The GLIF is not properly formatted.") | |
| if root.text and root.text.strip() != "": | |
| raise GlifLibError("Invalid GLIF structure.") | |
| return root | |
| def _glifTreeFromString(aString): | |
| data = tobytes(aString, encoding="utf-8") | |
| try: | |
| if etree._have_lxml: | |
| root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True)) | |
| else: | |
| root = etree.fromstring(data) | |
| except Exception as etree_exception: | |
| raise GlifLibError("GLIF contains invalid XML.") from etree_exception | |
| if root.tag != "glyph": | |
| raise GlifLibError("The GLIF is not properly formatted.") | |
| if root.text and root.text.strip() != "": | |
| raise GlifLibError("Invalid GLIF structure.") | |
| return root | |
| def _readGlyphFromTree( | |
| tree, | |
| glyphObject=None, | |
| pointPen=None, | |
| formatVersions=GLIFFormatVersion.supported_versions(), | |
| validate=True, | |
| ): | |
| # check the format version | |
| formatVersionMajor = tree.get("format") | |
| if validate and formatVersionMajor is None: | |
| raise GlifLibError("Unspecified format version in GLIF.") | |
| formatVersionMinor = tree.get("formatMinor", 0) | |
| try: | |
| formatVersion = GLIFFormatVersion( | |
| (int(formatVersionMajor), int(formatVersionMinor)) | |
| ) | |
| except ValueError as e: | |
| msg = "Unsupported GLIF format: %s.%s" % ( | |
| formatVersionMajor, | |
| formatVersionMinor, | |
| ) | |
| if validate: | |
| from fontTools.ufoLib.errors import UnsupportedGLIFFormat | |
| raise UnsupportedGLIFFormat(msg) from e | |
| # warn but continue using the latest supported format | |
| formatVersion = GLIFFormatVersion.default() | |
| logger.warning( | |
| "%s. Assuming the latest supported version (%s). " | |
| "Some data may be skipped or parsed incorrectly.", | |
| msg, | |
| formatVersion, | |
| ) | |
| if validate and formatVersion not in formatVersions: | |
| raise GlifLibError(f"Forbidden GLIF format version: {formatVersion!s}") | |
| try: | |
| readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[formatVersion] | |
| except KeyError: | |
| raise NotImplementedError(formatVersion) | |
| readGlyphFromTree( | |
| tree=tree, | |
| glyphObject=glyphObject, | |
| pointPen=pointPen, | |
| validate=validate, | |
| formatMinor=formatVersion.minor, | |
| ) | |
| def _readGlyphFromTreeFormat1( | |
| tree, glyphObject=None, pointPen=None, validate=None, **kwargs | |
| ): | |
| # get the name | |
| _readName(glyphObject, tree, validate) | |
| # populate the sub elements | |
| unicodes = [] | |
| haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False | |
| for element in tree: | |
| if element.tag == "outline": | |
| if validate: | |
| if haveSeenOutline: | |
| raise GlifLibError("The outline element occurs more than once.") | |
| if element.attrib: | |
| raise GlifLibError( | |
| "The outline element contains unknown attributes." | |
| ) | |
| if element.text and element.text.strip() != "": | |
| raise GlifLibError("Invalid outline structure.") | |
| haveSeenOutline = True | |
| buildOutlineFormat1(glyphObject, pointPen, element, validate) | |
| elif glyphObject is None: | |
| continue | |
| elif element.tag == "advance": | |
| if validate and haveSeenAdvance: | |
| raise GlifLibError("The advance element occurs more than once.") | |
| haveSeenAdvance = True | |
| _readAdvance(glyphObject, element) | |
| elif element.tag == "unicode": | |
| v = element.get("hex") | |
| if v is None: | |
| raise GlifLibError( | |
| "A unicode element is missing its required hex attribute." | |
| ) | |
| try: | |
| v = int(v, 16) | |
| if v not in unicodes: | |
| unicodes.append(v) | |
| except ValueError: | |
| raise GlifLibError( | |
| "Illegal value for hex attribute of unicode element." | |
| ) | |
| elif element.tag == "note": | |
| if validate and haveSeenNote: | |
| raise GlifLibError("The note element occurs more than once.") | |
| haveSeenNote = True | |
| _readNote(glyphObject, element) | |
| elif element.tag == "lib": | |
| if validate and haveSeenLib: | |
| raise GlifLibError("The lib element occurs more than once.") | |
| haveSeenLib = True | |
| _readLib(glyphObject, element, validate) | |
| else: | |
| raise GlifLibError("Unknown element in GLIF: %s" % element) | |
| # set the collected unicodes | |
| if unicodes: | |
| _relaxedSetattr(glyphObject, "unicodes", unicodes) | |
| def _readGlyphFromTreeFormat2( | |
| tree, glyphObject=None, pointPen=None, validate=None, formatMinor=0 | |
| ): | |
| # get the name | |
| _readName(glyphObject, tree, validate) | |
| # populate the sub elements | |
| unicodes = [] | |
| guidelines = [] | |
| anchors = [] | |
| haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = ( | |
| False | |
| ) | |
| identifiers = set() | |
| for element in tree: | |
| if element.tag == "outline": | |
| if validate: | |
| if haveSeenOutline: | |
| raise GlifLibError("The outline element occurs more than once.") | |
| if element.attrib: | |
| raise GlifLibError( | |
| "The outline element contains unknown attributes." | |
| ) | |
| if element.text and element.text.strip() != "": | |
| raise GlifLibError("Invalid outline structure.") | |
| haveSeenOutline = True | |
| if pointPen is not None: | |
| buildOutlineFormat2( | |
| glyphObject, pointPen, element, identifiers, validate | |
| ) | |
| elif glyphObject is None: | |
| continue | |
| elif element.tag == "advance": | |
| if validate and haveSeenAdvance: | |
| raise GlifLibError("The advance element occurs more than once.") | |
| haveSeenAdvance = True | |
| _readAdvance(glyphObject, element) | |
| elif element.tag == "unicode": | |
| v = element.get("hex") | |
| if v is None: | |
| raise GlifLibError( | |
| "A unicode element is missing its required hex attribute." | |
| ) | |
| try: | |
| v = int(v, 16) | |
| if v not in unicodes: | |
| unicodes.append(v) | |
| except ValueError: | |
| raise GlifLibError( | |
| "Illegal value for hex attribute of unicode element." | |
| ) | |
| elif element.tag == "guideline": | |
| if validate and len(element): | |
| raise GlifLibError("Unknown children in guideline element.") | |
| attrib = dict(element.attrib) | |
| for attr in ("x", "y", "angle"): | |
| if attr in attrib: | |
| attrib[attr] = _number(attrib[attr]) | |
| guidelines.append(attrib) | |
| elif element.tag == "anchor": | |
| if validate and len(element): | |
| raise GlifLibError("Unknown children in anchor element.") | |
| attrib = dict(element.attrib) | |
| for attr in ("x", "y"): | |
| if attr in element.attrib: | |
| attrib[attr] = _number(attrib[attr]) | |
| anchors.append(attrib) | |
| elif element.tag == "image": | |
| if validate: | |
| if haveSeenImage: | |
| raise GlifLibError("The image element occurs more than once.") | |
| if len(element): | |
| raise GlifLibError("Unknown children in image element.") | |
| haveSeenImage = True | |
| _readImage(glyphObject, element, validate) | |
| elif element.tag == "note": | |
| if validate and haveSeenNote: | |
| raise GlifLibError("The note element occurs more than once.") | |
| haveSeenNote = True | |
| _readNote(glyphObject, element) | |
| elif element.tag == "lib": | |
| if validate and haveSeenLib: | |
| raise GlifLibError("The lib element occurs more than once.") | |
| haveSeenLib = True | |
| _readLib(glyphObject, element, validate) | |
| else: | |
| raise GlifLibError("Unknown element in GLIF: %s" % element) | |
| # set the collected unicodes | |
| if unicodes: | |
| _relaxedSetattr(glyphObject, "unicodes", unicodes) | |
| # set the collected guidelines | |
| if guidelines: | |
| if validate and not guidelinesValidator(guidelines, identifiers): | |
| raise GlifLibError("The guidelines are improperly formatted.") | |
| _relaxedSetattr(glyphObject, "guidelines", guidelines) | |
| # set the collected anchors | |
| if anchors: | |
| if validate and not anchorsValidator(anchors, identifiers): | |
| raise GlifLibError("The anchors are improperly formatted.") | |
| _relaxedSetattr(glyphObject, "anchors", anchors) | |
| _READ_GLYPH_FROM_TREE_FUNCS = { | |
| GLIFFormatVersion.FORMAT_1_0: _readGlyphFromTreeFormat1, | |
| GLIFFormatVersion.FORMAT_2_0: _readGlyphFromTreeFormat2, | |
| } | |
| def _readName(glyphObject, root, validate): | |
| glyphName = root.get("name") | |
| if validate and not glyphName: | |
| raise GlifLibError("Empty glyph name in GLIF.") | |
| if glyphName and glyphObject is not None: | |
| _relaxedSetattr(glyphObject, "name", glyphName) | |
| def _readAdvance(glyphObject, advance): | |
| width = _number(advance.get("width", 0)) | |
| _relaxedSetattr(glyphObject, "width", width) | |
| height = _number(advance.get("height", 0)) | |
| _relaxedSetattr(glyphObject, "height", height) | |
| def _readNote(glyphObject, note): | |
| lines = note.text.split("\n") | |
| note = "\n".join(line.strip() for line in lines if line.strip()) | |
| _relaxedSetattr(glyphObject, "note", note) | |
| def _readLib(glyphObject, lib, validate): | |
| assert len(lib) == 1 | |
| child = lib[0] | |
| plist = plistlib.fromtree(child) | |
| if validate: | |
| valid, message = glyphLibValidator(plist) | |
| if not valid: | |
| raise GlifLibError(message) | |
| _relaxedSetattr(glyphObject, "lib", plist) | |
| def _readImage(glyphObject, image, validate): | |
| imageData = dict(image.attrib) | |
| for attr, default in _transformationInfo: | |
| value = imageData.get(attr, default) | |
| imageData[attr] = _number(value) | |
| if validate and not imageValidator(imageData): | |
| raise GlifLibError("The image element is not properly formatted.") | |
| _relaxedSetattr(glyphObject, "image", imageData) | |
| # ---------------- | |
| # GLIF to PointPen | |
| # ---------------- | |
| contourAttributesFormat2 = {"identifier"} | |
| componentAttributesFormat1 = { | |
| "base", | |
| "xScale", | |
| "xyScale", | |
| "yxScale", | |
| "yScale", | |
| "xOffset", | |
| "yOffset", | |
| } | |
| componentAttributesFormat2 = componentAttributesFormat1 | {"identifier"} | |
| pointAttributesFormat1 = {"x", "y", "type", "smooth", "name"} | |
| pointAttributesFormat2 = pointAttributesFormat1 | {"identifier"} | |
| pointSmoothOptions = {"no", "yes"} | |
| pointTypeOptions = {"move", "line", "offcurve", "curve", "qcurve"} | |
| # format 1 | |
| def buildOutlineFormat1(glyphObject, pen, outline, validate): | |
| anchors = [] | |
| for element in outline: | |
| if element.tag == "contour": | |
| if len(element) == 1: | |
| point = element[0] | |
| if point.tag == "point": | |
| anchor = _buildAnchorFormat1(point, validate) | |
| if anchor is not None: | |
| anchors.append(anchor) | |
| continue | |
| if pen is not None: | |
| _buildOutlineContourFormat1(pen, element, validate) | |
| elif element.tag == "component": | |
| if pen is not None: | |
| _buildOutlineComponentFormat1(pen, element, validate) | |
| else: | |
| raise GlifLibError("Unknown element in outline element: %s" % element) | |
| if glyphObject is not None and anchors: | |
| if validate and not anchorsValidator(anchors): | |
| raise GlifLibError("GLIF 1 anchors are not properly formatted.") | |
| _relaxedSetattr(glyphObject, "anchors", anchors) | |
| def _buildAnchorFormat1(point, validate): | |
| if point.get("type") != "move": | |
| return None | |
| name = point.get("name") | |
| if name is None: | |
| return None | |
| x = point.get("x") | |
| y = point.get("y") | |
| if validate and x is None: | |
| raise GlifLibError("Required x attribute is missing in point element.") | |
| if validate and y is None: | |
| raise GlifLibError("Required y attribute is missing in point element.") | |
| x = _number(x) | |
| y = _number(y) | |
| anchor = dict(x=x, y=y, name=name) | |
| return anchor | |
| def _buildOutlineContourFormat1(pen, contour, validate): | |
| if validate and contour.attrib: | |
| raise GlifLibError("Unknown attributes in contour element.") | |
| pen.beginPath() | |
| if len(contour): | |
| massaged = _validateAndMassagePointStructures( | |
| contour, | |
| pointAttributesFormat1, | |
| openContourOffCurveLeniency=True, | |
| validate=validate, | |
| ) | |
| _buildOutlinePointsFormat1(pen, massaged) | |
| pen.endPath() | |
| def _buildOutlinePointsFormat1(pen, contour): | |
| for point in contour: | |
| x = point["x"] | |
| y = point["y"] | |
| segmentType = point["segmentType"] | |
| smooth = point["smooth"] | |
| name = point["name"] | |
| pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name) | |
| def _buildOutlineComponentFormat1(pen, component, validate): | |
| if validate: | |
| if len(component): | |
| raise GlifLibError("Unknown child elements of component element.") | |
| for attr in component.attrib.keys(): | |
| if attr not in componentAttributesFormat1: | |
| raise GlifLibError("Unknown attribute in component element: %s" % attr) | |
| baseGlyphName = component.get("base") | |
| if validate and baseGlyphName is None: | |
| raise GlifLibError("The base attribute is not defined in the component.") | |
| transformation = [] | |
| for attr, default in _transformationInfo: | |
| value = component.get(attr) | |
| if value is None: | |
| value = default | |
| else: | |
| value = _number(value) | |
| transformation.append(value) | |
| pen.addComponent(baseGlyphName, tuple(transformation)) | |
| # format 2 | |
| def buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate): | |
| for element in outline: | |
| if element.tag == "contour": | |
| _buildOutlineContourFormat2(pen, element, identifiers, validate) | |
| elif element.tag == "component": | |
| _buildOutlineComponentFormat2(pen, element, identifiers, validate) | |
| else: | |
| raise GlifLibError("Unknown element in outline element: %s" % element.tag) | |
| def _buildOutlineContourFormat2(pen, contour, identifiers, validate): | |
| if validate: | |
| for attr in contour.attrib.keys(): | |
| if attr not in contourAttributesFormat2: | |
| raise GlifLibError("Unknown attribute in contour element: %s" % attr) | |
| identifier = contour.get("identifier") | |
| if identifier is not None: | |
| if validate: | |
| if identifier in identifiers: | |
| raise GlifLibError( | |
| "The identifier %s is used more than once." % identifier | |
| ) | |
| if not identifierValidator(identifier): | |
| raise GlifLibError( | |
| "The contour identifier %s is not valid." % identifier | |
| ) | |
| identifiers.add(identifier) | |
| try: | |
| pen.beginPath(identifier=identifier) | |
| except TypeError: | |
| pen.beginPath() | |
| warn( | |
| "The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.", | |
| DeprecationWarning, | |
| ) | |
| if len(contour): | |
| massaged = _validateAndMassagePointStructures( | |
| contour, pointAttributesFormat2, validate=validate | |
| ) | |
| _buildOutlinePointsFormat2(pen, massaged, identifiers, validate) | |
| pen.endPath() | |
| def _buildOutlinePointsFormat2(pen, contour, identifiers, validate): | |
| for point in contour: | |
| x = point["x"] | |
| y = point["y"] | |
| segmentType = point["segmentType"] | |
| smooth = point["smooth"] | |
| name = point["name"] | |
| identifier = point.get("identifier") | |
| if identifier is not None: | |
| if validate: | |
| if identifier in identifiers: | |
| raise GlifLibError( | |
| "The identifier %s is used more than once." % identifier | |
| ) | |
| if not identifierValidator(identifier): | |
| raise GlifLibError("The identifier %s is not valid." % identifier) | |
| identifiers.add(identifier) | |
| try: | |
| pen.addPoint( | |
| (x, y), | |
| segmentType=segmentType, | |
| smooth=smooth, | |
| name=name, | |
| identifier=identifier, | |
| ) | |
| except TypeError: | |
| pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name) | |
| warn( | |
| "The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.", | |
| DeprecationWarning, | |
| ) | |
| def _buildOutlineComponentFormat2(pen, component, identifiers, validate): | |
| if validate: | |
| if len(component): | |
| raise GlifLibError("Unknown child elements of component element.") | |
| for attr in component.attrib.keys(): | |
| if attr not in componentAttributesFormat2: | |
| raise GlifLibError("Unknown attribute in component element: %s" % attr) | |
| baseGlyphName = component.get("base") | |
| if validate and baseGlyphName is None: | |
| raise GlifLibError("The base attribute is not defined in the component.") | |
| transformation = [] | |
| for attr, default in _transformationInfo: | |
| value = component.get(attr) | |
| if value is None: | |
| value = default | |
| else: | |
| value = _number(value) | |
| transformation.append(value) | |
| identifier = component.get("identifier") | |
| if identifier is not None: | |
| if validate: | |
| if identifier in identifiers: | |
| raise GlifLibError( | |
| "The identifier %s is used more than once." % identifier | |
| ) | |
| if validate and not identifierValidator(identifier): | |
| raise GlifLibError("The identifier %s is not valid." % identifier) | |
| identifiers.add(identifier) | |
| try: | |
| pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier) | |
| except TypeError: | |
| pen.addComponent(baseGlyphName, tuple(transformation)) | |
| warn( | |
| "The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.", | |
| DeprecationWarning, | |
| ) | |
| # all formats | |
| def _validateAndMassagePointStructures( | |
| contour, pointAttributes, openContourOffCurveLeniency=False, validate=True | |
| ): | |
| if not len(contour): | |
| return | |
| # store some data for later validation | |
| lastOnCurvePoint = None | |
| haveOffCurvePoint = False | |
| # validate and massage the individual point elements | |
| massaged = [] | |
| for index, element in enumerate(contour): | |
| # not <point> | |
| if element.tag != "point": | |
| raise GlifLibError( | |
| "Unknown child element (%s) of contour element." % element.tag | |
| ) | |
| point = dict(element.attrib) | |
| massaged.append(point) | |
| if validate: | |
| # unknown attributes | |
| for attr in point.keys(): | |
| if attr not in pointAttributes: | |
| raise GlifLibError("Unknown attribute in point element: %s" % attr) | |
| # search for unknown children | |
| if len(element): | |
| raise GlifLibError("Unknown child elements in point element.") | |
| # x and y are required | |
| for attr in ("x", "y"): | |
| try: | |
| point[attr] = _number(point[attr]) | |
| except KeyError as e: | |
| raise GlifLibError( | |
| f"Required {attr} attribute is missing in point element." | |
| ) from e | |
| # segment type | |
| pointType = point.pop("type", "offcurve") | |
| if validate and pointType not in pointTypeOptions: | |
| raise GlifLibError("Unknown point type: %s" % pointType) | |
| if pointType == "offcurve": | |
| pointType = None | |
| point["segmentType"] = pointType | |
| if pointType is None: | |
| haveOffCurvePoint = True | |
| else: | |
| lastOnCurvePoint = index | |
| # move can only occur as the first point | |
| if validate and pointType == "move" and index != 0: | |
| raise GlifLibError( | |
| "A move point occurs after the first point in the contour." | |
| ) | |
| # smooth is optional | |
| smooth = point.get("smooth", "no") | |
| if validate and smooth is not None: | |
| if smooth not in pointSmoothOptions: | |
| raise GlifLibError("Unknown point smooth value: %s" % smooth) | |
| smooth = smooth == "yes" | |
| point["smooth"] = smooth | |
| # smooth can only be applied to curve and qcurve | |
| if validate and smooth and pointType is None: | |
| raise GlifLibError("smooth attribute set in an offcurve point.") | |
| # name is optional | |
| if "name" not in element.attrib: | |
| point["name"] = None | |
| if openContourOffCurveLeniency: | |
| # remove offcurves that precede a move. this is technically illegal, | |
| # but we let it slide because there are fonts out there in the wild like this. | |
| if massaged[0]["segmentType"] == "move": | |
| count = 0 | |
| for point in reversed(massaged): | |
| if point["segmentType"] is None: | |
| count += 1 | |
| else: | |
| break | |
| if count: | |
| massaged = massaged[:-count] | |
| # validate the off-curves in the segments | |
| if validate and haveOffCurvePoint and lastOnCurvePoint is not None: | |
| # we only care about how many offCurves there are before an onCurve | |
| # filter out the trailing offCurves | |
| offCurvesCount = len(massaged) - 1 - lastOnCurvePoint | |
| for point in massaged: | |
| segmentType = point["segmentType"] | |
| if segmentType is None: | |
| offCurvesCount += 1 | |
| else: | |
| if offCurvesCount: | |
| # move and line can't be preceded by off-curves | |
| if segmentType == "move": | |
| # this will have been filtered out already | |
| raise GlifLibError("move can not have an offcurve.") | |
| elif segmentType == "line": | |
| raise GlifLibError("line can not have an offcurve.") | |
| elif segmentType == "curve": | |
| if offCurvesCount > 2: | |
| raise GlifLibError("Too many offcurves defined for curve.") | |
| elif segmentType == "qcurve": | |
| pass | |
| else: | |
| # unknown segment type. it'll be caught later. | |
| pass | |
| offCurvesCount = 0 | |
| return massaged | |
| # --------------------- | |
| # Misc Helper Functions | |
| # --------------------- | |
| def _relaxedSetattr(object, attr, value): | |
| try: | |
| setattr(object, attr, value) | |
| except AttributeError: | |
| pass | |
| def _number(s): | |
| """ | |
| Given a numeric string, return an integer or a float, whichever | |
| the string indicates. _number("1") will return the integer 1, | |
| _number("1.0") will return the float 1.0. | |
| >>> _number("1") | |
| 1 | |
| >>> _number("1.0") | |
| 1.0 | |
| >>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL | |
| Traceback (most recent call last): | |
| ... | |
| GlifLibError: Could not convert a to an int or float. | |
| """ | |
| try: | |
| n = int(s) | |
| return n | |
| except ValueError: | |
| pass | |
| try: | |
| n = float(s) | |
| return n | |
| except ValueError: | |
| raise GlifLibError("Could not convert %s to an int or float." % s) | |
| # -------------------- | |
| # Rapid Value Fetching | |
| # -------------------- | |
| # base | |
| class _DoneParsing(Exception): | |
| pass | |
| class _BaseParser: | |
| def __init__(self): | |
| self._elementStack = [] | |
| def parse(self, text): | |
| from xml.parsers.expat import ParserCreate | |
| parser = ParserCreate() | |
| parser.StartElementHandler = self.startElementHandler | |
| parser.EndElementHandler = self.endElementHandler | |
| parser.Parse(text, 1) | |
| def startElementHandler(self, name, attrs): | |
| self._elementStack.append(name) | |
| def endElementHandler(self, name): | |
| other = self._elementStack.pop(-1) | |
| assert other == name | |
| # unicodes | |
| def _fetchUnicodes(glif): | |
| """ | |
| Get a list of unicodes listed in glif. | |
| """ | |
| parser = _FetchUnicodesParser() | |
| parser.parse(glif) | |
| return parser.unicodes | |
| class _FetchUnicodesParser(_BaseParser): | |
| def __init__(self): | |
| self.unicodes = [] | |
| super().__init__() | |
| def startElementHandler(self, name, attrs): | |
| if ( | |
| name == "unicode" | |
| and self._elementStack | |
| and self._elementStack[-1] == "glyph" | |
| ): | |
| value = attrs.get("hex") | |
| if value is not None: | |
| try: | |
| value = int(value, 16) | |
| if value not in self.unicodes: | |
| self.unicodes.append(value) | |
| except ValueError: | |
| pass | |
| super().startElementHandler(name, attrs) | |
| # image | |
| def _fetchImageFileName(glif): | |
| """ | |
| The image file name (if any) from glif. | |
| """ | |
| parser = _FetchImageFileNameParser() | |
| try: | |
| parser.parse(glif) | |
| except _DoneParsing: | |
| pass | |
| return parser.fileName | |
| class _FetchImageFileNameParser(_BaseParser): | |
| def __init__(self): | |
| self.fileName = None | |
| super().__init__() | |
| def startElementHandler(self, name, attrs): | |
| if name == "image" and self._elementStack and self._elementStack[-1] == "glyph": | |
| self.fileName = attrs.get("fileName") | |
| raise _DoneParsing | |
| super().startElementHandler(name, attrs) | |
| # component references | |
| def _fetchComponentBases(glif): | |
| """ | |
| Get a list of component base glyphs listed in glif. | |
| """ | |
| parser = _FetchComponentBasesParser() | |
| try: | |
| parser.parse(glif) | |
| except _DoneParsing: | |
| pass | |
| return list(parser.bases) | |
| class _FetchComponentBasesParser(_BaseParser): | |
| def __init__(self): | |
| self.bases = [] | |
| super().__init__() | |
| def startElementHandler(self, name, attrs): | |
| if ( | |
| name == "component" | |
| and self._elementStack | |
| and self._elementStack[-1] == "outline" | |
| ): | |
| base = attrs.get("base") | |
| if base is not None: | |
| self.bases.append(base) | |
| super().startElementHandler(name, attrs) | |
| def endElementHandler(self, name): | |
| if name == "outline": | |
| raise _DoneParsing | |
| super().endElementHandler(name) | |
| # -------------- | |
| # GLIF Point Pen | |
| # -------------- | |
| _transformationInfo = [ | |
| # field name, default value | |
| ("xScale", 1), | |
| ("xyScale", 0), | |
| ("yxScale", 0), | |
| ("yScale", 1), | |
| ("xOffset", 0), | |
| ("yOffset", 0), | |
| ] | |
| class GLIFPointPen(AbstractPointPen): | |
| """ | |
| Helper class using the PointPen protocol to write the <outline> | |
| part of .glif files. | |
| """ | |
| def __init__(self, element, formatVersion=None, identifiers=None, validate=True): | |
| if identifiers is None: | |
| identifiers = set() | |
| self.formatVersion = GLIFFormatVersion(formatVersion) | |
| self.identifiers = identifiers | |
| self.outline = element | |
| self.contour = None | |
| self.prevOffCurveCount = 0 | |
| self.prevPointTypes = [] | |
| self.validate = validate | |
| def beginPath(self, identifier=None, **kwargs): | |
| attrs = OrderedDict() | |
| if identifier is not None and self.formatVersion.major >= 2: | |
| if self.validate: | |
| if identifier in self.identifiers: | |
| raise GlifLibError( | |
| "identifier used more than once: %s" % identifier | |
| ) | |
| if not identifierValidator(identifier): | |
| raise GlifLibError( | |
| "identifier not formatted properly: %s" % identifier | |
| ) | |
| attrs["identifier"] = identifier | |
| self.identifiers.add(identifier) | |
| self.contour = etree.SubElement(self.outline, "contour", attrs) | |
| self.prevOffCurveCount = 0 | |
| def endPath(self): | |
| if self.prevPointTypes and self.prevPointTypes[0] == "move": | |
| if self.validate and self.prevPointTypes[-1] == "offcurve": | |
| raise GlifLibError("open contour has loose offcurve point") | |
| # prevent lxml from writing self-closing tags | |
| if not len(self.contour): | |
| self.contour.text = "\n " | |
| self.contour = None | |
| self.prevPointType = None | |
| self.prevOffCurveCount = 0 | |
| self.prevPointTypes = [] | |
| def addPoint( | |
| self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs | |
| ): | |
| attrs = OrderedDict() | |
| # coordinates | |
| if pt is not None: | |
| if self.validate: | |
| for coord in pt: | |
| if not isinstance(coord, numberTypes): | |
| raise GlifLibError("coordinates must be int or float") | |
| attrs["x"] = repr(pt[0]) | |
| attrs["y"] = repr(pt[1]) | |
| # segment type | |
| if segmentType == "offcurve": | |
| segmentType = None | |
| if self.validate: | |
| if segmentType == "move" and self.prevPointTypes: | |
| raise GlifLibError( | |
| "move occurs after a point has already been added to the contour." | |
| ) | |
| if ( | |
| segmentType in ("move", "line") | |
| and self.prevPointTypes | |
| and self.prevPointTypes[-1] == "offcurve" | |
| ): | |
| raise GlifLibError("offcurve occurs before %s point." % segmentType) | |
| if segmentType == "curve" and self.prevOffCurveCount > 2: | |
| raise GlifLibError("too many offcurve points before curve point.") | |
| if segmentType is not None: | |
| attrs["type"] = segmentType | |
| else: | |
| segmentType = "offcurve" | |
| if segmentType == "offcurve": | |
| self.prevOffCurveCount += 1 | |
| else: | |
| self.prevOffCurveCount = 0 | |
| self.prevPointTypes.append(segmentType) | |
| # smooth | |
| if smooth: | |
| if self.validate and segmentType == "offcurve": | |
| raise GlifLibError("can't set smooth in an offcurve point.") | |
| attrs["smooth"] = "yes" | |
| # name | |
| if name is not None: | |
| attrs["name"] = name | |
| # identifier | |
| if identifier is not None and self.formatVersion.major >= 2: | |
| if self.validate: | |
| if identifier in self.identifiers: | |
| raise GlifLibError( | |
| "identifier used more than once: %s" % identifier | |
| ) | |
| if not identifierValidator(identifier): | |
| raise GlifLibError( | |
| "identifier not formatted properly: %s" % identifier | |
| ) | |
| attrs["identifier"] = identifier | |
| self.identifiers.add(identifier) | |
| etree.SubElement(self.contour, "point", attrs) | |
| def addComponent(self, glyphName, transformation, identifier=None, **kwargs): | |
| attrs = OrderedDict([("base", glyphName)]) | |
| for (attr, default), value in zip(_transformationInfo, transformation): | |
| if self.validate and not isinstance(value, numberTypes): | |
| raise GlifLibError("transformation values must be int or float") | |
| if value != default: | |
| attrs[attr] = repr(value) | |
| if identifier is not None and self.formatVersion.major >= 2: | |
| if self.validate: | |
| if identifier in self.identifiers: | |
| raise GlifLibError( | |
| "identifier used more than once: %s" % identifier | |
| ) | |
| if self.validate and not identifierValidator(identifier): | |
| raise GlifLibError( | |
| "identifier not formatted properly: %s" % identifier | |
| ) | |
| attrs["identifier"] = identifier | |
| self.identifiers.add(identifier) | |
| etree.SubElement(self.outline, "component", attrs) | |
| if __name__ == "__main__": | |
| import doctest | |
| doctest.testmod() | |