| # -*- coding: utf-8 -*- |
| """ |
| 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 absolute_import, unicode_literals |
| 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.py23 import basestring, unicode, tobytes, tounicode |
| 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 |
| from fontTools.ufoLib.utils import integerTypes, numberTypes |
| |
| |
| __all__ = [ |
| "GlyphSet", |
| "GlifLibError", |
| "readGlyphFromString", "writeGlyphToString", |
| "glyphNameToFileName" |
| ] |
| |
| |
| # --------- |
| # Constants |
| # --------- |
| |
| CONTENTS_FILENAME = "contents.plist" |
| LAYERINFO_FILENAME = "layerinfo.plist" |
| supportedUFOFormatVersions = [1, 2, 3] |
| supportedGLIFFormatVersions = [1, 2] |
| |
| |
| # ------------ |
| # Simple Glyph |
| # ------------ |
| |
| class Glyph(object): |
| |
| """ |
| 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): |
| """ |
| 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) |
| |
| |
| # --------- |
| # Glyph Set |
| # --------- |
| |
| class GlyphSet(_UFOBaseIO): |
| |
| """ |
| GlyphSet manages a set of .glif files inside one directory. |
| |
| GlyphSet's constructor takes a path to an existing directory as it's |
| first argument. Reading glyph data can either be done through the |
| readGlyph() method, or by using GlyphSet's dictionary interface, where |
| the keys are glyph names and the values are (very) simple glyph objects. |
| |
| To write a glyph to the glyph set, you use the writeGlyph() method. |
| The simple glyph objects returned through the dict interface do not |
| support writing, they are just a convenient way to get at the glyph data. |
| """ |
| |
| glyphClass = Glyph |
| |
| def __init__( |
| self, |
| path, |
| glyphNameToFileNameFunc=None, |
| ufoFormatVersion=3, |
| validateRead=True, |
| validateWrite=True, |
| ): |
| """ |
| '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``. |
| """ |
| if ufoFormatVersion not in supportedUFOFormatVersions: |
| raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion) |
| if isinstance(path, basestring): |
| try: |
| filesystem = fs.osfs.OSFS(path) |
| except fs.errors.CreateFailed: |
| raise GlifLibError("No glyphs directory '%s'" % path) |
| self._shouldClose = True |
| elif isinstance(path, fs.base.FS): |
| filesystem = path |
| try: |
| filesystem.check() |
| except fs.errors.FilesystemClosed: |
| raise GlifLibError("the filesystem '%s' is closed" % filesystem) |
| self._shouldClose = False |
| else: |
| raise TypeError( |
| "Expected a path string or fs object, found %s" |
| % type(path).__name__ |
| ) |
| try: |
| path = filesystem.getsyspath("/") |
| except fs.errors.NoSysPath: |
| # network or in-memory FS may not map to the local one |
| path = unicode(filesystem) |
| # 'dirName' is kept for backward compatibility only, but it's DEPRECATED |
| # as it's not guaranteed that it maps to an existing OSFS directory. |
| # Client could use the FS api via the `self.fs` attribute instead. |
| self.dirName = fs.path.parts(path)[-1] |
| self.fs = filesystem |
| # if glyphSet contains no 'contents.plist', we consider it empty |
| self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) |
| self.ufoFormatVersion = ufoFormatVersion |
| if glyphNameToFileNameFunc is None: |
| glyphNameToFileNameFunc = glyphNameToFileName |
| self.glyphNameToFileName = glyphNameToFileNameFunc |
| self._validateRead = validateRead |
| self._validateWrite = validateWrite |
| self._existingFileNames = None |
| self._reverseContents = None |
| |
| self.rebuildContents() |
| |
| def rebuildContents(self, validateRead=None): |
| """ |
| Rebuild the contents dict by loading contents.plist. |
| |
| ``validateRead`` will validate the data, by default it is set to the |
| class's ``validateRead`` value, can be overridden. |
| """ |
| if validateRead is None: |
| validateRead = self._validateRead |
| contents = self._getPlist(CONTENTS_FILENAME, {}) |
| # validate the contents |
| if validateRead: |
| invalidFormat = False |
| if not isinstance(contents, dict): |
| invalidFormat = True |
| else: |
| for name, fileName in contents.items(): |
| if not isinstance(name, basestring): |
| invalidFormat = True |
| if not isinstance(fileName, basestring): |
| invalidFormat = True |
| elif not self.fs.exists(fileName): |
| raise GlifLibError( |
| "%s references a file that does not exist: %s" |
| % (CONTENTS_FILENAME, fileName) |
| ) |
| if invalidFormat: |
| raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME) |
| self.contents = contents |
| self._existingFileNames = None |
| self._reverseContents = None |
| |
| def getReverseContents(self): |
| """ |
| Return a reversed dict of self.contents, mapping file names to |
| glyph names. This is primarily an aid for custom glyph name to file |
| name schemes that want to make sure they don't generate duplicate |
| file names. The file names are converted to lowercase so we can |
| reliably check for duplicates that only differ in case, which is |
| important for case-insensitive file systems. |
| """ |
| if self._reverseContents is None: |
| d = {} |
| for k, v in self.contents.items(): |
| d[v.lower()] = k |
| self._reverseContents = d |
| return self._reverseContents |
| |
| def writeContents(self): |
| """ |
| Write the contents.plist file out to disk. Call this method when |
| you're done writing glyphs. |
| """ |
| self._writePlist(CONTENTS_FILENAME, self.contents) |
| |
| # layer info |
| |
| def readLayerInfo(self, info, validateRead=None): |
| """ |
| ``validateRead`` will validate the data, by default it is set to the |
| class's ``validateRead`` value, can be overridden. |
| """ |
| if validateRead is None: |
| validateRead = self._validateRead |
| infoDict = self._getPlist(LAYERINFO_FILENAME, {}) |
| if validateRead: |
| if not isinstance(infoDict, dict): |
| raise GlifLibError("layerinfo.plist is not properly formatted.") |
| infoDict = validateLayerInfoVersion3Data(infoDict) |
| # populate the object |
| for attr, value in infoDict.items(): |
| try: |
| setattr(info, attr, value) |
| except AttributeError: |
| raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr) |
| |
| def writeLayerInfo(self, info, validateWrite=None): |
| """ |
| ``validateWrite`` will validate the data, by default it is set to the |
| class's ``validateWrite`` value, can be overridden. |
| """ |
| if validateWrite is None: |
| validateWrite = self._validateWrite |
| if self.ufoFormatVersion < 3: |
| raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion) |
| # gather data |
| infoData = {} |
| for attr in layerInfoVersion3ValueData.keys(): |
| if hasattr(info, attr): |
| try: |
| value = getattr(info, attr) |
| except AttributeError: |
| raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) |
| if value is None or (attr == 'lib' and not value): |
| continue |
| infoData[attr] = value |
| if infoData: |
| # validate |
| if validateWrite: |
| infoData = validateLayerInfoVersion3Data(infoData) |
| # write file |
| self._writePlist(LAYERINFO_FILENAME, infoData) |
| elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME): |
| # data empty, remove existing file |
| self.fs.remove(LAYERINFO_FILENAME) |
| |
| def getGLIF(self, glyphName): |
| """ |
| Get the raw GLIF text for a given glyph name. This only works |
| for GLIF files that are already on disk. |
| |
| This method is useful in situations when the raw XML needs to be |
| read from a glyph set for a particular glyph before fully parsing |
| it into an object structure via the readGlyph method. |
| |
| Raises KeyError if 'glyphName' is not in contents.plist, or |
| GlifLibError if the file associated with can't be found. |
| """ |
| fileName = self.contents[glyphName] |
| try: |
| return self.fs.readbytes(fileName) |
| except fs.errors.ResourceNotFound: |
| raise GlifLibError( |
| "The file '%s' associated with glyph '%s' in contents.plist " |
| "does not exist on %s" % (fileName, glyphName, self.fs) |
| ) |
| |
| def getGLIFModificationTime(self, glyphName): |
| """ |
| Returns the modification time for the GLIF file with 'glyphName', as |
| a floating point number giving the number of seconds since the epoch. |
| Return None if the associated file does not exist or the underlying |
| filesystem does not support getting modified times. |
| Raises KeyError if the glyphName is not in contents.plist. |
| """ |
| fileName = self.contents[glyphName] |
| return self.getFileModificationTime(fileName) |
| |
| # reading/writing API |
| |
| def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=None): |
| """ |
| Read a .glif file for 'glyphName' from the glyph set. The |
| 'glyphObject' argument can be any kind of object (even None); |
| the readGlyph() method will attempt to set the following |
| attributes on it: |
| "width" the advance width of the glyph |
| "height" the advance height of the glyph |
| "unicodes" a list of unicode values for this glyph |
| "note" a string |
| "lib" a dictionary containing custom data |
| "image" a dictionary containing image data |
| "guidelines" a list of guideline data dictionaries |
| "anchors" a list of anchor data dictionaries |
| |
| All attributes are optional, in two ways: |
| 1) An attribute *won't* be set if the .glif file doesn't |
| contain data for it. 'glyphObject' will have to deal |
| with default values itself. |
| 2) If setting the attribute fails with an AttributeError |
| (for example if the 'glyphObject' attribute is read- |
| only), readGlyph() will not propagate that exception, |
| but ignore that attribute. |
| |
| To retrieve outline information, you need to pass an object |
| conforming to the PointPen protocol as the 'pointPen' argument. |
| This argument may be None if you don't need the outline data. |
| |
| readGlyph() will raise KeyError if the glyph is not present in |
| the glyph set. |
| |
| ``validate`` will validate the data, by default it is set to the |
| class's ``validateRead`` value, can be overridden. |
| """ |
| if validate is None: |
| validate = self._validateRead |
| text = self.getGLIF(glyphName) |
| tree = _glifTreeFromString(text) |
| if self.ufoFormatVersion < 3: |
| formatVersions = (1,) |
| else: |
| formatVersions = (1, 2) |
| _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate) |
| |
| def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=None): |
| """ |
| Write a .glif file for 'glyphName' to the glyph set. The |
| 'glyphObject' argument can be any kind of object (even None); |
| the writeGlyph() method will attempt to get the following |
| attributes from it: |
| "width" the advance 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 |
| "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. |
| |
| ``validate`` will validate the data, by default it is set to the |
| class's ``validateWrite`` value, can be overridden. |
| """ |
| if formatVersion is None: |
| if self.ufoFormatVersion >= 3: |
| formatVersion = 2 |
| else: |
| formatVersion = 1 |
| 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) |
| ) |
| if validate is None: |
| validate = self._validateWrite |
| fileName = self.contents.get(glyphName) |
| if fileName is None: |
| if self._existingFileNames is None: |
| self._existingFileNames = {} |
| for fileName in self.contents.values(): |
| self._existingFileNames[fileName] = fileName.lower() |
| fileName = self.glyphNameToFileName(glyphName, self._existingFileNames) |
| self.contents[glyphName] = fileName |
| self._existingFileNames[fileName] = 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: |
| del self._existingFileNames[fileName] |
| if self._reverseContents is not None: |
| del self._reverseContents[self.contents[glyphName].lower()] |
| del self.contents[glyphName] |
| |
| # dict-like support |
| |
| def keys(self): |
| return list(self.contents.keys()) |
| |
| def has_key(self, glyphName): |
| return glyphName in self.contents |
| |
| __contains__ = has_key |
| |
| def __len__(self): |
| return len(self.contents) |
| |
| def __getitem__(self, glyphName): |
| if glyphName not in self.contents: |
| raise KeyError(glyphName) |
| return self.glyphClass(glyphName, self) |
| |
| # quickly fetch unicode values |
| |
| def getUnicodes(self, glyphNames=None): |
| """ |
| Return a dictionary that maps glyph names to lists containing |
| the unicode value[s] for that glyph, if any. This parses the .glif |
| files partially, so it is a lot faster than parsing all files completely. |
| By default this checks all glyphs, but a subset can be passed with glyphNames. |
| """ |
| unicodes = {} |
| if glyphNames is None: |
| glyphNames = self.contents.keys() |
| for glyphName in glyphNames: |
| text = self.getGLIF(glyphName) |
| unicodes[glyphName] = _fetchUnicodes(text) |
| return unicodes |
| |
| def getComponentReferences(self, glyphNames=None): |
| """ |
| Return a dictionary that maps glyph names to lists containing the |
| base glyph name of components in the glyph. This parses the .glif |
| files partially, so it is a lot faster than parsing all files completely. |
| By default this checks all glyphs, but a subset can be passed with glyphNames. |
| """ |
| components = {} |
| if glyphNames is None: |
| glyphNames = self.contents.keys() |
| for glyphName in glyphNames: |
| text = self.getGLIF(glyphName) |
| components[glyphName] = _fetchComponentBases(text) |
| return components |
| |
| def getImageReferences(self, glyphNames=None): |
| """ |
| Return a dictionary that maps glyph names to the file name of the image |
| referenced by the glyph. This parses the .glif files partially, so it is a |
| lot faster than parsing all files completely. |
| By default this checks all glyphs, but a subset can be passed with glyphNames. |
| """ |
| images = {} |
| if glyphNames is None: |
| glyphNames = self.contents.keys() |
| for glyphName in glyphNames: |
| text = self.getGLIF(glyphName) |
| images[glyphName] = _fetchImageFileName(text) |
| return images |
| |
| def close(self): |
| if self._shouldClose: |
| self.fs.close() |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, exc_type, exc_value, exc_tb): |
| self.close() |
| |
| |
| # ----------------------- |
| # Glyph Name to File Name |
| # ----------------------- |
| |
| def glyphNameToFileName(glyphName, existingFileNames): |
| """ |
| Wrapper around the userNameToFileName function in filenames.py |
| """ |
| if existingFileNames is None: |
| existingFileNames = [] |
| if not isinstance(glyphName, unicode): |
| try: |
| new = unicode(glyphName) |
| glyphName = new |
| except UnicodeDecodeError: |
| pass |
| return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif") |
| |
| # ----------------------- |
| # GLIF To and From String |
| # ----------------------- |
| |
| def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2), 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 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 |
| "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 argument defined the GLIF format versions |
| that are allowed to be read. |
| |
| ``validate`` will validate the read data. It is set to ``True`` by default. |
| """ |
| tree = _glifTreeFromString(aString) |
| _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate) |
| |
| |
| 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.""" |
| # start |
| if validate and not isinstance(glyphName, basestring): |
| raise GlifLibError("The glyph name is not properly formatted.") |
| if validate and len(glyphName) == 0: |
| raise GlifLibError("The glyph name is empty.") |
| root = etree.Element("glyph", OrderedDict([("name", glyphName), ("format", repr(formatVersion))])) |
| identifiers = set() |
| # advance |
| _writeAdvance(glyphObject, root, validate) |
| # unicodes |
| if getattr(glyphObject, "unicodes", None): |
| _writeUnicodes(glyphObject, root, validate) |
| # note |
| if getattr(glyphObject, "note", None): |
| _writeNote(glyphObject, root, validate) |
| # image |
| if formatVersion >= 2 and getattr(glyphObject, "image", None): |
| _writeImage(glyphObject, root, validate) |
| # guidelines |
| if formatVersion >= 2 and getattr(glyphObject, "guidelines", None): |
| _writeGuidelines(glyphObject, root, identifiers, validate) |
| # anchors |
| anchors = getattr(glyphObject, "anchors", None) |
| if formatVersion >= 2 and anchors: |
| _writeAnchors(glyphObject, root, identifiers, validate) |
| # outline |
| if drawPointsFunc is not None: |
| outline = etree.SubElement(root, "outline") |
| pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate) |
| drawPointsFunc(pen) |
| if formatVersion == 1 and anchors: |
| _writeAnchorsFormat1(pen, anchors, validate) |
| # prevent lxml from writing self-closing tags |
| if not len(outline): |
| outline.text = "\n " |
| # lib |
| if getattr(glyphObject, "lib", None): |
| _writeLib(glyphObject, root, validate) |
| # return the text |
| data = etree.tostring( |
| root, encoding="UTF-8", xml_declaration=True, pretty_print=True |
| ) |
| return data |
| |
| |
| def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=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") |
| |
| |
| 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, integerTypes): |
| unicodes = [unicodes] |
| seen = set() |
| for code in unicodes: |
| if validate and not isinstance(code, integerTypes): |
| 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, basestring): |
| raise GlifLibError("note attribute must be str or unicode") |
| note = note.strip() |
| note = "\n" + note + "\n" |
| # ensure text is unicode, if it's bytes decode as ASCII |
| etree.SubElement(element, "note").text = tounicode(note) |
| |
| def _writeImage(glyphObject, element, validate): |
| image = getattr(glyphObject, "image", None) |
| if validate and not imageValidator(image): |
| raise GlifLibError("image attribute must be a dict or dict-like object with the proper structure.") |
| attrs = OrderedDict([("fileName", image["fileName"])]) |
| for attr, default in _transformationInfo: |
| value = image.get(attr, default) |
| if value != default: |
| attrs[attr] = repr(value) |
| color = image.get("color") |
| if color is not None: |
| attrs["color"] = color |
| etree.SubElement(element, "image", attrs) |
| |
| def _writeGuidelines(glyphObject, element, identifiers, validate): |
| guidelines = getattr(glyphObject, "guidelines", []) |
| if validate and not guidelinesValidator(guidelines): |
| raise GlifLibError("guidelines attribute does not have the proper structure.") |
| for guideline in guidelines: |
| attrs = OrderedDict() |
| x = guideline.get("x") |
| if x is not None: |
| attrs["x"] = repr(x) |
| y = guideline.get("y") |
| if y is not None: |
| attrs["y"] = repr(y) |
| angle = guideline.get("angle") |
| if angle is not None: |
| attrs["angle"] = repr(angle) |
| name = guideline.get("name") |
| if name is not None: |
| attrs["name"] = name |
| color = guideline.get("color") |
| if color is not None: |
| attrs["color"] = color |
| identifier = guideline.get("identifier") |
| if identifier is not None: |
| if validate and identifier in identifiers: |
| raise GlifLibError("identifier used more than once: %s" % identifier) |
| attrs["identifier"] = identifier |
| identifiers.add(identifier) |
| etree.SubElement(element, "guideline", attrs) |
| |
| def _writeAnchorsFormat1(pen, anchors, validate): |
| if validate and not anchorsValidator(anchors): |
| raise GlifLibError("anchors attribute does not have the proper structure.") |
| for anchor in anchors: |
| attrs = {} |
| x = anchor["x"] |
| attrs["x"] = repr(x) |
| y = anchor["y"] |
| attrs["y"] = repr(y) |
| name = anchor.get("name") |
| if name is not None: |
| attrs["name"] = name |
| pen.beginPath() |
| pen.addPoint((x, y), segmentType="move", name=name) |
| pen.endPath() |
| |
| def _writeAnchors(glyphObject, element, identifiers, validate): |
| anchors = getattr(glyphObject, "anchors", []) |
| if validate and not anchorsValidator(anchors): |
| raise GlifLibError("anchors attribute does not have the proper structure.") |
| for anchor in anchors: |
| attrs = OrderedDict() |
| x = anchor["x"] |
| attrs["x"] = repr(x) |
| y = anchor["y"] |
| attrs["y"] = repr(y) |
| name = anchor.get("name") |
| if name is not None: |
| attrs["name"] = name |
| color = anchor.get("color") |
| if color is not None: |
| attrs["color"] = color |
| identifier = anchor.get("identifier") |
| if identifier is not None: |
| if validate and identifier in identifiers: |
| raise GlifLibError("identifier used more than once: %s" % identifier) |
| attrs["identifier"] = identifier |
| identifiers.add(identifier) |
| etree.SubElement(element, "anchor", attrs) |
| |
| def _writeLib(glyphObject, element, validate): |
| lib = getattr(glyphObject, "lib", None) |
| if not lib: |
| # don't write empty lib |
| return |
| if validate: |
| valid, message = glyphLibValidator(lib) |
| if not valid: |
| raise GlifLibError(message) |
| if not isinstance(lib, dict): |
| lib = dict(lib) |
| # plist inside GLIF begins with 2 levels of indentation |
| e = plistlib.totree(lib, indent_level=2) |
| etree.SubElement(element, "lib").append(e) |
| |
| # ----------------------- |
| # layerinfo.plist Support |
| # ----------------------- |
| |
| layerInfoVersion3ValueData = { |
| "color" : dict(type=basestring, valueValidator=colorValidator), |
| "lib" : dict(type=dict, valueValidator=genericTypeValidator) |
| } |
| |
| def validateLayerInfoVersion3ValueForAttribute(attr, value): |
| """ |
| This performs very basic validation of the value for attribute |
| following the UFO 3 fontinfo.plist specification. The results |
| of this should not be interpretted as *correct* for the font |
| that they are part of. This merely indicates that the value |
| is of the proper type and, where the specification defines |
| a set range of possible values for an attribute, that the |
| value is in the accepted range. |
| """ |
| if attr not in layerInfoVersion3ValueData: |
| return False |
| dataValidationDict = layerInfoVersion3ValueData[attr] |
| valueType = dataValidationDict.get("type") |
| validator = dataValidationDict.get("valueValidator") |
| valueOptions = dataValidationDict.get("valueOptions") |
| # have specific options for the validator |
| if valueOptions is not None: |
| isValidValue = validator(value, valueOptions) |
| # no specific options |
| else: |
| if validator == genericTypeValidator: |
| isValidValue = validator(value, valueType) |
| else: |
| isValidValue = validator(value) |
| return isValidValue |
| |
| def validateLayerInfoVersion3Data(infoData): |
| """ |
| This performs very basic validation of the value for infoData |
| following the UFO 3 layerinfo.plist specification. The results |
| of this should not be interpretted as *correct* for the font |
| that they are part of. This merely indicates that the values |
| are of the proper type and, where the specification defines |
| a set range of possible values for an attribute, that the |
| value is in the accepted range. |
| """ |
| for attr, value in infoData.items(): |
| if attr not in layerInfoVersion3ValueData: |
| raise GlifLibError("Unknown attribute %s." % attr) |
| isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value) |
| if not isValidValue: |
| raise GlifLibError("Invalid value for attribute %s (%s)." % (attr, repr(value))) |
| return infoData |
| |
| # ----------------- |
| # GLIF Tree Support |
| # ----------------- |
| |
| def _glifTreeFromFile(aFile): |
| root = etree.parse(aFile).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") |
| root = etree.fromstring(data) |
| if root.tag != "glyph": |
| raise GlifLibError("The GLIF is not properly formatted.") |
| if root.text and root.text.strip() != '': |
| raise GlifLibError("Invalid GLIF structure.") |
| return root |
| |
| def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True): |
| # check the format version |
| formatVersion = tree.get("format") |
| if validate and formatVersion is None: |
| raise GlifLibError("Unspecified format version in GLIF.") |
| try: |
| v = int(formatVersion) |
| formatVersion = v |
| except ValueError: |
| pass |
| if validate and formatVersion not in formatVersions: |
| raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion) |
| if formatVersion == 1: |
| _readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate) |
| elif formatVersion == 2: |
| _readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate) |
| else: |
| raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) |
| |
| |
| def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None): |
| # get the name |
| _readName(glyphObject, tree, validate) |
| # populate the sub elements |
| unicodes = [] |
| haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False |
| for element in tree: |
| if element.tag == "outline": |
| if validate: |
| if haveSeenOutline: |
| raise GlifLibError("The outline element occurs more than once.") |
| if element.attrib: |
| raise GlifLibError("The outline element contains unknown attributes.") |
| if element.text and element.text.strip() != '': |
| raise GlifLibError("Invalid outline structure.") |
| haveSeenOutline = True |
| buildOutlineFormat1(glyphObject, pointPen, element, validate) |
| elif glyphObject is None: |
| continue |
| elif element.tag == "advance": |
| if validate and haveSeenAdvance: |
| raise GlifLibError("The advance element occurs more than once.") |
| haveSeenAdvance = True |
| _readAdvance(glyphObject, element) |
| elif element.tag == "unicode": |
| try: |
| v = element.get("hex") |
| v = int(v, 16) |
| if v not in unicodes: |
| unicodes.append(v) |
| except ValueError: |
| raise GlifLibError("Illegal value for hex attribute of unicode element.") |
| elif element.tag == "note": |
| if validate and haveSeenNote: |
| raise GlifLibError("The note element occurs more than once.") |
| haveSeenNote = True |
| _readNote(glyphObject, element) |
| elif element.tag == "lib": |
| if validate and haveSeenLib: |
| raise GlifLibError("The lib element occurs more than once.") |
| haveSeenLib = True |
| _readLib(glyphObject, element, validate) |
| else: |
| raise GlifLibError("Unknown element in GLIF: %s" % element) |
| # set the collected unicodes |
| if unicodes: |
| _relaxedSetattr(glyphObject, "unicodes", unicodes) |
| |
| def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=None): |
| # get the name |
| _readName(glyphObject, tree, validate) |
| # populate the sub elements |
| unicodes = [] |
| guidelines = [] |
| anchors = [] |
| haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = False |
| identifiers = set() |
| for element in tree: |
| if element.tag == "outline": |
| if validate: |
| if haveSeenOutline: |
| raise GlifLibError("The outline element occurs more than once.") |
| if element.attrib: |
| raise GlifLibError("The outline element contains unknown attributes.") |
| if element.text and element.text.strip() != '': |
| raise GlifLibError("Invalid outline structure.") |
| haveSeenOutline = True |
| if pointPen is not None: |
| buildOutlineFormat2(glyphObject, pointPen, element, identifiers, validate) |
| elif glyphObject is None: |
| continue |
| elif element.tag == "advance": |
| if validate and haveSeenAdvance: |
| raise GlifLibError("The advance element occurs more than once.") |
| haveSeenAdvance = True |
| _readAdvance(glyphObject, element) |
| elif element.tag == "unicode": |
| try: |
| v = element.get("hex") |
| v = int(v, 16) |
| if v not in unicodes: |
| unicodes.append(v) |
| except ValueError: |
| raise GlifLibError("Illegal value for hex attribute of unicode element.") |
| elif element.tag == "guideline": |
| if validate and len(element): |
| raise GlifLibError("Unknown children in guideline element.") |
| attrib = dict(element.attrib) |
| for attr in ("x", "y", "angle"): |
| if attr in attrib: |
| attrib[attr] = _number(attrib[attr]) |
| guidelines.append(attrib) |
| elif element.tag == "anchor": |
| if validate and len(element): |
| raise GlifLibError("Unknown children in anchor element.") |
| attrib = dict(element.attrib) |
| for attr in ("x", "y"): |
| if attr in element.attrib: |
| attrib[attr] = _number(attrib[attr]) |
| anchors.append(attrib) |
| elif element.tag == "image": |
| if validate: |
| if haveSeenImage: |
| raise GlifLibError("The image element occurs more than once.") |
| if len(element): |
| raise GlifLibError("Unknown children in image element.") |
| haveSeenImage = True |
| _readImage(glyphObject, element, validate) |
| elif element.tag == "note": |
| if validate and haveSeenNote: |
| raise GlifLibError("The note element occurs more than once.") |
| haveSeenNote = True |
| _readNote(glyphObject, element) |
| elif element.tag == "lib": |
| if validate and haveSeenLib: |
| raise GlifLibError("The lib element occurs more than once.") |
| haveSeenLib = True |
| _readLib(glyphObject, element, validate) |
| else: |
| raise GlifLibError("Unknown element in GLIF: %s" % element) |
| # set the collected unicodes |
| if unicodes: |
| _relaxedSetattr(glyphObject, "unicodes", unicodes) |
| # set the collected guidelines |
| if guidelines: |
| if validate and not guidelinesValidator(guidelines, identifiers): |
| raise GlifLibError("The guidelines are improperly formatted.") |
| _relaxedSetattr(glyphObject, "guidelines", guidelines) |
| # set the collected anchors |
| if anchors: |
| if validate and not anchorsValidator(anchors, identifiers): |
| raise GlifLibError("The anchors are improperly formatted.") |
| _relaxedSetattr(glyphObject, "anchors", anchors) |
| |
| def _readName(glyphObject, root, validate): |
| glyphName = root.get("name") |
| if validate and not glyphName: |
| raise GlifLibError("Empty glyph name in GLIF.") |
| if glyphName and glyphObject is not None: |
| _relaxedSetattr(glyphObject, "name", glyphName) |
| |
| def _readAdvance(glyphObject, advance): |
| width = _number(advance.get("width", 0)) |
| _relaxedSetattr(glyphObject, "width", width) |
| height = _number(advance.get("height", 0)) |
| _relaxedSetattr(glyphObject, "height", height) |
| |
| def _readNote(glyphObject, note): |
| lines = note.text.split("\n") |
| note = "\n".join(line.strip() for line in lines if line.strip()) |
| _relaxedSetattr(glyphObject, "note", note) |
| |
| def _readLib(glyphObject, lib, validate): |
| assert len(lib) == 1 |
| child = lib[0] |
| plist = plistlib.fromtree(child) |
| if validate: |
| valid, message = glyphLibValidator(plist) |
| if not valid: |
| raise GlifLibError(message) |
| _relaxedSetattr(glyphObject, "lib", plist) |
| |
| def _readImage(glyphObject, image, validate): |
| imageData = dict(image.attrib) |
| for attr, default in _transformationInfo: |
| value = imageData.get(attr, default) |
| imageData[attr] = _number(value) |
| if validate and not imageValidator(imageData): |
| raise GlifLibError("The image element is not properly formatted.") |
| _relaxedSetattr(glyphObject, "image", imageData) |
| |
| # ---------------- |
| # GLIF to PointPen |
| # ---------------- |
| |
| contourAttributesFormat2 = 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 |
| |
| def buildOutlineFormat1(glyphObject, pen, outline, validate): |
| anchors = [] |
| for element in outline: |
| if element.tag == "contour": |
| if len(element) == 1: |
| point = element[0] |
| if point.tag == "point": |
| anchor = _buildAnchorFormat1(point, validate) |
| if anchor is not None: |
| anchors.append(anchor) |
| continue |
| if pen is not None: |
| _buildOutlineContourFormat1(pen, element, validate) |
| elif element.tag == "component": |
| if pen is not None: |
| _buildOutlineComponentFormat1(pen, element, validate) |
| else: |
| raise GlifLibError("Unknown element in outline element: %s" % element) |
| if glyphObject is not None and anchors: |
| if validate and not anchorsValidator(anchors): |
| raise GlifLibError("GLIF 1 anchors are not properly formatted.") |
| _relaxedSetattr(glyphObject, "anchors", anchors) |
| |
| def _buildAnchorFormat1(point, validate): |
| if point.get("type") != "move": |
| return None |
| name = point.get("name") |
| if name is None: |
| return None |
| x = point.get("x") |
| y = point.get("y") |
| if validate and x is None: |
| raise GlifLibError("Required x attribute is missing in point element.") |
| if validate and y is None: |
| raise GlifLibError("Required y attribute is missing in point element.") |
| x = _number(x) |
| y = _number(y) |
| anchor = dict(x=x, y=y, name=name) |
| return anchor |
| |
| def _buildOutlineContourFormat1(pen, contour, validate): |
| if validate and contour.attrib: |
| raise GlifLibError("Unknown attributes in contour element.") |
| pen.beginPath() |
| if len(contour): |
| massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat1, openContourOffCurveLeniency=True, validate=validate) |
| _buildOutlinePointsFormat1(pen, massaged) |
| pen.endPath() |
| |
| def _buildOutlinePointsFormat1(pen, contour): |
| for point in contour: |
| x = point["x"] |
| y = point["y"] |
| segmentType = point["segmentType"] |
| smooth = point["smooth"] |
| name = point["name"] |
| pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name) |
| |
| def _buildOutlineComponentFormat1(pen, component, validate): |
| if validate: |
| if len(component): |
| raise GlifLibError("Unknown child elements of component element.") |
| for attr in component.attrib.keys(): |
| if attr not in componentAttributesFormat1: |
| raise GlifLibError("Unknown attribute in component element: %s" % attr) |
| baseGlyphName = component.get("base") |
| if validate and baseGlyphName is None: |
| raise GlifLibError("The base attribute is not defined in the component.") |
| transformation = [] |
| for attr, default in _transformationInfo: |
| value = component.get(attr) |
| if value is None: |
| value = default |
| else: |
| value = _number(value) |
| transformation.append(value) |
| pen.addComponent(baseGlyphName, tuple(transformation)) |
| |
| # format 2 |
| |
| def buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate): |
| for element in outline: |
| if element.tag == "contour": |
| _buildOutlineContourFormat2(pen, element, identifiers, validate) |
| elif element.tag == "component": |
| _buildOutlineComponentFormat2(pen, element, identifiers, validate) |
| else: |
| raise GlifLibError("Unknown element in outline element: %s" % element.tag) |
| |
| def _buildOutlineContourFormat2(pen, contour, identifiers, validate): |
| if validate: |
| for attr in contour.attrib.keys(): |
| if attr not in contourAttributesFormat2: |
| raise GlifLibError("Unknown attribute in contour element: %s" % attr) |
| identifier = contour.get("identifier") |
| if identifier is not None: |
| if validate: |
| if identifier in identifiers: |
| raise GlifLibError("The identifier %s is used more than once." % identifier) |
| if not identifierValidator(identifier): |
| raise GlifLibError("The contour identifier %s is not valid." % identifier) |
| identifiers.add(identifier) |
| try: |
| pen.beginPath(identifier=identifier) |
| except TypeError: |
| pen.beginPath() |
| warn("The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.", DeprecationWarning) |
| if len(contour): |
| massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat2, validate=validate) |
| _buildOutlinePointsFormat2(pen, massaged, identifiers, validate) |
| pen.endPath() |
| |
| def _buildOutlinePointsFormat2(pen, contour, identifiers, validate): |
| for point in contour: |
| x = point["x"] |
| y = point["y"] |
| segmentType = point["segmentType"] |
| smooth = point["smooth"] |
| name = point["name"] |
| identifier = point.get("identifier") |
| if identifier is not None: |
| if validate: |
| if identifier in identifiers: |
| raise GlifLibError("The identifier %s is used more than once." % identifier) |
| if not identifierValidator(identifier): |
| raise GlifLibError("The identifier %s is not valid." % identifier) |
| identifiers.add(identifier) |
| try: |
| pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name, identifier=identifier) |
| except TypeError: |
| pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name) |
| warn("The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.", DeprecationWarning) |
| |
| def _buildOutlineComponentFormat2(pen, component, identifiers, validate): |
| if validate: |
| if len(component): |
| raise GlifLibError("Unknown child elements of component element.") |
| for attr in component.attrib.keys(): |
| if attr not in componentAttributesFormat2: |
| raise GlifLibError("Unknown attribute in component element: %s" % attr) |
| baseGlyphName = component.get("base") |
| if validate and baseGlyphName is None: |
| raise GlifLibError("The base attribute is not defined in the component.") |
| transformation = [] |
| for attr, default in _transformationInfo: |
| value = component.get(attr) |
| if value is None: |
| value = default |
| else: |
| value = _number(value) |
| transformation.append(value) |
| identifier = component.get("identifier") |
| if identifier is not None: |
| if validate: |
| if identifier in identifiers: |
| raise GlifLibError("The identifier %s is used more than once." % identifier) |
| if validate and not identifierValidator(identifier): |
| raise GlifLibError("The identifier %s is not valid." % identifier) |
| identifiers.add(identifier) |
| try: |
| pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier) |
| except TypeError: |
| pen.addComponent(baseGlyphName, tuple(transformation)) |
| warn("The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.", DeprecationWarning) |
| |
| # all formats |
| |
| def _validateAndMassagePointStructures(contour, pointAttributes, openContourOffCurveLeniency=False, validate=True): |
| if not len(contour): |
| return |
| # store some data for later validation |
| lastOnCurvePoint = None |
| haveOffCurvePoint = False |
| # validate and massage the individual point elements |
| massaged = [] |
| for index, element in enumerate(contour): |
| # not <point> |
| if element.tag != "point": |
| raise GlifLibError("Unknown child element (%s) of contour element." % element.tag) |
| point = dict(element.attrib) |
| massaged.append(point) |
| if validate: |
| # unknown attributes |
| for attr in point.keys(): |
| if attr not in pointAttributes: |
| raise GlifLibError("Unknown attribute in point element: %s" % attr) |
| # search for unknown children |
| if len(element): |
| raise GlifLibError("Unknown child elements in point element.") |
| # x and y are required |
| for attr in ("x", "y"): |
| value = element.get(attr) |
| if validate and value is None: |
| raise GlifLibError("Required %s attribute is missing in point element." % attr) |
| point[attr] = _number(value) |
| # segment type |
| pointType = point.pop("type", "offcurve") |
| if validate and pointType not in pointTypeOptions: |
| raise GlifLibError("Unknown point type: %s" % pointType) |
| if pointType == "offcurve": |
| pointType = None |
| point["segmentType"] = pointType |
| if pointType is None: |
| haveOffCurvePoint = True |
| else: |
| lastOnCurvePoint = index |
| # move can only occur as the first point |
| if validate and pointType == "move" and index != 0: |
| raise GlifLibError("A move point occurs after the first point in the contour.") |
| # smooth is optional |
| smooth = point.get("smooth", "no") |
| if validate and smooth is not None: |
| if smooth not in pointSmoothOptions: |
| raise GlifLibError("Unknown point smooth value: %s" % smooth) |
| smooth = smooth == "yes" |
| point["smooth"] = smooth |
| # smooth can only be applied to curve and qcurve |
| if validate and smooth and pointType is None: |
| raise GlifLibError("smooth attribute set in an offcurve point.") |
| # name is optional |
| if "name" not in element.attrib: |
| point["name"] = None |
| if openContourOffCurveLeniency: |
| # remove offcurves that precede a move. this is technically illegal, |
| # but we let it slide because there are fonts out there in the wild like this. |
| if massaged[0]["segmentType"] == "move": |
| count = 0 |
| for point in reversed(massaged): |
| if point["segmentType"] is None: |
| count += 1 |
| else: |
| break |
| if count: |
| massaged = massaged[:-count] |
| # validate the off-curves in the segments |
| if validate and haveOffCurvePoint and lastOnCurvePoint is not None: |
| # we only care about how many offCurves there are before an onCurve |
| # filter out the trailing offCurves |
| offCurvesCount = len(massaged) - 1 - lastOnCurvePoint |
| for point in massaged: |
| segmentType = point["segmentType"] |
| if segmentType is None: |
| offCurvesCount += 1 |
| else: |
| if offCurvesCount: |
| # move and line can't be preceded by off-curves |
| if segmentType == "move": |
| # this will have been filtered out already |
| raise GlifLibError("move can not have an offcurve.") |
| elif segmentType == "line": |
| raise GlifLibError("line can not have an offcurve.") |
| elif segmentType == "curve": |
| if offCurvesCount > 2: |
| raise GlifLibError("Too many offcurves defined for curve.") |
| elif segmentType == "qcurve": |
| pass |
| else: |
| # unknown segment type. it'll be caught later. |
| pass |
| offCurvesCount = 0 |
| return massaged |
| |
| # --------------------- |
| # Misc Helper Functions |
| # --------------------- |
| |
| def _relaxedSetattr(object, attr, value): |
| try: |
| setattr(object, attr, value) |
| except AttributeError: |
| pass |
| |
| def _number(s): |
| """ |
| Given a numeric string, return an integer or a float, whichever |
| the string indicates. _number("1") will return the integer 1, |
| _number("1.0") will return the float 1.0. |
| |
| >>> _number("1") |
| 1 |
| >>> _number("1.0") |
| 1.0 |
| >>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL |
| Traceback (most recent call last): |
| ... |
| GlifLibError: Could not convert a to an int or float. |
| """ |
| try: |
| n = int(s) |
| return n |
| except ValueError: |
| pass |
| try: |
| n = float(s) |
| return n |
| except ValueError: |
| raise GlifLibError("Could not convert %s to an int or float." % s) |
| |
| # -------------------- |
| # Rapid Value Fetching |
| # -------------------- |
| |
| # base |
| |
| class _DoneParsing(Exception): pass |
| |
| class _BaseParser(object): |
| |
| def __init__(self): |
| self._elementStack = [] |
| |
| def parse(self, text): |
| from xml.parsers.expat import ParserCreate |
| parser = ParserCreate() |
| parser.StartElementHandler = self.startElementHandler |
| parser.EndElementHandler = self.endElementHandler |
| parser.Parse(text) |
| |
| def startElementHandler(self, name, attrs): |
| self._elementStack.append(name) |
| |
| def endElementHandler(self, name): |
| other = self._elementStack.pop(-1) |
| assert other == name |
| |
| |
| # unicodes |
| |
| def _fetchUnicodes(glif): |
| """ |
| Get a list of unicodes listed in glif. |
| """ |
| parser = _FetchUnicodesParser() |
| parser.parse(glif) |
| return parser.unicodes |
| |
| class _FetchUnicodesParser(_BaseParser): |
| |
| def __init__(self): |
| self.unicodes = [] |
| super(_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) |
| |
| # -------------- |
| # GLIF Point Pen |
| # -------------- |
| |
| _transformationInfo = [ |
| # field name, default value |
| ("xScale", 1), |
| ("xyScale", 0), |
| ("yxScale", 0), |
| ("yScale", 1), |
| ("xOffset", 0), |
| ("yOffset", 0), |
| ] |
| |
| class GLIFPointPen(AbstractPointPen): |
| |
| """ |
| Helper class using the PointPen protocol to write the <outline> |
| part of .glif files. |
| """ |
| |
| def __init__(self, element, formatVersion=2, identifiers=None, validate=True): |
| if identifiers is None: |
| identifiers = set() |
| self.formatVersion = 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 >= 2: |
| if self.validate: |
| if identifier in self.identifiers: |
| raise GlifLibError("identifier used more than once: %s" % identifier) |
| if not identifierValidator(identifier): |
| raise GlifLibError("identifier not formatted properly: %s" % identifier) |
| attrs["identifier"] = identifier |
| self.identifiers.add(identifier) |
| self.contour = etree.SubElement(self.outline, "contour", attrs) |
| self.prevOffCurveCount = 0 |
| |
| def endPath(self): |
| if self.prevPointTypes and self.prevPointTypes[0] == "move": |
| if self.validate and self.prevPointTypes[-1] == "offcurve": |
| raise GlifLibError("open contour has loose offcurve point") |
| # prevent lxml from writing self-closing tags |
| if not len(self.contour): |
| self.contour.text = "\n " |
| self.contour = None |
| self.prevPointType = None |
| self.prevOffCurveCount = 0 |
| self.prevPointTypes = [] |
| |
| def addPoint(self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs): |
| attrs = OrderedDict() |
| # coordinates |
| if pt is not None: |
| if self.validate: |
| for coord in pt: |
| if not isinstance(coord, numberTypes): |
| raise GlifLibError("coordinates must be int or float") |
| attrs["x"] = repr(pt[0]) |
| attrs["y"] = repr(pt[1]) |
| # segment type |
| if segmentType == "offcurve": |
| segmentType = None |
| if self.validate: |
| if segmentType == "move" and self.prevPointTypes: |
| raise GlifLibError("move occurs after a point has already been added to the contour.") |
| if segmentType in ("move", "line") and self.prevPointTypes and self.prevPointTypes[-1] == "offcurve": |
| raise GlifLibError("offcurve occurs before %s point." % segmentType) |
| if segmentType == "curve" and self.prevOffCurveCount > 2: |
| raise GlifLibError("too many offcurve points before curve point.") |
| if segmentType is not None: |
| attrs["type"] = segmentType |
| else: |
| segmentType = "offcurve" |
| if segmentType == "offcurve": |
| self.prevOffCurveCount += 1 |
| else: |
| self.prevOffCurveCount = 0 |
| self.prevPointTypes.append(segmentType) |
| # smooth |
| if smooth: |
| if self.validate and segmentType == "offcurve": |
| raise GlifLibError("can't set smooth in an offcurve point.") |
| attrs["smooth"] = "yes" |
| # name |
| if name is not None: |
| attrs["name"] = name |
| # identifier |
| if identifier is not None and self.formatVersion >= 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 >= 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() |