aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ufoLib/glifLib.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ufoLib/glifLib.py')
-rwxr-xr-xLib/fontTools/ufoLib/glifLib.py3339
1 files changed, 1771 insertions, 1568 deletions
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
index 7d28eaf7..6dee9db3 100755
--- a/Lib/fontTools/ufoLib/glifLib.py
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -27,13 +27,13 @@ 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,
+ genericTypeValidator,
+ colorValidator,
+ guidelinesValidator,
+ anchorsValidator,
+ identifierValidator,
+ imageValidator,
+ glyphLibValidator,
)
from fontTools.misc import etree
from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion
@@ -41,10 +41,11 @@ from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
__all__ = [
- "GlyphSet",
- "GlifLibError",
- "readGlyphFromString", "writeGlyphToString",
- "glyphNameToFileName"
+ "GlyphSet",
+ "GlifLibError",
+ "readGlyphFromString",
+ "writeGlyphToString",
+ "glyphNameToFileName",
]
logger = logging.getLogger(__name__)
@@ -59,25 +60,26 @@ LAYERINFO_FILENAME = "layerinfo.plist"
class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
- FORMAT_1_0 = (1, 0)
- FORMAT_2_0 = (2, 0)
-
- @classmethod
- def default(cls, ufoFormatVersion=None):
- if ufoFormatVersion is not None:
- return max(cls.supported_versions(ufoFormatVersion))
- return super().default()
-
- @classmethod
- 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)
+ FORMAT_1_0 = (1, 0)
+ FORMAT_2_0 = (2, 0)
+
+ @classmethod
+ def default(cls, ufoFormatVersion=None):
+ if ufoFormatVersion is not None:
+ return max(cls.supported_versions(ufoFormatVersion))
+ return super().default()
+
+ @classmethod
+ 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__
@@ -87,1188 +89,1295 @@ 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.
- """
+ """
+ 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 __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 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)
+ 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)
- tree = _glifTreeFromString(text)
- formatVersions = GLIFFormatVersion.supported_versions(self.ufoFormatVersionTuple)
- _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
-
- 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()
+ """
+ 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
+ """
+ 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")
- 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,
+ 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
- )
+ """
+ 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,
+ 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
+ """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,
+ 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")
+ """
+ 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)))
+ 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))
+ 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
+ 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)
+ 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)
+ 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()
+ 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)
+ 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)
+ 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)
+ "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
+ """
+ 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
+ """
+ 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
+ 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")
- if etree._have_lxml:
- root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True))
- else:
- root = etree.fromstring(data)
- 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
+ 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,
+ 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":
- try:
- v = element.get("hex")
- 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)
+ # 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":
+ try:
+ v = element.get("hex")
+ 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
+ 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":
- try:
- v = element.get("hex")
- 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)
+ # 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":
+ try:
+ v = element.get("hex")
+ 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,
+ 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)
+ 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)
+ 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)
+ 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)
+ 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)
+ 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"}
+componentAttributesFormat1 = {
+ "base",
+ "xScale",
+ "xyScale",
+ "yxScale",
+ "yScale",
+ "xOffset",
+ "yOffset",
+}
componentAttributesFormat2 = componentAttributesFormat1 | {"identifier"}
pointAttributesFormat1 = {"x", "y", "type", "smooth", "name"}
pointAttributesFormat2 = pointAttributesFormat1 | {"identifier"}
@@ -1277,303 +1386,357 @@ 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)
+ 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
+ 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()
+ 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)
+ 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))
+ 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)
+ 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()
+ 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)
+ 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)
+ 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
+
+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
+ 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)
+ """
+ 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
@@ -1581,234 +1744,274 @@ def _number(s):
# base
-class _DoneParsing(Exception): pass
+
+class _DoneParsing(Exception):
+ pass
+
class _BaseParser:
+ def __init__(self):
+ self._elementStack = []
- def __init__(self):
- self._elementStack = []
+ def parse(self, text):
+ from xml.parsers.expat import ParserCreate
- def parse(self, text):
- from xml.parsers.expat import ParserCreate
- parser = ParserCreate()
- parser.StartElementHandler = self.startElementHandler
- parser.EndElementHandler = self.endElementHandler
- parser.Parse(text)
+ parser = ParserCreate()
+ parser.StartElementHandler = self.startElementHandler
+ parser.EndElementHandler = self.endElementHandler
+ parser.Parse(text)
- def startElementHandler(self, name, attrs):
- self._elementStack.append(name)
+ def startElementHandler(self, name, attrs):
+ self._elementStack.append(name)
- def endElementHandler(self, name):
- other = self._elementStack.pop(-1)
- assert other == 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
+ """
+ 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)
- 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
+ """
+ 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 __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)
- 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 _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)
- 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)
+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)
- 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),
+ # 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)
+ """
+ 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()
+ import doctest
+
+ doctest.testmod()