|
""" |
|
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__) |
|
|
|
|
|
|
|
|
|
|
|
|
|
CONTENTS_FILENAME = "contents.plist" |
|
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: |
|
|
|
return super().supported_versions() |
|
|
|
versions = {cls.FORMAT_1_0} |
|
if ufoFormatVersion >= UFOFormatVersion.FORMAT_3_0: |
|
versions.add(cls.FORMAT_2_0) |
|
return frozenset(versions) |
|
|
|
|
|
|
|
GLIFFormatVersion.__str__ = _VersionTupleEnumMixin.__str__ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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__"): |
|
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: |
|
|
|
path = str(filesystem) |
|
|
|
|
|
|
|
self.dirName = fs.path.parts(path)[-1] |
|
self.fs = filesystem |
|
|
|
self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) |
|
if expectContentsFile and not self._havePreviousFile: |
|
raise GlifLibError(f"{CONTENTS_FILENAME} is missing.") |
|
|
|
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, {}) |
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
|
|
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 |
|
) |
|
|
|
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: |
|
|
|
if validateWrite: |
|
infoData = validateLayerInfoVersion3Data(infoData) |
|
|
|
self._writePlist(LAYERINFO_FILENAME, infoData) |
|
elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME): |
|
|
|
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) |
|
|
|
|
|
|
|
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: |
|
|
|
|
|
fileName = self.contents[glyphName] |
|
try: |
|
glifLocation = f"'{self.fs.getsyspath(fileName)}'" |
|
except fs.errors.NoSysPath: |
|
|
|
|
|
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] |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}" |
|
) |
|
|
|
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() |
|
|
|
_writeAdvance(glyphObject, root, validate) |
|
|
|
if getattr(glyphObject, "unicodes", None): |
|
_writeUnicodes(glyphObject, root, validate) |
|
|
|
if getattr(glyphObject, "note", None): |
|
_writeNote(glyphObject, root, validate) |
|
|
|
if formatVersion.major >= 2 and getattr(glyphObject, "image", None): |
|
_writeImage(glyphObject, root, validate) |
|
|
|
if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None): |
|
_writeGuidelines(glyphObject, root, identifiers, validate) |
|
|
|
anchors = getattr(glyphObject, "anchors", None) |
|
if formatVersion.major >= 2 and anchors: |
|
_writeAnchors(glyphObject, root, identifiers, validate) |
|
|
|
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) |
|
|
|
if not len(outline): |
|
outline.text = "\n " |
|
|
|
if getattr(glyphObject, "lib", None): |
|
_writeLib(glyphObject, root, validate) |
|
|
|
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: |
|
|
|
return |
|
if validate: |
|
valid, message = glyphLibValidator(lib) |
|
if not valid: |
|
raise GlifLibError(message) |
|
if not isinstance(lib, dict): |
|
lib = dict(lib) |
|
|
|
e = plistlib.totree(lib, indent_level=2) |
|
etree.SubElement(element, "lib").append(e) |
|
|
|
|
|
|
|
|
|
|
|
|
|
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") |
|
|
|
if valueOptions is not None: |
|
isValidValue = validator(value, valueOptions) |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
): |
|
|
|
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 |
|
|
|
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 |
|
): |
|
|
|
_readName(glyphObject, tree, validate) |
|
|
|
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) |
|
|
|
if unicodes: |
|
_relaxedSetattr(glyphObject, "unicodes", unicodes) |
|
|
|
|
|
def _readGlyphFromTreeFormat2( |
|
tree, glyphObject=None, pointPen=None, validate=None, formatMinor=0 |
|
): |
|
|
|
_readName(glyphObject, tree, validate) |
|
|
|
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) |
|
|
|
if unicodes: |
|
_relaxedSetattr(glyphObject, "unicodes", unicodes) |
|
|
|
if guidelines: |
|
if validate and not guidelinesValidator(guidelines, identifiers): |
|
raise GlifLibError("The guidelines are improperly formatted.") |
|
_relaxedSetattr(glyphObject, "guidelines", guidelines) |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
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"} |
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
|
|
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, |
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _validateAndMassagePointStructures( |
|
contour, pointAttributes, openContourOffCurveLeniency=False, validate=True |
|
): |
|
if not len(contour): |
|
return |
|
|
|
lastOnCurvePoint = None |
|
haveOffCurvePoint = False |
|
|
|
massaged = [] |
|
for index, element in enumerate(contour): |
|
|
|
if element.tag != "point": |
|
raise GlifLibError( |
|
"Unknown child element (%s) of contour element." % element.tag |
|
) |
|
point = dict(element.attrib) |
|
massaged.append(point) |
|
if validate: |
|
|
|
for attr in point.keys(): |
|
if attr not in pointAttributes: |
|
raise GlifLibError("Unknown attribute in point element: %s" % attr) |
|
|
|
if len(element): |
|
raise GlifLibError("Unknown child elements in point element.") |
|
|
|
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 |
|
|
|
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 |
|
|
|
if validate and pointType == "move" and index != 0: |
|
raise GlifLibError( |
|
"A move point occurs after the first point in the contour." |
|
) |
|
|
|
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 |
|
|
|
if validate and smooth and pointType is None: |
|
raise GlifLibError("smooth attribute set in an offcurve point.") |
|
|
|
if "name" not in element.attrib: |
|
point["name"] = None |
|
if openContourOffCurveLeniency: |
|
|
|
|
|
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] |
|
|
|
if validate and haveOffCurvePoint and lastOnCurvePoint is not None: |
|
|
|
|
|
offCurvesCount = len(massaged) - 1 - lastOnCurvePoint |
|
for point in massaged: |
|
segmentType = point["segmentType"] |
|
if segmentType is None: |
|
offCurvesCount += 1 |
|
else: |
|
if offCurvesCount: |
|
|
|
if segmentType == "move": |
|
|
|
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: |
|
|
|
pass |
|
offCurvesCount = 0 |
|
return massaged |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
_transformationInfo = [ |
|
|
|
("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") |
|
|
|
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() |
|
|
|
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]) |
|
|
|
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) |
|
|
|
if smooth: |
|
if self.validate and segmentType == "offcurve": |
|
raise GlifLibError("can't set smooth in an offcurve point.") |
|
attrs["smooth"] = "yes" |
|
|
|
if name is not None: |
|
attrs["name"] = name |
|
|
|
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() |
|
|