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