2008-02-02 12:37:51 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
glifLib.py -- Generic module for reading and writing the .glif format.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
More info about the .glif format (GLyphInterchangeFormat) can be found here:
|
|
|
|
|
2011-06-18 17:08:25 +00:00
|
|
|
http://unifiedfontobject.org
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2016-05-31 11:23:05 +01:00
|
|
|
from __future__ import unicode_literals
|
2008-01-07 17:40:34 +00:00
|
|
|
import os
|
2016-05-19 18:40:59 -07:00
|
|
|
from io import BytesIO, open
|
2011-10-17 19:31:38 +00:00
|
|
|
from warnings import warn
|
2018-06-13 16:42:33 -05:00
|
|
|
from collections import OrderedDict
|
2018-07-10 15:39:58 +01:00
|
|
|
from fontTools.misc.py23 import basestring, unicode, tobytes
|
2016-03-01 12:03:24 +01:00
|
|
|
from ufoLib.plistlib import PlistWriter, readPlist, writePlist
|
2016-05-19 18:40:59 -07:00
|
|
|
from ufoLib.plistFromETree import readPlistFromTree
|
2015-11-13 10:02:35 +01:00
|
|
|
from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen
|
2015-11-02 13:21:05 +00:00
|
|
|
from ufoLib.filenames import userNameToFileName
|
|
|
|
from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\
|
2011-10-17 20:32:20 +00:00
|
|
|
guidelinesValidator, anchorsValidator, identifierValidator, imageValidator, glyphLibValidator
|
2011-09-28 18:07:13 +00:00
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
from lxml import etree
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
__all__ = [
|
|
|
|
"GlyphSet",
|
|
|
|
"GlifLibError",
|
|
|
|
"readGlyphFromString", "writeGlyphToString",
|
|
|
|
"glyphNameToFileName"
|
|
|
|
]
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
class GlifLibError(Exception): pass
|
|
|
|
|
2017-11-04 22:52:10 +01:00
|
|
|
class MissingPlistError(GlifLibError): pass
|
|
|
|
|
2011-09-29 14:09:45 +00:00
|
|
|
# ---------
|
|
|
|
# Constants
|
|
|
|
# ---------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
LAYERINFO_FILENAME = "layerinfo.plist"
|
2011-09-29 14:09:45 +00:00
|
|
|
supportedUFOFormatVersions = [1, 2, 3]
|
2011-09-28 17:21:07 +00:00
|
|
|
supportedGLIFFormatVersions = [1, 2]
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:16:25 +00:00
|
|
|
# ------------
|
|
|
|
# Simple Glyph
|
|
|
|
# ------------
|
|
|
|
|
|
|
|
class Glyph(object):
|
|
|
|
|
|
|
|
"""
|
|
|
|
Minimal glyph object. It has no glyph attributes until either
|
2015-11-08 11:49:06 +01:00
|
|
|
the draw() or the drawPoints() method has been called.
|
2011-09-28 13:16:25 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, glyphName, glyphSet):
|
|
|
|
self.glyphName = glyphName
|
|
|
|
self.glyphSet = glyphSet
|
|
|
|
|
|
|
|
def draw(self, pen):
|
|
|
|
"""
|
|
|
|
Draw this glyph onto a *FontTools* Pen.
|
|
|
|
"""
|
|
|
|
pointPen = PointToSegmentPen(pen)
|
|
|
|
self.drawPoints(pointPen)
|
|
|
|
|
|
|
|
def drawPoints(self, pointPen):
|
|
|
|
"""
|
|
|
|
Draw this glyph onto a PointPen.
|
|
|
|
"""
|
|
|
|
self.glyphSet.readGlyph(self.glyphName, self, pointPen)
|
|
|
|
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# ---------
|
|
|
|
# Glyph Set
|
|
|
|
# ---------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 00:46:51 +00:00
|
|
|
class GlyphSet(object):
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
GlyphSet manages a set of .glif files inside one directory.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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
|
2011-06-18 17:08:25 +00:00
|
|
|
support writing, they are just a convenient way to get at the glyph data.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
glyphClass = Glyph
|
|
|
|
|
2018-07-04 13:56:44 -05:00
|
|
|
def __init__(self, dirName, glyphNameToFileNameFunc=None, ufoFormatVersion=3, validateRead=True, validateWrite=True):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
'dirName' should be a path to an existing directory.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
The optional 'glyphNameToFileNameFunc' argument must be a callback
|
2018-06-13 12:37:49 -04:00
|
|
|
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.
|
2018-06-11 15:03:54 -05:00
|
|
|
|
2018-07-04 15:02:26 -05:00
|
|
|
``validateRead`` will validate read operations. Its default is ``True``.
|
|
|
|
``validateWrite`` will validate write operations. Its default is ``True``.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
self.dirName = dirName
|
2011-09-29 14:09:45 +00:00
|
|
|
if ufoFormatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
|
2011-09-28 12:52:10 +00:00
|
|
|
self.ufoFormatVersion = ufoFormatVersion
|
2008-01-07 17:40:34 +00:00
|
|
|
if glyphNameToFileNameFunc is None:
|
|
|
|
glyphNameToFileNameFunc = glyphNameToFileName
|
|
|
|
self.glyphNameToFileName = glyphNameToFileNameFunc
|
2018-06-11 15:03:54 -05:00
|
|
|
self._validateRead = validateRead
|
|
|
|
self._validateWrite = validateWrite
|
2011-09-29 13:26:25 +00:00
|
|
|
self.rebuildContents()
|
2018-06-13 12:37:49 -04:00
|
|
|
self._existingFileNames = None
|
2008-01-07 17:40:34 +00:00
|
|
|
self._reverseContents = None
|
2011-06-18 17:08:25 +00:00
|
|
|
self._glifCache = {}
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def rebuildContents(self, validateRead=None):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
2011-09-29 00:55:34 +00:00
|
|
|
Rebuild the contents dict by loading contents.plist.
|
2018-06-11 15:03:54 -05:00
|
|
|
|
|
|
|
``validateRead`` will validate the data, by default it is set to the
|
|
|
|
class's ``validateRead`` value, can be overridden.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
2018-06-11 15:03:54 -05:00
|
|
|
if validateRead is None:
|
|
|
|
validateRead = self._validateRead
|
2011-09-29 00:55:34 +00:00
|
|
|
contentsPath = os.path.join(self.dirName, "contents.plist")
|
2017-11-04 19:37:01 +01:00
|
|
|
try:
|
|
|
|
contents = self._readPlist(contentsPath)
|
2017-11-04 22:52:10 +01:00
|
|
|
except MissingPlistError:
|
2011-09-29 13:26:25 +00:00
|
|
|
# missing, consider the glyphset empty.
|
|
|
|
contents = {}
|
2011-09-29 00:55:34 +00:00
|
|
|
# validate the contents
|
2018-06-11 15:03:54 -05:00
|
|
|
if validateRead:
|
2018-06-11 12:05:36 -05:00
|
|
|
invalidFormat = False
|
|
|
|
if not isinstance(contents, dict):
|
|
|
|
invalidFormat = True
|
|
|
|
else:
|
|
|
|
for name, fileName in contents.items():
|
|
|
|
if not isinstance(name, basestring):
|
|
|
|
invalidFormat = True
|
|
|
|
if not isinstance(fileName, basestring):
|
|
|
|
invalidFormat = True
|
|
|
|
elif not os.path.exists(os.path.join(self.dirName, fileName)):
|
2018-06-12 20:40:32 -04:00
|
|
|
raise GlifLibError("contents.plist references a file that does not exist: %s" % fileName)
|
2018-06-11 12:05:36 -05:00
|
|
|
if invalidFormat:
|
|
|
|
raise GlifLibError("contents.plist is not properly formatted")
|
2011-09-29 00:55:34 +00:00
|
|
|
self.contents = contents
|
2018-06-13 12:37:49 -04:00
|
|
|
self._existingFileNames = None
|
2008-01-07 17:40:34 +00:00
|
|
|
self._reverseContents = None
|
|
|
|
|
|
|
|
def getReverseContents(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Return a reversed dict of self.contents, mapping file names to
|
2008-01-07 17:40:34 +00:00
|
|
|
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 = {}
|
2015-11-05 09:03:19 +00:00
|
|
|
for k, v in self.contents.items():
|
2008-01-07 17:40:34 +00:00
|
|
|
d[v.lower()] = k
|
|
|
|
self._reverseContents = d
|
|
|
|
return self._reverseContents
|
|
|
|
|
|
|
|
def writeContents(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Write the contents.plist file out to disk. Call this method when
|
2008-01-07 17:40:34 +00:00
|
|
|
you're done writing glyphs.
|
|
|
|
"""
|
|
|
|
contentsPath = os.path.join(self.dirName, "contents.plist")
|
2015-11-05 10:12:15 +00:00
|
|
|
with open(contentsPath, "wb") as f:
|
2016-03-01 12:03:24 +01:00
|
|
|
writePlist(self.contents, f)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 12:52:10 +00:00
|
|
|
# layer info
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
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
|
2011-09-28 12:52:10 +00:00
|
|
|
path = os.path.join(self.dirName, LAYERINFO_FILENAME)
|
2017-11-04 19:37:01 +01:00
|
|
|
try:
|
|
|
|
infoDict = self._readPlist(path)
|
2017-11-04 22:52:10 +01:00
|
|
|
except MissingPlistError:
|
2011-09-28 12:52:10 +00:00
|
|
|
return
|
2018-06-11 15:03:54 -05:00
|
|
|
if validateRead:
|
2018-06-11 23:13:00 -05:00
|
|
|
if not isinstance(infoDict, dict):
|
|
|
|
raise GlifLibError("layerinfo.plist is not properly formatted.")
|
2018-06-11 15:03:54 -05:00
|
|
|
infoDict = validateLayerInfoVersion3Data(infoDict)
|
2011-09-28 12:52:10 +00:00
|
|
|
# populate the object
|
2017-11-04 19:37:01 +01:00
|
|
|
for attr, value in infoDict.items():
|
2011-09-28 12:52:10 +00:00
|
|
|
try:
|
|
|
|
setattr(info, attr, value)
|
|
|
|
except AttributeError:
|
|
|
|
raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr)
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
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
|
2018-06-12 20:40:32 -04:00
|
|
|
if self.ufoFormatVersion < 3:
|
2011-09-28 12:52:10 +00:00
|
|
|
raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion)
|
|
|
|
# gather data
|
|
|
|
infoData = {}
|
2017-11-04 19:37:01 +01:00
|
|
|
for attr in layerInfoVersion3ValueData.keys():
|
2011-09-28 12:52:10 +00:00
|
|
|
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)
|
2016-06-24 13:43:42 +01:00
|
|
|
if value is None or (attr == 'lib' and not value):
|
2011-09-28 12:52:10 +00:00
|
|
|
continue
|
|
|
|
infoData[attr] = value
|
|
|
|
# validate
|
2018-06-11 15:03:54 -05:00
|
|
|
if validateWrite:
|
|
|
|
infoData = validateLayerInfoVersion3Data(infoData)
|
2011-09-28 12:52:10 +00:00
|
|
|
# write file
|
|
|
|
path = os.path.join(self.dirName, LAYERINFO_FILENAME)
|
2015-11-05 10:12:15 +00:00
|
|
|
with open(path, "wb") as f:
|
2016-03-01 12:03:24 +01:00
|
|
|
writePlist(infoData, f)
|
2011-09-28 12:52:10 +00:00
|
|
|
|
2011-06-18 17:08:25 +00:00
|
|
|
# read caching
|
|
|
|
|
|
|
|
def getGLIF(self, glyphName):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Get the raw GLIF text for a given glyph name. This only works
|
2011-06-18 17:08:25 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
Internally, this method will load a GLIF the first time it is
|
|
|
|
called and then cache it. The next time this method is called
|
|
|
|
the GLIF will be pulled from the cache if the file's modification
|
|
|
|
time has not changed since the GLIF was cached. For memory
|
|
|
|
efficiency, the cached GLIF will be purged by various other methods
|
|
|
|
such as readGlyph.
|
|
|
|
"""
|
|
|
|
needRead = False
|
|
|
|
fileName = self.contents.get(glyphName)
|
|
|
|
path = None
|
|
|
|
if fileName is not None:
|
|
|
|
path = os.path.join(self.dirName, fileName)
|
|
|
|
if glyphName not in self._glifCache:
|
|
|
|
needRead = True
|
|
|
|
elif fileName is not None and os.path.getmtime(path) != self._glifCache[glyphName][1]:
|
|
|
|
needRead = True
|
|
|
|
if needRead:
|
|
|
|
fileName = self.contents[glyphName]
|
2017-11-04 19:37:01 +01:00
|
|
|
try:
|
|
|
|
with open(path, "rb") as f:
|
|
|
|
text = f.read()
|
|
|
|
except IOError as e:
|
2017-11-04 22:59:01 +01:00
|
|
|
if e.errno == 2: # aka. FileNotFoundError
|
2017-11-04 19:37:01 +01:00
|
|
|
raise KeyError(glyphName)
|
|
|
|
raise
|
2011-06-18 17:08:25 +00:00
|
|
|
self._glifCache[glyphName] = (text, os.path.getmtime(path))
|
|
|
|
return self._glifCache[glyphName][0]
|
|
|
|
|
2011-10-28 16:59:16 +00:00
|
|
|
def getGLIFModificationTime(self, glyphName):
|
|
|
|
"""
|
|
|
|
Get the modification time (as reported by os.path.getmtime)
|
|
|
|
of the GLIF with glyphName.
|
|
|
|
"""
|
|
|
|
self.getGLIF(glyphName)
|
|
|
|
return self._glifCache[glyphName][1]
|
|
|
|
|
2011-06-18 17:08:25 +00:00
|
|
|
def _purgeCachedGLIF(self, glyphName):
|
|
|
|
if glyphName in self._glifCache:
|
|
|
|
del self._glifCache[glyphName]
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
# reading/writing API
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=None):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Read a .glif file for 'glyphName' from the glyph set. The
|
2008-01-07 17:40:34 +00:00
|
|
|
'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the readGlyph() method will attempt to set the following
|
|
|
|
attributes on it:
|
2011-10-03 16:24:24 +00:00
|
|
|
"width" the advance with 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
|
2015-11-04 08:57:08 +00:00
|
|
|
"anchors" a list of anchor data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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.
|
2018-06-11 15:03:54 -05:00
|
|
|
|
|
|
|
``validate`` will validate the data, by default it is set to the
|
|
|
|
class's ``validateRead`` value, can be overridden.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate is None:
|
|
|
|
validate = self._validateRead
|
2011-06-18 17:08:25 +00:00
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
self._purgeCachedGLIF(glyphName)
|
2016-05-19 18:40:59 -07:00
|
|
|
tree = _glifTreeFromString(text)
|
2011-10-07 13:54:14 +00:00
|
|
|
if self.ufoFormatVersion < 3:
|
|
|
|
formatVersions = (1,)
|
|
|
|
else:
|
|
|
|
formatVersions = (1, 2)
|
2018-06-11 15:03:54 -05:00
|
|
|
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=None):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Write a .glif file for 'glyphName' to the glyph set. The
|
2008-01-07 17:40:34 +00:00
|
|
|
'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the writeGlyph() method will attempt to get the following
|
|
|
|
attributes from it:
|
2011-10-03 16:24:24 +00:00
|
|
|
"width" the advance with 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
|
2015-11-04 08:57:08 +00:00
|
|
|
"anchors" a list of anchor data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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.
|
2011-09-29 14:09:45 +00:00
|
|
|
|
|
|
|
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.
|
2018-06-11 15:03:54 -05:00
|
|
|
|
|
|
|
``validate`` will validate the data, by default it is set to the
|
|
|
|
class's ``validateWrite`` value, can be overridden.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
2011-09-29 14:09:45 +00:00
|
|
|
if formatVersion is None:
|
|
|
|
if self.ufoFormatVersion >= 3:
|
|
|
|
formatVersion = 2
|
|
|
|
else:
|
|
|
|
formatVersion = 1
|
2018-06-12 20:40:32 -04:00
|
|
|
if formatVersion not in supportedGLIFFormatVersions:
|
|
|
|
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
|
|
|
|
if formatVersion == 2 and self.ufoFormatVersion < 3:
|
|
|
|
raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion))
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate is None:
|
|
|
|
validate = self._validateWrite
|
2011-06-18 17:08:25 +00:00
|
|
|
self._purgeCachedGLIF(glyphName)
|
2018-07-10 12:37:15 +01:00
|
|
|
data = _writeGlyphToBytes(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion, validate=validate)
|
2008-01-07 17:40:34 +00:00
|
|
|
fileName = self.contents.get(glyphName)
|
|
|
|
if fileName is None:
|
2018-06-13 12:37:49 -04:00
|
|
|
if self._existingFileNames is None:
|
|
|
|
self._existingFileNames = {}
|
|
|
|
for fileName in self.contents.values():
|
|
|
|
self._existingFileNames[fileName] = fileName.lower()
|
|
|
|
fileName = self.glyphNameToFileName(glyphName, self._existingFileNames)
|
2008-01-07 17:40:34 +00:00
|
|
|
self.contents[glyphName] = fileName
|
2018-06-13 12:37:49 -04:00
|
|
|
self._existingFileNames[fileName] = fileName.lower()
|
2008-01-07 17:40:34 +00:00
|
|
|
if self._reverseContents is not None:
|
|
|
|
self._reverseContents[fileName.lower()] = glyphName
|
|
|
|
path = os.path.join(self.dirName, fileName)
|
|
|
|
if os.path.exists(path):
|
2015-11-08 15:22:23 +01:00
|
|
|
with open(path, "rb") as f:
|
2015-11-02 13:21:05 +00:00
|
|
|
oldData = f.read()
|
2008-01-07 17:40:34 +00:00
|
|
|
if data == oldData:
|
|
|
|
return
|
2015-11-08 15:22:23 +01:00
|
|
|
with open(path, "wb") as f:
|
2018-07-10 12:37:15 +01:00
|
|
|
f.write(data)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
2011-06-18 17:08:25 +00:00
|
|
|
self._purgeCachedGLIF(glyphName)
|
2008-01-07 17:40:34 +00:00
|
|
|
fileName = self.contents[glyphName]
|
|
|
|
os.remove(os.path.join(self.dirName, fileName))
|
2018-06-13 12:46:40 -04:00
|
|
|
if self._existingFileNames is not None:
|
2018-06-13 12:37:49 -04:00
|
|
|
del self._existingFileNames[fileName]
|
2008-01-07 17:40:34 +00:00
|
|
|
if self._reverseContents is not None:
|
|
|
|
del self._reverseContents[self.contents[glyphName].lower()]
|
|
|
|
del self.contents[glyphName]
|
|
|
|
|
|
|
|
# dict-like support
|
|
|
|
|
|
|
|
def keys(self):
|
2015-11-05 09:03:19 +00:00
|
|
|
return list(self.contents.keys())
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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:
|
2015-11-05 09:03:19 +00:00
|
|
|
raise KeyError(glyphName)
|
2008-01-07 17:40:34 +00:00
|
|
|
return self.glyphClass(glyphName, self)
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# quickly fetch unicode values
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-10-18 19:13:09 +00:00
|
|
|
def getUnicodes(self, glyphNames=None):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
2011-10-18 19:13:09 +00:00
|
|
|
Return a dictionary that maps glyph names to lists containing
|
2008-01-07 17:40:34 +00:00
|
|
|
the unicode value[s] for that glyph, if any. This parses the .glif
|
2011-10-11 14:08:24 +00:00
|
|
|
files partially, so it is a lot faster than parsing all files completely.
|
2011-10-18 19:13:09 +00:00
|
|
|
By default this checks all glyphs, but a subset can be passed with glyphNames.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
unicodes = {}
|
2011-10-18 19:13:09 +00:00
|
|
|
if glyphNames is None:
|
2015-11-05 09:03:19 +00:00
|
|
|
glyphNames = list(self.contents.keys())
|
2011-10-18 19:13:09 +00:00
|
|
|
for glyphName in glyphNames:
|
2011-06-18 17:08:25 +00:00
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
unicodes[glyphName] = _fetchUnicodes(text)
|
2008-01-07 17:40:34 +00:00
|
|
|
return unicodes
|
|
|
|
|
2011-10-18 19:13:09 +00:00
|
|
|
def getComponentReferences(self, glyphNames=None):
|
2011-10-11 14:08:24 +00:00
|
|
|
"""
|
2011-10-18 19:13:09 +00:00
|
|
|
Return a dictionary that maps glyph names to lists containing the
|
2011-10-11 14:08:24 +00:00
|
|
|
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.
|
2011-10-18 19:13:09 +00:00
|
|
|
By default this checks all glyphs, but a subset can be passed with glyphNames.
|
2011-10-11 14:08:24 +00:00
|
|
|
"""
|
|
|
|
components = {}
|
2011-10-18 19:13:09 +00:00
|
|
|
if glyphNames is None:
|
2017-11-04 19:37:01 +01:00
|
|
|
glyphNames = self.contents.keys()
|
2011-10-18 19:13:09 +00:00
|
|
|
for glyphName in glyphNames:
|
2011-10-11 14:08:24 +00:00
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
components[glyphName] = _fetchComponentBases(text)
|
|
|
|
return components
|
|
|
|
|
2011-10-18 19:13:09 +00:00
|
|
|
def getImageReferences(self, glyphNames=None):
|
2011-10-11 14:08:24 +00:00
|
|
|
"""
|
2011-10-18 19:13:09 +00:00
|
|
|
Return a dictionary that maps glyph names to the file name of the image
|
2011-10-11 14:08:24 +00:00
|
|
|
referenced by the glyph. This parses the .glif files partially, so it is a
|
|
|
|
lot faster than parsing all files completely.
|
2011-10-18 19:13:09 +00:00
|
|
|
By default this checks all glyphs, but a subset can be passed with glyphNames.
|
2011-10-11 14:08:24 +00:00
|
|
|
"""
|
|
|
|
images = {}
|
2011-10-18 19:13:09 +00:00
|
|
|
if glyphNames is None:
|
2017-11-04 19:37:01 +01:00
|
|
|
glyphNames = self.contents.keys()
|
2011-10-18 19:13:09 +00:00
|
|
|
for glyphName in glyphNames:
|
2011-10-11 14:08:24 +00:00
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
images[glyphName] = _fetchImageFileName(text)
|
|
|
|
return images
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
# internal methods
|
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
def _readPlist(self, path):
|
|
|
|
try:
|
2015-11-02 13:21:05 +00:00
|
|
|
with open(path, "rb") as f:
|
2016-03-01 12:03:24 +01:00
|
|
|
data = readPlist(f)
|
2011-09-28 15:29:10 +00:00
|
|
|
return data
|
2017-11-04 19:37:01 +01:00
|
|
|
except Exception as e:
|
|
|
|
if isinstance(e, IOError) and e.errno == 2:
|
2017-11-17 13:18:44 +01:00
|
|
|
raise MissingPlistError(path)
|
2011-09-28 15:29:10 +00:00
|
|
|
raise GlifLibError("The file %s could not be read." % path)
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# -----------------------
|
|
|
|
# Glyph Name to File Name
|
|
|
|
# -----------------------
|
|
|
|
|
2018-06-13 12:37:49 -04:00
|
|
|
def glyphNameToFileName(glyphName, existingFileNames):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Wrapper around the userNameToFileName function in filenames.py
|
|
|
|
"""
|
2018-06-13 12:46:40 -04:00
|
|
|
if existingFileNames is None:
|
|
|
|
existingFileNames = []
|
2016-07-11 08:00:23 +01:00
|
|
|
if not isinstance(glyphName, unicode):
|
2011-09-28 13:13:07 +00:00
|
|
|
try:
|
2016-07-11 08:00:23 +01:00
|
|
|
new = unicode(glyphName)
|
2011-09-28 13:13:07 +00:00
|
|
|
glyphName = new
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
pass
|
2018-06-13 12:37:49 -04:00
|
|
|
return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif")
|
2011-09-28 13:13:07 +00:00
|
|
|
|
|
|
|
# -----------------------
|
|
|
|
# GLIF To and From String
|
|
|
|
# -----------------------
|
|
|
|
|
2018-07-04 13:56:44 -05:00
|
|
|
def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Read .glif data from a string into a glyph object.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
The 'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the readGlyphFromString() method will attempt to set the following
|
|
|
|
attributes on it:
|
2011-10-15 17:43:57 +00:00
|
|
|
"width" the advance with of the glyph
|
2011-10-03 16:24:24 +00:00
|
|
|
"height" the advance height of the glyph
|
2011-10-15 17:43:57 +00:00
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
2011-10-03 16:24:24 +00:00
|
|
|
"image" a dictionary containing image data
|
|
|
|
"guidelines" a list of guideline data dictionaries
|
2011-10-15 17:43:57 +00:00
|
|
|
"anchors" a list of anchor data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
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.
|
2011-10-07 13:54:14 +00:00
|
|
|
|
|
|
|
The formatVersions argument defined the GLIF format versions
|
|
|
|
that are allowed to be read.
|
2018-06-11 15:03:54 -05:00
|
|
|
|
2018-07-04 13:56:44 -05:00
|
|
|
``validate`` will validate the read data. It is set to ``True`` by default.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
2016-05-19 18:40:59 -07:00
|
|
|
tree = _glifTreeFromString(aString)
|
2018-06-11 15:03:54 -05:00
|
|
|
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
|
2018-07-10 18:52:38 +01:00
|
|
|
# we use a custom XML declaration for backward compatibility with older
|
|
|
|
# ufoLib versions which would write it using double quotes.
|
|
|
|
# https://github.com/unified-font-object/ufoLib/issues/158
|
|
|
|
XML_DECLARATION = b"""<?xml version="1.0" encoding="UTF-8"?>\n"""
|
|
|
|
|
|
|
|
|
2018-07-10 12:37:15 +01:00
|
|
|
def _writeGlyphToBytes(
|
|
|
|
glyphName, glyphObject=None, drawPointsFunc=None, writer=None,
|
|
|
|
formatVersion=2, validate=True):
|
|
|
|
"""Return .glif data for a glyph as a UTF-8 encoded bytes string."""
|
2011-09-30 00:28:24 +00:00
|
|
|
# start
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not isinstance(glyphName, basestring):
|
2011-09-30 15:25:47 +00:00
|
|
|
raise GlifLibError("The glyph name is not properly formatted.")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and len(glyphName) == 0:
|
2011-10-03 16:36:29 +00:00
|
|
|
raise GlifLibError("The glyph name is empty.")
|
2018-06-13 16:42:33 -05:00
|
|
|
root = etree.Element("glyph", OrderedDict([("name", glyphName), ("format", repr(formatVersion))]))
|
2018-06-13 14:22:51 -04:00
|
|
|
identifiers = set()
|
2011-09-30 00:28:24 +00:00
|
|
|
# advance
|
2018-06-13 14:22:51 -04:00
|
|
|
_writeAdvance(glyphObject, root, validate)
|
2011-09-30 00:28:24 +00:00
|
|
|
# unicodes
|
|
|
|
if getattr(glyphObject, "unicodes", None):
|
2018-06-13 14:22:51 -04:00
|
|
|
_writeUnicodes(glyphObject, root, validate)
|
2011-09-30 00:28:24 +00:00
|
|
|
# note
|
|
|
|
if getattr(glyphObject, "note", None):
|
2018-06-13 14:22:51 -04:00
|
|
|
_writeNote(glyphObject, root, validate)
|
2011-09-30 00:28:24 +00:00
|
|
|
# image
|
2011-09-30 00:39:34 +00:00
|
|
|
if formatVersion >= 2 and getattr(glyphObject, "image", None):
|
2018-06-13 21:34:41 -04:00
|
|
|
_writeImage(glyphObject, root, validate)
|
2011-09-30 00:28:24 +00:00
|
|
|
# guidelines
|
2011-09-30 00:39:34 +00:00
|
|
|
if formatVersion >= 2 and getattr(glyphObject, "guidelines", None):
|
2018-06-13 21:34:41 -04:00
|
|
|
_writeGuidelines(glyphObject, root, identifiers, validate)
|
2011-10-15 17:43:57 +00:00
|
|
|
# anchors
|
|
|
|
anchors = getattr(glyphObject, "anchors", None)
|
|
|
|
if formatVersion >= 2 and anchors:
|
2018-06-13 14:22:51 -04:00
|
|
|
_writeAnchors(glyphObject, root, identifiers, validate)
|
2011-09-30 00:28:24 +00:00
|
|
|
# outline
|
|
|
|
if drawPointsFunc is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
outline = etree.SubElement(root, "outline")
|
|
|
|
pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
|
2011-09-30 00:28:24 +00:00
|
|
|
drawPointsFunc(pen)
|
2011-10-17 16:39:25 +00:00
|
|
|
if formatVersion == 1 and anchors:
|
2018-06-11 15:03:54 -05:00
|
|
|
_writeAnchorsFormat1(pen, anchors, validate)
|
2018-06-14 08:36:52 -04:00
|
|
|
# prevent lxml from writing self-closing tags
|
|
|
|
if not len(outline):
|
2018-06-14 10:59:51 -04:00
|
|
|
outline.text = "\n "
|
2011-09-30 00:28:24 +00:00
|
|
|
# lib
|
|
|
|
if getattr(glyphObject, "lib", None):
|
2018-06-13 14:22:51 -04:00
|
|
|
_writeLib(glyphObject, root, validate)
|
|
|
|
# return the text
|
2018-07-10 18:52:38 +01:00
|
|
|
data = XML_DECLARATION + etree.tostring(
|
|
|
|
root, encoding="utf-8", xml_declaration=False, pretty_print=True
|
2018-07-09 20:12:21 +01:00
|
|
|
)
|
2018-07-10 12:37:15 +01:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=2, validate=True):
|
|
|
|
"""
|
|
|
|
Return .glif data for a glyph as a Unicode string (`unicode` in py2, `str`
|
|
|
|
in py3). 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.
|
|
|
|
|
|
|
|
``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")
|
2018-06-13 14:22:51 -04:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def _writeAdvance(glyphObject, element, validate):
|
2008-01-07 17:40:34 +00:00
|
|
|
width = getattr(glyphObject, "width", None)
|
|
|
|
if width is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not isinstance(width, (int, float)):
|
2011-09-29 19:38:33 +00:00
|
|
|
raise GlifLibError("width attribute must be int or float")
|
2011-10-04 01:42:57 +00:00
|
|
|
if width == 0:
|
2017-11-04 19:37:01 +01:00
|
|
|
width = None
|
2011-09-29 14:15:40 +00:00
|
|
|
height = getattr(glyphObject, "height", None)
|
|
|
|
if height is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not isinstance(height, (int, float)):
|
2011-09-29 19:38:33 +00:00
|
|
|
raise GlifLibError("height attribute must be int or float")
|
2011-10-04 01:42:57 +00:00
|
|
|
if height == 0:
|
2017-11-04 19:37:01 +01:00
|
|
|
height = None
|
2011-09-29 14:15:40 +00:00
|
|
|
if width is not None and height is not None:
|
2018-06-14 08:39:53 -04:00
|
|
|
etree.SubElement(element, "advance", OrderedDict([("height", repr(height)), ("width", repr(width))]))
|
2011-09-29 14:15:40 +00:00
|
|
|
elif width is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "advance", dict(width=repr(width)))
|
2011-09-29 14:15:40 +00:00
|
|
|
elif height is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "advance", dict(height=repr(height)))
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def _writeUnicodes(glyphObject, element, validate):
|
2008-01-07 17:40:34 +00:00
|
|
|
unicodes = getattr(glyphObject, "unicodes", None)
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and isinstance(unicodes, int):
|
2011-09-30 00:28:24 +00:00
|
|
|
unicodes = [unicodes]
|
2011-09-30 15:25:47 +00:00
|
|
|
seen = set()
|
2011-09-30 00:28:24 +00:00
|
|
|
for code in unicodes:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not isinstance(code, int):
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("unicode values must be int")
|
2011-09-30 15:25:47 +00:00
|
|
|
if code in seen:
|
|
|
|
continue
|
|
|
|
seen.add(code)
|
2016-05-31 11:23:05 +01:00
|
|
|
hexCode = "%04X" % code
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "unicode", dict(hex=hexCode))
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def _writeNote(glyphObject, element, validate):
|
2008-01-07 17:40:34 +00:00
|
|
|
note = getattr(glyphObject, "note", None)
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not isinstance(note, basestring):
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("note attribute must be str or unicode")
|
2018-06-14 08:43:30 -04:00
|
|
|
note = note.strip()
|
|
|
|
note = "\n" + note + "\n"
|
2011-09-30 00:28:24 +00:00
|
|
|
note = note.encode("utf-8")
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "note").text = note
|
|
|
|
|
|
|
|
def _writeImage(glyphObject, element, validate):
|
2011-09-30 00:28:24 +00:00
|
|
|
image = getattr(glyphObject, "image", None)
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate and not imageValidator(image):
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("image attribute must be a dict or dict-like object with the proper structure.")
|
2018-06-14 00:31:29 -05:00
|
|
|
attrs = OrderedDict([("fileName", image["fileName"])])
|
2011-09-30 00:39:34 +00:00
|
|
|
for attr, default in _transformationInfo:
|
|
|
|
value = image.get(attr, default)
|
2011-09-30 19:24:10 +00:00
|
|
|
if value != default:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs[attr] = repr(value)
|
2011-09-30 00:28:24 +00:00
|
|
|
color = image.get("color")
|
|
|
|
if color is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["color"] = color
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "image", attrs)
|
2011-09-30 00:28:24 +00:00
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def _writeGuidelines(glyphObject, element, identifiers, validate):
|
2011-09-30 00:28:24 +00:00
|
|
|
guidelines = getattr(glyphObject, "guidelines", [])
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate and not guidelinesValidator(guidelines):
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("guidelines attribute does not have the proper structure.")
|
|
|
|
for guideline in guidelines:
|
2018-06-14 00:31:29 -05:00
|
|
|
attrs = OrderedDict()
|
2011-09-30 00:28:24 +00:00
|
|
|
x = guideline.get("x")
|
|
|
|
if x is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["x"] = repr(x)
|
2011-09-30 00:28:24 +00:00
|
|
|
y = guideline.get("y")
|
|
|
|
if y is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["y"] = repr(y)
|
2011-09-30 00:28:24 +00:00
|
|
|
angle = guideline.get("angle")
|
|
|
|
if angle is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["angle"] = repr(angle)
|
2011-09-30 00:28:24 +00:00
|
|
|
name = guideline.get("name")
|
|
|
|
if name is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["name"] = name
|
2011-09-30 00:39:34 +00:00
|
|
|
color = guideline.get("color")
|
2011-09-30 00:28:24 +00:00
|
|
|
if color is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["color"] = color
|
2011-09-30 00:39:34 +00:00
|
|
|
identifier = guideline.get("identifier")
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and identifier in identifiers:
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("identifier used more than once: %s" % identifier)
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["identifier"] = identifier
|
2011-09-30 18:55:15 +00:00
|
|
|
identifiers.add(identifier)
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "guideline", attrs)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _writeAnchorsFormat1(pen, anchors, validate):
|
|
|
|
if validate and not anchorsValidator(anchors):
|
2011-10-17 16:39:25 +00:00
|
|
|
raise GlifLibError("anchors attribute does not have the proper structure.")
|
|
|
|
for anchor in anchors:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs = {}
|
2011-10-17 16:39:25 +00:00
|
|
|
x = anchor["x"]
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["x"] = repr(x)
|
2011-10-17 16:39:25 +00:00
|
|
|
y = anchor["y"]
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["y"] = repr(y)
|
2011-10-17 16:39:25 +00:00
|
|
|
name = anchor.get("name")
|
|
|
|
if name is not None:
|
2018-06-13 21:34:41 -04:00
|
|
|
attrs["name"] = name
|
2011-10-17 16:39:25 +00:00
|
|
|
pen.beginPath()
|
|
|
|
pen.addPoint((x, y), segmentType="move", name=name)
|
|
|
|
pen.endPath()
|
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def _writeAnchors(glyphObject, element, identifiers, validate):
|
2011-10-15 17:43:57 +00:00
|
|
|
anchors = getattr(glyphObject, "anchors", [])
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate and not anchorsValidator(anchors):
|
2011-10-15 17:43:57 +00:00
|
|
|
raise GlifLibError("anchors attribute does not have the proper structure.")
|
|
|
|
for anchor in anchors:
|
2018-06-14 00:31:29 -05:00
|
|
|
attrs = OrderedDict()
|
2011-10-15 17:43:57 +00:00
|
|
|
x = anchor["x"]
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["x"] = repr(x)
|
2011-10-15 17:43:57 +00:00
|
|
|
y = anchor["y"]
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["y"] = repr(y)
|
2011-10-15 17:43:57 +00:00
|
|
|
name = anchor.get("name")
|
|
|
|
if name is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["name"] = name
|
2011-10-15 17:43:57 +00:00
|
|
|
color = anchor.get("color")
|
|
|
|
if color is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["color"] = color
|
2011-10-15 17:43:57 +00:00
|
|
|
identifier = anchor.get("identifier")
|
|
|
|
if identifier is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and identifier in identifiers:
|
2011-10-15 17:43:57 +00:00
|
|
|
raise GlifLibError("identifier used more than once: %s" % identifier)
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["identifier"] = identifier
|
2011-10-15 17:43:57 +00:00
|
|
|
identifiers.add(identifier)
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(element, "anchor", attrs)
|
2011-10-15 17:43:57 +00:00
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def _writeLib(glyphObject, element, validate):
|
2008-01-07 17:40:34 +00:00
|
|
|
lib = getattr(glyphObject, "lib", None)
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate:
|
|
|
|
valid, message = glyphLibValidator(lib)
|
|
|
|
if not valid:
|
|
|
|
raise GlifLibError(message)
|
2011-10-06 19:23:48 +00:00
|
|
|
if not isinstance(lib, dict):
|
2015-11-02 13:21:05 +00:00
|
|
|
lib = dict(lib)
|
2018-06-13 14:22:51 -04:00
|
|
|
f = BytesIO()
|
|
|
|
plistWriter = PlistWriter(f, writeHeader=False) # TODO: fix indent
|
2011-09-30 00:28:24 +00:00
|
|
|
plistWriter.writeValue(lib)
|
2018-06-13 14:22:51 -04:00
|
|
|
text = f.getvalue()
|
2018-06-14 09:22:24 -05:00
|
|
|
text = etree.fromstring(text)
|
2018-06-14 11:55:54 -04:00
|
|
|
if len(text):
|
2018-06-14 09:22:24 -05:00
|
|
|
etree.SubElement(element, "lib").append(text)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:16:25 +00:00
|
|
|
# -----------------------
|
|
|
|
# layerinfo.plist Support
|
|
|
|
# -----------------------
|
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
layerInfoVersion3ValueData = {
|
2015-11-02 13:21:43 +00:00
|
|
|
"color" : dict(type=basestring, valueValidator=colorValidator),
|
2011-09-28 15:29:10 +00:00
|
|
|
"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.
|
|
|
|
"""
|
2017-11-04 19:37:01 +01:00
|
|
|
for attr, value in infoData.items():
|
2011-09-28 15:29:10 +00:00
|
|
|
if attr not in layerInfoVersion3ValueData:
|
|
|
|
raise GlifLibError("Unknown attribute %s." % attr)
|
|
|
|
isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value)
|
|
|
|
if not isValidValue:
|
|
|
|
raise GlifLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
|
2017-11-04 19:37:01 +01:00
|
|
|
return infoData
|
2011-09-28 15:29:10 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# -----------------
|
|
|
|
# GLIF Tree Support
|
|
|
|
# -----------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
def _glifTreeFromFile(aFile):
|
2018-06-14 13:28:20 -04:00
|
|
|
root = etree.parse(aFile).getroot()
|
2016-05-19 18:40:59 -07:00
|
|
|
if root.tag != "glyph":
|
|
|
|
raise GlifLibError("The GLIF is not properly formatted.")
|
2016-06-25 19:04:24 +01:00
|
|
|
if root.text and root.text.strip() != '':
|
2016-05-31 14:33:58 +01:00
|
|
|
raise GlifLibError("Invalid GLIF structure.")
|
2016-05-19 18:40:59 -07:00
|
|
|
return root
|
|
|
|
|
2018-07-10 15:39:58 +01:00
|
|
|
|
2016-05-19 18:40:59 -07:00
|
|
|
def _glifTreeFromString(aString):
|
2018-07-10 15:39:58 +01:00
|
|
|
data = tobytes(aString, encoding="utf-8")
|
|
|
|
root = etree.fromstring(data)
|
2016-05-19 18:40:59 -07:00
|
|
|
if root.tag != "glyph":
|
2011-09-29 01:14:14 +00:00
|
|
|
raise GlifLibError("The GLIF is not properly formatted.")
|
2016-06-25 19:04:24 +01:00
|
|
|
if root.text and root.text.strip() != '':
|
2016-05-31 14:33:58 +01:00
|
|
|
raise GlifLibError("Invalid GLIF structure.")
|
2016-05-19 18:40:59 -07:00
|
|
|
return root
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-07-04 13:56:44 -05:00
|
|
|
def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
|
2011-09-28 17:21:07 +00:00
|
|
|
# check the format version
|
2016-05-19 18:40:59 -07:00
|
|
|
formatVersion = tree.get("format")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and formatVersion is None:
|
2016-05-19 18:40:59 -07:00
|
|
|
raise GlifLibError("Unspecified format version in GLIF.")
|
2011-09-28 17:21:07 +00:00
|
|
|
try:
|
|
|
|
v = int(formatVersion)
|
|
|
|
formatVersion = v
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and formatVersion not in formatVersions:
|
2016-05-19 18:40:59 -07:00
|
|
|
raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion)
|
2011-10-17 19:31:38 +00:00
|
|
|
if formatVersion == 1:
|
2018-06-11 15:03:54 -05:00
|
|
|
_readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
|
2011-10-17 19:31:38 +00:00
|
|
|
elif formatVersion == 2:
|
2018-06-11 15:03:54 -05:00
|
|
|
_readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
|
2011-10-17 19:31:38 +00:00
|
|
|
else:
|
|
|
|
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
|
|
|
|
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None):
|
2011-09-28 17:21:07 +00:00
|
|
|
# get the name
|
2018-06-11 23:13:00 -05:00
|
|
|
_readName(glyphObject, tree, validate)
|
2011-10-17 19:31:38 +00:00
|
|
|
# populate the sub elements
|
|
|
|
unicodes = []
|
|
|
|
haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False
|
2016-05-19 18:40:59 -07:00
|
|
|
for element in tree:
|
|
|
|
if element.tag == "outline":
|
2018-06-11 23:13:00 -05:00
|
|
|
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.")
|
2011-10-17 19:31:38 +00:00
|
|
|
haveSeenOutline = True
|
2018-06-11 15:03:54 -05:00
|
|
|
buildOutlineFormat1(glyphObject, pointPen, element, validate)
|
2011-10-17 19:31:38 +00:00
|
|
|
elif glyphObject is None:
|
|
|
|
continue
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "advance":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and haveSeenAdvance:
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("The advance element occurs more than once.")
|
|
|
|
haveSeenAdvance = True
|
2016-05-19 18:40:59 -07:00
|
|
|
_readAdvance(glyphObject, element)
|
|
|
|
elif element.tag == "unicode":
|
2011-10-17 19:31:38 +00:00
|
|
|
try:
|
2016-05-19 18:40:59 -07:00
|
|
|
v = element.get("hex")
|
2011-10-17 19:31:38 +00:00
|
|
|
v = int(v, 16)
|
|
|
|
if v not in unicodes:
|
|
|
|
unicodes.append(v)
|
|
|
|
except ValueError:
|
|
|
|
raise GlifLibError("Illegal value for hex attribute of unicode element.")
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "note":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and haveSeenNote:
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("The note element occurs more than once.")
|
|
|
|
haveSeenNote = True
|
2016-05-19 18:40:59 -07:00
|
|
|
_readNote(glyphObject, element)
|
|
|
|
elif element.tag == "lib":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and haveSeenLib:
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("The lib element occurs more than once.")
|
|
|
|
haveSeenLib = True
|
2018-06-11 15:03:54 -05:00
|
|
|
_readLib(glyphObject, element, validate)
|
2011-10-17 19:31:38 +00:00
|
|
|
else:
|
|
|
|
raise GlifLibError("Unknown element in GLIF: %s" % element)
|
|
|
|
# set the collected unicodes
|
|
|
|
if unicodes:
|
|
|
|
_relaxedSetattr(glyphObject, "unicodes", unicodes)
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=None):
|
2011-10-17 19:31:38 +00:00
|
|
|
# get the name
|
2018-06-11 23:13:00 -05:00
|
|
|
_readName(glyphObject, tree, validate)
|
2011-09-28 17:21:07 +00:00
|
|
|
# populate the sub elements
|
|
|
|
unicodes = []
|
2011-09-28 20:58:42 +00:00
|
|
|
guidelines = []
|
2011-10-15 17:43:57 +00:00
|
|
|
anchors = []
|
2011-09-29 14:21:02 +00:00
|
|
|
haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = False
|
2011-09-28 18:07:13 +00:00
|
|
|
identifiers = set()
|
2016-05-19 18:40:59 -07:00
|
|
|
for element in tree:
|
|
|
|
if element.tag == "outline":
|
2018-06-11 23:13:00 -05:00
|
|
|
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.")
|
2011-09-29 14:21:02 +00:00
|
|
|
haveSeenOutline = True
|
2008-01-07 17:40:34 +00:00
|
|
|
if pointPen is not None:
|
2018-06-11 15:03:54 -05:00
|
|
|
buildOutlineFormat2(glyphObject, pointPen, element, identifiers, validate)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif glyphObject is None:
|
|
|
|
continue
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "advance":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and haveSeenAdvance:
|
2011-09-29 14:21:02 +00:00
|
|
|
raise GlifLibError("The advance element occurs more than once.")
|
|
|
|
haveSeenAdvance = True
|
2016-05-19 18:40:59 -07:00
|
|
|
_readAdvance(glyphObject, element)
|
|
|
|
elif element.tag == "unicode":
|
2011-09-28 20:39:28 +00:00
|
|
|
try:
|
2016-05-19 18:40:59 -07:00
|
|
|
v = element.get("hex")
|
2011-09-28 20:39:28 +00:00
|
|
|
v = int(v, 16)
|
2011-09-30 15:25:47 +00:00
|
|
|
if v not in unicodes:
|
|
|
|
unicodes.append(v)
|
2011-09-28 20:39:28 +00:00
|
|
|
except ValueError:
|
|
|
|
raise GlifLibError("Illegal value for hex attribute of unicode element.")
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "guideline":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and len(element):
|
2011-09-28 20:58:42 +00:00
|
|
|
raise GlifLibError("Unknown children in guideline element.")
|
2018-06-14 13:28:20 -04:00
|
|
|
attrib = dict(element.attrib)
|
2011-09-30 00:39:34 +00:00
|
|
|
for attr in ("x", "y", "angle"):
|
2018-06-14 13:28:20 -04:00
|
|
|
if attr in attrib:
|
|
|
|
attrib[attr] = _number(attrib[attr])
|
|
|
|
guidelines.append(attrib)
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "anchor":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and len(element):
|
2011-10-15 17:43:57 +00:00
|
|
|
raise GlifLibError("Unknown children in anchor element.")
|
2018-06-14 13:28:20 -04:00
|
|
|
attrib = dict(element.attrib)
|
2011-10-15 17:43:57 +00:00
|
|
|
for attr in ("x", "y"):
|
2016-05-19 18:40:59 -07:00
|
|
|
if attr in element.attrib:
|
2018-06-14 13:28:20 -04:00
|
|
|
attrib[attr] = _number(attrib[attr])
|
|
|
|
anchors.append(attrib)
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "image":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate:
|
|
|
|
if haveSeenImage:
|
|
|
|
raise GlifLibError("The image element occurs more than once.")
|
|
|
|
if len(element):
|
|
|
|
raise GlifLibError("Unknown children in image element.")
|
2011-09-29 00:29:57 +00:00
|
|
|
haveSeenImage = True
|
2018-06-11 15:03:54 -05:00
|
|
|
_readImage(glyphObject, element, validate)
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "note":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and haveSeenNote:
|
2011-09-29 14:21:02 +00:00
|
|
|
raise GlifLibError("The note element occurs more than once.")
|
|
|
|
haveSeenNote = True
|
2016-05-19 18:40:59 -07:00
|
|
|
_readNote(glyphObject, element)
|
|
|
|
elif element.tag == "lib":
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and haveSeenLib:
|
2011-09-29 14:21:02 +00:00
|
|
|
raise GlifLibError("The lib element occurs more than once.")
|
|
|
|
haveSeenLib = True
|
2018-06-11 15:03:54 -05:00
|
|
|
_readLib(glyphObject, element, validate)
|
2011-09-28 17:21:07 +00:00
|
|
|
else:
|
|
|
|
raise GlifLibError("Unknown element in GLIF: %s" % element)
|
|
|
|
# set the collected unicodes
|
2008-01-07 17:40:34 +00:00
|
|
|
if unicodes:
|
|
|
|
_relaxedSetattr(glyphObject, "unicodes", unicodes)
|
2011-09-28 20:58:42 +00:00
|
|
|
# set the collected guidelines
|
2011-10-17 19:31:38 +00:00
|
|
|
if guidelines:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not guidelinesValidator(guidelines, identifiers):
|
2011-09-29 00:29:57 +00:00
|
|
|
raise GlifLibError("The guidelines are improperly formatted.")
|
2011-09-28 20:58:42 +00:00
|
|
|
_relaxedSetattr(glyphObject, "guidelines", guidelines)
|
2011-10-15 17:43:57 +00:00
|
|
|
# set the collected anchors
|
2011-10-17 19:31:38 +00:00
|
|
|
if anchors:
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not anchorsValidator(anchors, identifiers):
|
2011-10-15 17:43:57 +00:00
|
|
|
raise GlifLibError("The anchors are improperly formatted.")
|
|
|
|
_relaxedSetattr(glyphObject, "anchors", anchors)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-06-11 23:13:00 -05:00
|
|
|
def _readName(glyphObject, root, validate):
|
2016-05-19 18:40:59 -07:00
|
|
|
glyphName = root.get("name")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and not glyphName:
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("Empty glyph name in GLIF.")
|
|
|
|
if glyphName and glyphObject is not None:
|
|
|
|
_relaxedSetattr(glyphObject, "name", glyphName)
|
|
|
|
|
2016-05-19 18:40:59 -07:00
|
|
|
def _readAdvance(glyphObject, advance):
|
|
|
|
width = _number(advance.get("width", 0))
|
2011-10-16 13:32:03 +00:00
|
|
|
_relaxedSetattr(glyphObject, "width", width)
|
2016-05-19 18:40:59 -07:00
|
|
|
height = _number(advance.get("height", 0))
|
2011-10-16 13:32:03 +00:00
|
|
|
_relaxedSetattr(glyphObject, "height", height)
|
|
|
|
|
2016-05-19 18:40:59 -07:00
|
|
|
def _readNote(glyphObject, note):
|
|
|
|
lines = note.text.split("\n")
|
|
|
|
note = "\n".join(line.strip() for line in lines if line.strip())
|
2011-10-16 13:32:03 +00:00
|
|
|
_relaxedSetattr(glyphObject, "note", note)
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _readLib(glyphObject, lib, validate):
|
2016-05-19 18:40:59 -07:00
|
|
|
assert len(lib) == 1
|
|
|
|
child = lib[0]
|
|
|
|
plist = readPlistFromTree(child)
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate:
|
|
|
|
valid, message = glyphLibValidator(plist)
|
|
|
|
if not valid:
|
|
|
|
raise GlifLibError(message)
|
2016-05-19 18:40:59 -07:00
|
|
|
_relaxedSetattr(glyphObject, "lib", plist)
|
2011-10-16 13:32:03 +00:00
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _readImage(glyphObject, image, validate):
|
2018-06-14 13:28:20 -04:00
|
|
|
imageData = dict(image.attrib)
|
2011-10-17 19:31:38 +00:00
|
|
|
for attr, default in _transformationInfo:
|
2016-05-19 18:40:59 -07:00
|
|
|
value = imageData.get(attr, default)
|
2011-10-17 19:31:38 +00:00
|
|
|
imageData[attr] = _number(value)
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate and not imageValidator(imageData):
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("The image element is not properly formatted.")
|
|
|
|
_relaxedSetattr(glyphObject, "image", imageData)
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# ----------------
|
|
|
|
# GLIF to PointPen
|
|
|
|
# ----------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-10-17 19:31:38 +00:00
|
|
|
contourAttributesFormat2 = set(["identifier"])
|
|
|
|
componentAttributesFormat1 = set(["base", "xScale", "xyScale", "yxScale", "yScale", "xOffset", "yOffset"])
|
|
|
|
componentAttributesFormat2 = componentAttributesFormat1 | set(["identifier"])
|
|
|
|
pointAttributesFormat1 = set(["x", "y", "type", "smooth", "name"])
|
|
|
|
pointAttributesFormat2 = pointAttributesFormat1 | set(["identifier"])
|
|
|
|
pointSmoothOptions = set(("no", "yes"))
|
|
|
|
pointTypeOptions = set(["move", "line", "offcurve", "curve", "qcurve"])
|
|
|
|
|
|
|
|
# format 1
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def buildOutlineFormat1(glyphObject, pen, outline, validate):
|
2011-10-17 13:29:07 +00:00
|
|
|
anchors = []
|
2016-05-19 18:40:59 -07:00
|
|
|
for element in outline:
|
|
|
|
if element.tag == "contour":
|
|
|
|
if len(element) == 1:
|
|
|
|
point = element[0]
|
|
|
|
if point.tag == "point":
|
2018-06-11 23:13:00 -05:00
|
|
|
anchor = _buildAnchorFormat1(point, validate)
|
2011-10-17 13:29:07 +00:00
|
|
|
if anchor is not None:
|
|
|
|
anchors.append(anchor)
|
|
|
|
continue
|
2015-11-03 16:38:21 +00:00
|
|
|
if pen is not None:
|
2018-06-11 15:03:54 -05:00
|
|
|
_buildOutlineContourFormat1(pen, element, validate)
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "component":
|
2015-11-03 16:38:21 +00:00
|
|
|
if pen is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
_buildOutlineComponentFormat1(pen, element, validate)
|
2011-09-30 16:00:20 +00:00
|
|
|
else:
|
|
|
|
raise GlifLibError("Unknown element in outline element: %s" % element)
|
2011-10-17 19:31:38 +00:00
|
|
|
if glyphObject is not None and anchors:
|
2018-06-11 15:03:54 -05:00
|
|
|
if validate and not anchorsValidator(anchors):
|
2011-10-17 16:27:33 +00:00
|
|
|
raise GlifLibError("GLIF 1 anchors are not properly formatted.")
|
|
|
|
_relaxedSetattr(glyphObject, "anchors", anchors)
|
2011-09-28 20:27:55 +00:00
|
|
|
|
2018-06-11 23:13:00 -05:00
|
|
|
def _buildAnchorFormat1(point, validate):
|
2011-10-17 13:29:07 +00:00
|
|
|
if point.get("type") != "move":
|
|
|
|
return None
|
2018-01-26 23:18:25 +01:00
|
|
|
name = point.get("name")
|
|
|
|
if name is None:
|
|
|
|
return None
|
2011-10-17 16:27:33 +00:00
|
|
|
x = point.get("x")
|
|
|
|
y = point.get("y")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and x is None:
|
2011-10-17 16:27:33 +00:00
|
|
|
raise GlifLibError("Required x attribute is missing in point element.")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and y is None:
|
2011-10-17 16:27:33 +00:00
|
|
|
raise GlifLibError("Required y attribute is missing in point element.")
|
|
|
|
x = _number(x)
|
|
|
|
y = _number(y)
|
2011-10-17 16:39:25 +00:00
|
|
|
anchor = dict(x=x, y=y, name=name)
|
2011-10-17 13:29:07 +00:00
|
|
|
return anchor
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _buildOutlineContourFormat1(pen, contour, validate):
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and contour.attrib:
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("Unknown attributes in contour element.")
|
|
|
|
pen.beginPath()
|
2016-06-05 19:07:45 +01:00
|
|
|
if len(contour):
|
2018-06-14 13:28:20 -04:00
|
|
|
massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat1, openContourOffCurveLeniency=True, validate=validate)
|
|
|
|
_buildOutlinePointsFormat1(pen, massaged)
|
2011-10-17 19:31:38 +00:00
|
|
|
pen.endPath()
|
|
|
|
|
2016-05-19 18:40:59 -07:00
|
|
|
def _buildOutlinePointsFormat1(pen, contour):
|
2018-06-14 13:28:20 -04:00
|
|
|
for point in contour:
|
|
|
|
x = point["x"]
|
|
|
|
y = point["y"]
|
|
|
|
segmentType = point["segmentType"]
|
|
|
|
smooth = point["smooth"]
|
|
|
|
name = point["name"]
|
2011-10-17 19:31:38 +00:00
|
|
|
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
|
|
|
|
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2016-05-19 18:40:59 -07:00
|
|
|
baseGlyphName = component.get("base")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and baseGlyphName is None:
|
2011-10-17 19:31:38 +00:00
|
|
|
raise GlifLibError("The base attribute is not defined in the component.")
|
|
|
|
transformation = []
|
|
|
|
for attr, default in _transformationInfo:
|
2016-05-19 18:40:59 -07:00
|
|
|
value = component.get(attr)
|
2011-10-17 19:31:38 +00:00
|
|
|
if value is None:
|
|
|
|
value = default
|
|
|
|
else:
|
|
|
|
value = _number(value)
|
|
|
|
transformation.append(value)
|
|
|
|
pen.addComponent(baseGlyphName, tuple(transformation))
|
|
|
|
|
|
|
|
# format 2
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate):
|
2016-05-19 18:40:59 -07:00
|
|
|
for element in outline:
|
|
|
|
if element.tag == "contour":
|
2018-06-11 15:03:54 -05:00
|
|
|
_buildOutlineContourFormat2(pen, element, identifiers, validate)
|
2016-05-19 18:40:59 -07:00
|
|
|
elif element.tag == "component":
|
2018-06-11 15:03:54 -05:00
|
|
|
_buildOutlineComponentFormat2(pen, element, identifiers, validate)
|
2011-10-17 19:31:38 +00:00
|
|
|
else:
|
2016-05-19 18:40:59 -07:00
|
|
|
raise GlifLibError("Unknown element in outline element: %s" % element.tag)
|
2011-10-17 19:31:38 +00:00
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _buildOutlineContourFormat2(pen, contour, identifiers, validate):
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate:
|
|
|
|
for attr in contour.attrib.keys():
|
|
|
|
if attr not in contourAttributesFormat2:
|
|
|
|
raise GlifLibError("Unknown attribute in contour element: %s" % attr)
|
2016-05-19 18:40:59 -07:00
|
|
|
identifier = contour.get("identifier")
|
2011-09-28 20:27:55 +00:00
|
|
|
if identifier is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2011-09-28 20:27:55 +00:00
|
|
|
identifiers.add(identifier)
|
2011-09-30 16:00:20 +00:00
|
|
|
try:
|
2011-09-30 16:58:04 +00:00
|
|
|
pen.beginPath(identifier=identifier)
|
2011-09-30 16:00:20 +00:00
|
|
|
except TypeError:
|
2011-09-28 20:27:55 +00:00
|
|
|
pen.beginPath()
|
2016-05-19 18:40:59 -07:00
|
|
|
warn("The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.", DeprecationWarning)
|
2016-06-05 19:07:45 +01:00
|
|
|
if len(contour):
|
2018-06-14 13:28:20 -04:00
|
|
|
massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat2, validate=validate)
|
|
|
|
_buildOutlinePointsFormat2(pen, massaged, identifiers, validate)
|
2011-09-28 20:27:55 +00:00
|
|
|
pen.endPath()
|
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _buildOutlinePointsFormat2(pen, contour, identifiers, validate):
|
2018-06-14 13:28:20 -04:00
|
|
|
for point in contour:
|
|
|
|
x = point["x"]
|
|
|
|
y = point["y"]
|
|
|
|
segmentType = point["segmentType"]
|
|
|
|
smooth = point["smooth"]
|
|
|
|
name = point["name"]
|
|
|
|
identifier = point.get("identifier")
|
2011-10-17 19:31:38 +00:00
|
|
|
if identifier is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2011-10-17 19:31:38 +00:00
|
|
|
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)
|
2016-05-19 18:40:59 -07:00
|
|
|
warn("The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.", DeprecationWarning)
|
2011-10-17 19:31:38 +00:00
|
|
|
|
2018-06-11 15:03:54 -05:00
|
|
|
def _buildOutlineComponentFormat2(pen, component, identifiers, validate):
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2016-05-19 18:40:59 -07:00
|
|
|
baseGlyphName = component.get("base")
|
2018-06-11 23:13:00 -05:00
|
|
|
if validate and baseGlyphName is None:
|
2011-09-28 20:27:55 +00:00
|
|
|
raise GlifLibError("The base attribute is not defined in the component.")
|
|
|
|
transformation = []
|
|
|
|
for attr, default in _transformationInfo:
|
2016-05-19 18:40:59 -07:00
|
|
|
value = component.get(attr)
|
2011-09-28 20:27:55 +00:00
|
|
|
if value is None:
|
|
|
|
value = default
|
|
|
|
else:
|
|
|
|
value = _number(value)
|
|
|
|
transformation.append(value)
|
2016-05-19 18:40:59 -07:00
|
|
|
identifier = component.get("identifier")
|
2011-09-28 20:27:55 +00:00
|
|
|
if identifier is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2011-09-28 20:27:55 +00:00
|
|
|
identifiers.add(identifier)
|
|
|
|
try:
|
|
|
|
pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier)
|
|
|
|
except TypeError:
|
|
|
|
pen.addComponent(baseGlyphName, tuple(transformation))
|
2016-05-19 18:40:59 -07:00
|
|
|
warn("The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.", DeprecationWarning)
|
2011-09-28 19:04:22 +00:00
|
|
|
|
2011-10-17 19:31:38 +00:00
|
|
|
# all formats
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2018-07-04 13:56:44 -05:00
|
|
|
def _validateAndMassagePointStructures(contour, pointAttributes, openContourOffCurveLeniency=False, validate=True):
|
2016-06-05 19:07:45 +01:00
|
|
|
if not len(contour):
|
2016-05-19 18:40:59 -07:00
|
|
|
return
|
2011-12-06 15:35:44 +00:00
|
|
|
# store some data for later validation
|
2016-05-19 18:40:59 -07:00
|
|
|
lastOnCurvePoint = None
|
2011-12-06 15:35:44 +00:00
|
|
|
haveOffCurvePoint = False
|
2011-10-17 19:31:38 +00:00
|
|
|
# validate and massage the individual point elements
|
2018-06-14 13:28:20 -04:00
|
|
|
massaged = []
|
2016-05-19 18:40:59 -07:00
|
|
|
for index, element in enumerate(contour):
|
2011-10-17 19:31:38 +00:00
|
|
|
# not <point>
|
2016-05-19 18:40:59 -07:00
|
|
|
if element.tag != "point":
|
|
|
|
raise GlifLibError("Unknown child element (%s) of contour element." % element.tag)
|
2018-06-14 13:28:20 -04:00
|
|
|
point = dict(element.attrib)
|
|
|
|
massaged.append(point)
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate:
|
|
|
|
# unknown attributes
|
2018-06-14 13:28:20 -04:00
|
|
|
for attr in point.keys():
|
2018-06-11 21:44:50 -04:00
|
|
|
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.")
|
2011-09-28 20:27:55 +00:00
|
|
|
# x and y are required
|
2017-11-04 19:37:01 +01:00
|
|
|
for attr in ("x", "y"):
|
|
|
|
value = element.get(attr)
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate and value is None:
|
2017-11-04 19:37:01 +01:00
|
|
|
raise GlifLibError("Required %s attribute is missing in point element." % attr)
|
2018-06-14 13:28:20 -04:00
|
|
|
point[attr] = _number(value)
|
2011-10-17 19:31:38 +00:00
|
|
|
# segment type
|
2018-06-14 13:28:20 -04:00
|
|
|
pointType = point.pop("type", "offcurve")
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate and pointType not in pointTypeOptions:
|
2011-12-06 15:35:44 +00:00
|
|
|
raise GlifLibError("Unknown point type: %s" % pointType)
|
|
|
|
if pointType == "offcurve":
|
|
|
|
pointType = None
|
2018-06-14 13:28:20 -04:00
|
|
|
point["segmentType"] = pointType
|
2011-12-06 15:35:44 +00:00
|
|
|
if pointType is None:
|
|
|
|
haveOffCurvePoint = True
|
|
|
|
else:
|
2016-05-19 18:40:59 -07:00
|
|
|
lastOnCurvePoint = index
|
2011-09-28 20:27:55 +00:00
|
|
|
# move can only occur as the first point
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate and pointType == "move" and index != 0:
|
2011-09-28 20:27:55 +00:00
|
|
|
raise GlifLibError("A move point occurs after the first point in the contour.")
|
2011-10-17 19:31:38 +00:00
|
|
|
# smooth is optional
|
2018-06-14 13:28:20 -04:00
|
|
|
smooth = point.get("smooth", "no")
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate and smooth is not None:
|
2011-09-28 20:27:55 +00:00
|
|
|
if smooth not in pointSmoothOptions:
|
|
|
|
raise GlifLibError("Unknown point smooth value: %s" % smooth)
|
|
|
|
smooth = smooth == "yes"
|
2018-06-14 13:28:20 -04:00
|
|
|
point["smooth"] = smooth
|
2011-10-17 19:31:38 +00:00
|
|
|
# smooth can only be applied to curve and qcurve
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate and smooth and pointType is None:
|
2012-01-17 15:13:52 +00:00
|
|
|
raise GlifLibError("smooth attribute set in an offcurve point.")
|
2011-10-17 19:31:38 +00:00
|
|
|
# name is optional
|
2016-05-19 18:40:59 -07:00
|
|
|
if "name" not in element.attrib:
|
2018-06-14 13:28:20 -04:00
|
|
|
point["name"] = None
|
2011-10-18 12:53:29 +00:00
|
|
|
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.
|
2018-06-14 13:28:20 -04:00
|
|
|
if massaged[0]["segmentType"] == "move":
|
|
|
|
count = 0
|
|
|
|
for point in reversed(massaged):
|
|
|
|
if point["segmentType"] is None:
|
|
|
|
count += 1
|
2011-12-06 15:35:44 +00:00
|
|
|
else:
|
2016-05-19 18:40:59 -07:00
|
|
|
break
|
2018-06-14 13:28:20 -04:00
|
|
|
if count:
|
|
|
|
massaged = massaged[:-count]
|
2016-05-19 18:40:59 -07:00
|
|
|
# validate the off-curves in the segments
|
2018-06-11 21:44:50 -04:00
|
|
|
if validate and haveOffCurvePoint and lastOnCurvePoint is not None:
|
2016-05-19 18:40:59 -07:00
|
|
|
# we only care about how many offCurves there are before an onCurve
|
|
|
|
# filter out the trailing offCurves
|
2018-06-14 13:28:20 -04:00
|
|
|
offCurvesCount = len(massaged) - 1 - lastOnCurvePoint
|
|
|
|
for point in massaged:
|
|
|
|
segmentType = point["segmentType"]
|
2016-05-19 18:40:59 -07:00
|
|
|
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
|
2018-06-14 13:28:20 -04:00
|
|
|
return massaged
|
2011-09-28 20:27:55 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# ---------------------
|
|
|
|
# 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.
|
2011-09-28 17:27:25 +00:00
|
|
|
|
|
|
|
>>> _number("1")
|
|
|
|
1
|
|
|
|
>>> _number("1.0")
|
|
|
|
1.0
|
2016-09-11 16:47:39 +01:00
|
|
|
>>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL
|
2011-09-28 17:27:25 +00:00
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
GlifLibError: Could not convert a to an int or float.
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
n = int(s)
|
2011-09-28 17:27:25 +00:00
|
|
|
return n
|
2011-09-28 13:13:07 +00:00
|
|
|
except ValueError:
|
2011-09-28 17:27:25 +00:00
|
|
|
pass
|
|
|
|
try:
|
2011-09-28 13:13:07 +00:00
|
|
|
n = float(s)
|
2011-09-28 17:27:25 +00:00
|
|
|
return n
|
|
|
|
except ValueError:
|
|
|
|
raise GlifLibError("Could not convert %s to an int or float." % s)
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
# --------------------
|
|
|
|
# Rapid Value Fetching
|
|
|
|
# --------------------
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
# base
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
class _DoneParsing(Exception): pass
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
class _BaseParser(object):
|
|
|
|
|
|
|
|
def __init__(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
self._elementStack = []
|
2011-10-11 14:08:24 +00:00
|
|
|
|
|
|
|
def parse(self, text):
|
|
|
|
from xml.parsers.expat import ParserCreate
|
2011-09-28 13:13:07 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
# 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(_FetchUnicodesParser, self).__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(_FetchUnicodesParser, self).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(_FetchImageFileNameParser, self).__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(_FetchImageFileNameParser, self).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(_FetchComponentBasesParser, self).__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(_FetchComponentBasesParser, self).startElementHandler(name, attrs)
|
|
|
|
|
|
|
|
def endElementHandler(self, name):
|
|
|
|
if name == "outline":
|
|
|
|
raise _DoneParsing
|
|
|
|
super(_FetchComponentBasesParser, self).endElementHandler(name)
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# --------------
|
|
|
|
# GLIF Point Pen
|
|
|
|
# --------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
_transformationInfo = [
|
|
|
|
# field name, default value
|
|
|
|
("xScale", 1),
|
|
|
|
("xyScale", 0),
|
|
|
|
("yxScale", 0),
|
|
|
|
("yScale", 1),
|
|
|
|
("xOffset", 0),
|
|
|
|
("yOffset", 0),
|
|
|
|
]
|
|
|
|
|
|
|
|
class GLIFPointPen(AbstractPointPen):
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Helper class using the PointPen protocol to write the <outline>
|
2008-01-07 17:40:34 +00:00
|
|
|
part of .glif files.
|
|
|
|
"""
|
|
|
|
|
2018-06-13 14:22:51 -04:00
|
|
|
def __init__(self, element, formatVersion=2, identifiers=None, validate=True):
|
2011-09-29 20:55:20 +00:00
|
|
|
if identifiers is None:
|
|
|
|
identifiers = set()
|
|
|
|
self.formatVersion = formatVersion
|
|
|
|
self.identifiers = identifiers
|
2018-06-13 14:22:51 -04:00
|
|
|
self.outline = element
|
|
|
|
self.contour = None
|
2011-09-30 18:28:15 +00:00
|
|
|
self.prevOffCurveCount = 0
|
2011-10-18 13:03:59 +00:00
|
|
|
self.prevPointTypes = []
|
2018-06-11 15:03:54 -05:00
|
|
|
self.validate = validate
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
def beginPath(self, identifier=None, **kwargs):
|
2018-06-14 00:31:29 -05:00
|
|
|
attrs = OrderedDict()
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None and self.formatVersion >= 2:
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["identifier"] = identifier
|
2011-09-30 18:55:15 +00:00
|
|
|
self.identifiers.add(identifier)
|
2018-06-13 14:22:51 -04:00
|
|
|
self.contour = etree.SubElement(self.outline, "contour", attrs)
|
2011-09-30 18:28:15 +00:00
|
|
|
self.prevOffCurveCount = 0
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
def endPath(self):
|
2011-10-18 13:03:59 +00:00
|
|
|
if self.prevPointTypes and self.prevPointTypes[0] == "move":
|
2018-06-11 23:13:00 -05:00
|
|
|
if self.validate and self.prevPointTypes[-1] == "offcurve":
|
2011-10-18 13:03:59 +00:00
|
|
|
raise GlifLibError("open contour has loose offcurve point")
|
2018-06-14 08:36:52 -04:00
|
|
|
# prevent lxml from writing self-closing tags
|
|
|
|
if not len(self.contour):
|
2018-06-14 10:59:51 -04:00
|
|
|
self.contour.text = "\n "
|
2018-06-13 14:22:51 -04:00
|
|
|
self.contour = None
|
2011-10-18 13:03:59 +00:00
|
|
|
self.prevPointType = None
|
2011-09-30 18:28:15 +00:00
|
|
|
self.prevOffCurveCount = 0
|
2011-10-18 13:03:59 +00:00
|
|
|
self.prevPointTypes = []
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
def addPoint(self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs):
|
2018-06-14 00:31:29 -05:00
|
|
|
attrs = OrderedDict()
|
2011-09-30 17:04:00 +00:00
|
|
|
# coordinates
|
2008-01-07 17:40:34 +00:00
|
|
|
if pt is not None:
|
2018-06-11 23:13:00 -05:00
|
|
|
if self.validate:
|
|
|
|
for coord in pt:
|
|
|
|
if not isinstance(coord, (int, float)):
|
|
|
|
raise GlifLibError("coordinates must be int or float")
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["x"] = repr(pt[0])
|
|
|
|
attrs["y"] = repr(pt[1])
|
2011-09-30 17:04:00 +00:00
|
|
|
# segment type
|
2011-10-18 13:03:59 +00:00
|
|
|
if segmentType == "offcurve":
|
|
|
|
segmentType = None
|
2018-06-11 23:13:00 -05:00
|
|
|
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.")
|
2008-01-07 17:40:34 +00:00
|
|
|
if segmentType is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["type"] = segmentType
|
2011-09-30 18:28:15 +00:00
|
|
|
else:
|
|
|
|
segmentType = "offcurve"
|
|
|
|
if segmentType == "offcurve":
|
|
|
|
self.prevOffCurveCount += 1
|
|
|
|
else:
|
|
|
|
self.prevOffCurveCount = 0
|
2011-10-18 13:03:59 +00:00
|
|
|
self.prevPointTypes.append(segmentType)
|
2011-09-30 17:04:00 +00:00
|
|
|
# smooth
|
2008-01-07 17:40:34 +00:00
|
|
|
if smooth:
|
2018-06-11 23:13:00 -05:00
|
|
|
if self.validate and segmentType == "offcurve":
|
2012-01-17 15:13:52 +00:00
|
|
|
raise GlifLibError("can't set smooth in an offcurve point.")
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["smooth"] = "yes"
|
2011-09-30 17:04:00 +00:00
|
|
|
# name
|
2008-01-07 17:40:34 +00:00
|
|
|
if name is not None:
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["name"] = name
|
2011-09-30 17:04:00 +00:00
|
|
|
# identifier
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None and self.formatVersion >= 2:
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["identifier"] = identifier
|
2011-09-30 18:55:15 +00:00
|
|
|
self.identifiers.add(identifier)
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(self.contour, "point", attrs)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
|
2018-06-14 00:31:29 -05:00
|
|
|
attrs = OrderedDict([("base", glyphName)])
|
2008-01-07 17:40:34 +00:00
|
|
|
for (attr, default), value in zip(_transformationInfo, transformation):
|
2018-06-11 23:13:00 -05:00
|
|
|
if self.validate and not isinstance(value, (int, float)):
|
2011-09-29 20:55:20 +00:00
|
|
|
raise GlifLibError("transformation values must be int or float")
|
2008-01-07 17:40:34 +00:00
|
|
|
if value != default:
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs[attr] = repr(value)
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None and self.formatVersion >= 2:
|
2018-06-11 23:13:00 -05:00
|
|
|
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)
|
2018-06-13 14:22:51 -04:00
|
|
|
attrs["identifier"] = identifier
|
2011-09-30 18:55:15 +00:00
|
|
|
self.identifiers.add(identifier)
|
2018-06-13 14:22:51 -04:00
|
|
|
etree.SubElement(self.outline, "component", attrs)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2011-09-28 17:27:25 +00:00
|
|
|
import doctest
|
|
|
|
doctest.testmod()
|