1*e1fe3e4aSElliott Hughes""" 2*e1fe3e4aSElliott HughesglifLib.py -- Generic module for reading and writing the .glif format. 3*e1fe3e4aSElliott Hughes 4*e1fe3e4aSElliott HughesMore info about the .glif format (GLyphInterchangeFormat) can be found here: 5*e1fe3e4aSElliott Hughes 6*e1fe3e4aSElliott Hughes http://unifiedfontobject.org 7*e1fe3e4aSElliott Hughes 8*e1fe3e4aSElliott HughesThe main class in this module is GlyphSet. It manages a set of .glif files 9*e1fe3e4aSElliott Hughesin a folder. It offers two ways to read glyph data, and one way to write 10*e1fe3e4aSElliott Hughesglyph data. See the class doc string for details. 11*e1fe3e4aSElliott Hughes""" 12*e1fe3e4aSElliott Hughes 13*e1fe3e4aSElliott Hughesfrom __future__ import annotations 14*e1fe3e4aSElliott Hughes 15*e1fe3e4aSElliott Hughesimport logging 16*e1fe3e4aSElliott Hughesimport enum 17*e1fe3e4aSElliott Hughesfrom warnings import warn 18*e1fe3e4aSElliott Hughesfrom collections import OrderedDict 19*e1fe3e4aSElliott Hughesimport fs 20*e1fe3e4aSElliott Hughesimport fs.base 21*e1fe3e4aSElliott Hughesimport fs.errors 22*e1fe3e4aSElliott Hughesimport fs.osfs 23*e1fe3e4aSElliott Hughesimport fs.path 24*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import tobytes 25*e1fe3e4aSElliott Hughesfrom fontTools.misc import plistlib 26*e1fe3e4aSElliott Hughesfrom fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen 27*e1fe3e4aSElliott Hughesfrom fontTools.ufoLib.errors import GlifLibError 28*e1fe3e4aSElliott Hughesfrom fontTools.ufoLib.filenames import userNameToFileName 29*e1fe3e4aSElliott Hughesfrom fontTools.ufoLib.validators import ( 30*e1fe3e4aSElliott Hughes genericTypeValidator, 31*e1fe3e4aSElliott Hughes colorValidator, 32*e1fe3e4aSElliott Hughes guidelinesValidator, 33*e1fe3e4aSElliott Hughes anchorsValidator, 34*e1fe3e4aSElliott Hughes identifierValidator, 35*e1fe3e4aSElliott Hughes imageValidator, 36*e1fe3e4aSElliott Hughes glyphLibValidator, 37*e1fe3e4aSElliott Hughes) 38*e1fe3e4aSElliott Hughesfrom fontTools.misc import etree 39*e1fe3e4aSElliott Hughesfrom fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion 40*e1fe3e4aSElliott Hughesfrom fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin 41*e1fe3e4aSElliott Hughes 42*e1fe3e4aSElliott Hughes 43*e1fe3e4aSElliott Hughes__all__ = [ 44*e1fe3e4aSElliott Hughes "GlyphSet", 45*e1fe3e4aSElliott Hughes "GlifLibError", 46*e1fe3e4aSElliott Hughes "readGlyphFromString", 47*e1fe3e4aSElliott Hughes "writeGlyphToString", 48*e1fe3e4aSElliott Hughes "glyphNameToFileName", 49*e1fe3e4aSElliott Hughes] 50*e1fe3e4aSElliott Hughes 51*e1fe3e4aSElliott Hugheslogger = logging.getLogger(__name__) 52*e1fe3e4aSElliott Hughes 53*e1fe3e4aSElliott Hughes 54*e1fe3e4aSElliott Hughes# --------- 55*e1fe3e4aSElliott Hughes# Constants 56*e1fe3e4aSElliott Hughes# --------- 57*e1fe3e4aSElliott Hughes 58*e1fe3e4aSElliott HughesCONTENTS_FILENAME = "contents.plist" 59*e1fe3e4aSElliott HughesLAYERINFO_FILENAME = "layerinfo.plist" 60*e1fe3e4aSElliott Hughes 61*e1fe3e4aSElliott Hughes 62*e1fe3e4aSElliott Hughesclass GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum): 63*e1fe3e4aSElliott Hughes FORMAT_1_0 = (1, 0) 64*e1fe3e4aSElliott Hughes FORMAT_2_0 = (2, 0) 65*e1fe3e4aSElliott Hughes 66*e1fe3e4aSElliott Hughes @classmethod 67*e1fe3e4aSElliott Hughes def default(cls, ufoFormatVersion=None): 68*e1fe3e4aSElliott Hughes if ufoFormatVersion is not None: 69*e1fe3e4aSElliott Hughes return max(cls.supported_versions(ufoFormatVersion)) 70*e1fe3e4aSElliott Hughes return super().default() 71*e1fe3e4aSElliott Hughes 72*e1fe3e4aSElliott Hughes @classmethod 73*e1fe3e4aSElliott Hughes def supported_versions(cls, ufoFormatVersion=None): 74*e1fe3e4aSElliott Hughes if ufoFormatVersion is None: 75*e1fe3e4aSElliott Hughes # if ufo format unspecified, return all the supported GLIF formats 76*e1fe3e4aSElliott Hughes return super().supported_versions() 77*e1fe3e4aSElliott Hughes # else only return the GLIF formats supported by the given UFO format 78*e1fe3e4aSElliott Hughes versions = {cls.FORMAT_1_0} 79*e1fe3e4aSElliott Hughes if ufoFormatVersion >= UFOFormatVersion.FORMAT_3_0: 80*e1fe3e4aSElliott Hughes versions.add(cls.FORMAT_2_0) 81*e1fe3e4aSElliott Hughes return frozenset(versions) 82*e1fe3e4aSElliott Hughes 83*e1fe3e4aSElliott Hughes 84*e1fe3e4aSElliott Hughes# workaround for py3.11, see https://github.com/fonttools/fonttools/pull/2655 85*e1fe3e4aSElliott HughesGLIFFormatVersion.__str__ = _VersionTupleEnumMixin.__str__ 86*e1fe3e4aSElliott Hughes 87*e1fe3e4aSElliott Hughes 88*e1fe3e4aSElliott Hughes# ------------ 89*e1fe3e4aSElliott Hughes# Simple Glyph 90*e1fe3e4aSElliott Hughes# ------------ 91*e1fe3e4aSElliott Hughes 92*e1fe3e4aSElliott Hughes 93*e1fe3e4aSElliott Hughesclass Glyph: 94*e1fe3e4aSElliott Hughes """ 95*e1fe3e4aSElliott Hughes Minimal glyph object. It has no glyph attributes until either 96*e1fe3e4aSElliott Hughes the draw() or the drawPoints() method has been called. 97*e1fe3e4aSElliott Hughes """ 98*e1fe3e4aSElliott Hughes 99*e1fe3e4aSElliott Hughes def __init__(self, glyphName, glyphSet): 100*e1fe3e4aSElliott Hughes self.glyphName = glyphName 101*e1fe3e4aSElliott Hughes self.glyphSet = glyphSet 102*e1fe3e4aSElliott Hughes 103*e1fe3e4aSElliott Hughes def draw(self, pen, outputImpliedClosingLine=False): 104*e1fe3e4aSElliott Hughes """ 105*e1fe3e4aSElliott Hughes Draw this glyph onto a *FontTools* Pen. 106*e1fe3e4aSElliott Hughes """ 107*e1fe3e4aSElliott Hughes pointPen = PointToSegmentPen( 108*e1fe3e4aSElliott Hughes pen, outputImpliedClosingLine=outputImpliedClosingLine 109*e1fe3e4aSElliott Hughes ) 110*e1fe3e4aSElliott Hughes self.drawPoints(pointPen) 111*e1fe3e4aSElliott Hughes 112*e1fe3e4aSElliott Hughes def drawPoints(self, pointPen): 113*e1fe3e4aSElliott Hughes """ 114*e1fe3e4aSElliott Hughes Draw this glyph onto a PointPen. 115*e1fe3e4aSElliott Hughes """ 116*e1fe3e4aSElliott Hughes self.glyphSet.readGlyph(self.glyphName, self, pointPen) 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughes 119*e1fe3e4aSElliott Hughes# --------- 120*e1fe3e4aSElliott Hughes# Glyph Set 121*e1fe3e4aSElliott Hughes# --------- 122*e1fe3e4aSElliott Hughes 123*e1fe3e4aSElliott Hughes 124*e1fe3e4aSElliott Hughesclass GlyphSet(_UFOBaseIO): 125*e1fe3e4aSElliott Hughes """ 126*e1fe3e4aSElliott Hughes GlyphSet manages a set of .glif files inside one directory. 127*e1fe3e4aSElliott Hughes 128*e1fe3e4aSElliott Hughes GlyphSet's constructor takes a path to an existing directory as it's 129*e1fe3e4aSElliott Hughes first argument. Reading glyph data can either be done through the 130*e1fe3e4aSElliott Hughes readGlyph() method, or by using GlyphSet's dictionary interface, where 131*e1fe3e4aSElliott Hughes the keys are glyph names and the values are (very) simple glyph objects. 132*e1fe3e4aSElliott Hughes 133*e1fe3e4aSElliott Hughes To write a glyph to the glyph set, you use the writeGlyph() method. 134*e1fe3e4aSElliott Hughes The simple glyph objects returned through the dict interface do not 135*e1fe3e4aSElliott Hughes support writing, they are just a convenient way to get at the glyph data. 136*e1fe3e4aSElliott Hughes """ 137*e1fe3e4aSElliott Hughes 138*e1fe3e4aSElliott Hughes glyphClass = Glyph 139*e1fe3e4aSElliott Hughes 140*e1fe3e4aSElliott Hughes def __init__( 141*e1fe3e4aSElliott Hughes self, 142*e1fe3e4aSElliott Hughes path, 143*e1fe3e4aSElliott Hughes glyphNameToFileNameFunc=None, 144*e1fe3e4aSElliott Hughes ufoFormatVersion=None, 145*e1fe3e4aSElliott Hughes validateRead=True, 146*e1fe3e4aSElliott Hughes validateWrite=True, 147*e1fe3e4aSElliott Hughes expectContentsFile=False, 148*e1fe3e4aSElliott Hughes ): 149*e1fe3e4aSElliott Hughes """ 150*e1fe3e4aSElliott Hughes 'path' should be a path (string) to an existing local directory, or 151*e1fe3e4aSElliott Hughes an instance of fs.base.FS class. 152*e1fe3e4aSElliott Hughes 153*e1fe3e4aSElliott Hughes The optional 'glyphNameToFileNameFunc' argument must be a callback 154*e1fe3e4aSElliott Hughes function that takes two arguments: a glyph name and a list of all 155*e1fe3e4aSElliott Hughes existing filenames (if any exist). It should return a file name 156*e1fe3e4aSElliott Hughes (including the .glif extension). The glyphNameToFileName function 157*e1fe3e4aSElliott Hughes is called whenever a file name is created for a given glyph name. 158*e1fe3e4aSElliott Hughes 159*e1fe3e4aSElliott Hughes ``validateRead`` will validate read operations. Its default is ``True``. 160*e1fe3e4aSElliott Hughes ``validateWrite`` will validate write operations. Its default is ``True``. 161*e1fe3e4aSElliott Hughes ``expectContentsFile`` will raise a GlifLibError if a contents.plist file is 162*e1fe3e4aSElliott Hughes not found on the glyph set file system. This should be set to ``True`` if you 163*e1fe3e4aSElliott Hughes are reading an existing UFO and ``False`` if you create a fresh glyph set. 164*e1fe3e4aSElliott Hughes """ 165*e1fe3e4aSElliott Hughes try: 166*e1fe3e4aSElliott Hughes ufoFormatVersion = UFOFormatVersion(ufoFormatVersion) 167*e1fe3e4aSElliott Hughes except ValueError as e: 168*e1fe3e4aSElliott Hughes from fontTools.ufoLib.errors import UnsupportedUFOFormat 169*e1fe3e4aSElliott Hughes 170*e1fe3e4aSElliott Hughes raise UnsupportedUFOFormat( 171*e1fe3e4aSElliott Hughes f"Unsupported UFO format: {ufoFormatVersion!r}" 172*e1fe3e4aSElliott Hughes ) from e 173*e1fe3e4aSElliott Hughes 174*e1fe3e4aSElliott Hughes if hasattr(path, "__fspath__"): # support os.PathLike objects 175*e1fe3e4aSElliott Hughes path = path.__fspath__() 176*e1fe3e4aSElliott Hughes 177*e1fe3e4aSElliott Hughes if isinstance(path, str): 178*e1fe3e4aSElliott Hughes try: 179*e1fe3e4aSElliott Hughes filesystem = fs.osfs.OSFS(path) 180*e1fe3e4aSElliott Hughes except fs.errors.CreateFailed: 181*e1fe3e4aSElliott Hughes raise GlifLibError("No glyphs directory '%s'" % path) 182*e1fe3e4aSElliott Hughes self._shouldClose = True 183*e1fe3e4aSElliott Hughes elif isinstance(path, fs.base.FS): 184*e1fe3e4aSElliott Hughes filesystem = path 185*e1fe3e4aSElliott Hughes try: 186*e1fe3e4aSElliott Hughes filesystem.check() 187*e1fe3e4aSElliott Hughes except fs.errors.FilesystemClosed: 188*e1fe3e4aSElliott Hughes raise GlifLibError("the filesystem '%s' is closed" % filesystem) 189*e1fe3e4aSElliott Hughes self._shouldClose = False 190*e1fe3e4aSElliott Hughes else: 191*e1fe3e4aSElliott Hughes raise TypeError( 192*e1fe3e4aSElliott Hughes "Expected a path string or fs object, found %s" % type(path).__name__ 193*e1fe3e4aSElliott Hughes ) 194*e1fe3e4aSElliott Hughes try: 195*e1fe3e4aSElliott Hughes path = filesystem.getsyspath("/") 196*e1fe3e4aSElliott Hughes except fs.errors.NoSysPath: 197*e1fe3e4aSElliott Hughes # network or in-memory FS may not map to the local one 198*e1fe3e4aSElliott Hughes path = str(filesystem) 199*e1fe3e4aSElliott Hughes # 'dirName' is kept for backward compatibility only, but it's DEPRECATED 200*e1fe3e4aSElliott Hughes # as it's not guaranteed that it maps to an existing OSFS directory. 201*e1fe3e4aSElliott Hughes # Client could use the FS api via the `self.fs` attribute instead. 202*e1fe3e4aSElliott Hughes self.dirName = fs.path.parts(path)[-1] 203*e1fe3e4aSElliott Hughes self.fs = filesystem 204*e1fe3e4aSElliott Hughes # if glyphSet contains no 'contents.plist', we consider it empty 205*e1fe3e4aSElliott Hughes self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) 206*e1fe3e4aSElliott Hughes if expectContentsFile and not self._havePreviousFile: 207*e1fe3e4aSElliott Hughes raise GlifLibError(f"{CONTENTS_FILENAME} is missing.") 208*e1fe3e4aSElliott Hughes # attribute kept for backward compatibility 209*e1fe3e4aSElliott Hughes self.ufoFormatVersion = ufoFormatVersion.major 210*e1fe3e4aSElliott Hughes self.ufoFormatVersionTuple = ufoFormatVersion 211*e1fe3e4aSElliott Hughes if glyphNameToFileNameFunc is None: 212*e1fe3e4aSElliott Hughes glyphNameToFileNameFunc = glyphNameToFileName 213*e1fe3e4aSElliott Hughes self.glyphNameToFileName = glyphNameToFileNameFunc 214*e1fe3e4aSElliott Hughes self._validateRead = validateRead 215*e1fe3e4aSElliott Hughes self._validateWrite = validateWrite 216*e1fe3e4aSElliott Hughes self._existingFileNames: set[str] | None = None 217*e1fe3e4aSElliott Hughes self._reverseContents = None 218*e1fe3e4aSElliott Hughes 219*e1fe3e4aSElliott Hughes self.rebuildContents() 220*e1fe3e4aSElliott Hughes 221*e1fe3e4aSElliott Hughes def rebuildContents(self, validateRead=None): 222*e1fe3e4aSElliott Hughes """ 223*e1fe3e4aSElliott Hughes Rebuild the contents dict by loading contents.plist. 224*e1fe3e4aSElliott Hughes 225*e1fe3e4aSElliott Hughes ``validateRead`` will validate the data, by default it is set to the 226*e1fe3e4aSElliott Hughes class's ``validateRead`` value, can be overridden. 227*e1fe3e4aSElliott Hughes """ 228*e1fe3e4aSElliott Hughes if validateRead is None: 229*e1fe3e4aSElliott Hughes validateRead = self._validateRead 230*e1fe3e4aSElliott Hughes contents = self._getPlist(CONTENTS_FILENAME, {}) 231*e1fe3e4aSElliott Hughes # validate the contents 232*e1fe3e4aSElliott Hughes if validateRead: 233*e1fe3e4aSElliott Hughes invalidFormat = False 234*e1fe3e4aSElliott Hughes if not isinstance(contents, dict): 235*e1fe3e4aSElliott Hughes invalidFormat = True 236*e1fe3e4aSElliott Hughes else: 237*e1fe3e4aSElliott Hughes for name, fileName in contents.items(): 238*e1fe3e4aSElliott Hughes if not isinstance(name, str): 239*e1fe3e4aSElliott Hughes invalidFormat = True 240*e1fe3e4aSElliott Hughes if not isinstance(fileName, str): 241*e1fe3e4aSElliott Hughes invalidFormat = True 242*e1fe3e4aSElliott Hughes elif not self.fs.exists(fileName): 243*e1fe3e4aSElliott Hughes raise GlifLibError( 244*e1fe3e4aSElliott Hughes "%s references a file that does not exist: %s" 245*e1fe3e4aSElliott Hughes % (CONTENTS_FILENAME, fileName) 246*e1fe3e4aSElliott Hughes ) 247*e1fe3e4aSElliott Hughes if invalidFormat: 248*e1fe3e4aSElliott Hughes raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME) 249*e1fe3e4aSElliott Hughes self.contents = contents 250*e1fe3e4aSElliott Hughes self._existingFileNames = None 251*e1fe3e4aSElliott Hughes self._reverseContents = None 252*e1fe3e4aSElliott Hughes 253*e1fe3e4aSElliott Hughes def getReverseContents(self): 254*e1fe3e4aSElliott Hughes """ 255*e1fe3e4aSElliott Hughes Return a reversed dict of self.contents, mapping file names to 256*e1fe3e4aSElliott Hughes glyph names. This is primarily an aid for custom glyph name to file 257*e1fe3e4aSElliott Hughes name schemes that want to make sure they don't generate duplicate 258*e1fe3e4aSElliott Hughes file names. The file names are converted to lowercase so we can 259*e1fe3e4aSElliott Hughes reliably check for duplicates that only differ in case, which is 260*e1fe3e4aSElliott Hughes important for case-insensitive file systems. 261*e1fe3e4aSElliott Hughes """ 262*e1fe3e4aSElliott Hughes if self._reverseContents is None: 263*e1fe3e4aSElliott Hughes d = {} 264*e1fe3e4aSElliott Hughes for k, v in self.contents.items(): 265*e1fe3e4aSElliott Hughes d[v.lower()] = k 266*e1fe3e4aSElliott Hughes self._reverseContents = d 267*e1fe3e4aSElliott Hughes return self._reverseContents 268*e1fe3e4aSElliott Hughes 269*e1fe3e4aSElliott Hughes def writeContents(self): 270*e1fe3e4aSElliott Hughes """ 271*e1fe3e4aSElliott Hughes Write the contents.plist file out to disk. Call this method when 272*e1fe3e4aSElliott Hughes you're done writing glyphs. 273*e1fe3e4aSElliott Hughes """ 274*e1fe3e4aSElliott Hughes self._writePlist(CONTENTS_FILENAME, self.contents) 275*e1fe3e4aSElliott Hughes 276*e1fe3e4aSElliott Hughes # layer info 277*e1fe3e4aSElliott Hughes 278*e1fe3e4aSElliott Hughes def readLayerInfo(self, info, validateRead=None): 279*e1fe3e4aSElliott Hughes """ 280*e1fe3e4aSElliott Hughes ``validateRead`` will validate the data, by default it is set to the 281*e1fe3e4aSElliott Hughes class's ``validateRead`` value, can be overridden. 282*e1fe3e4aSElliott Hughes """ 283*e1fe3e4aSElliott Hughes if validateRead is None: 284*e1fe3e4aSElliott Hughes validateRead = self._validateRead 285*e1fe3e4aSElliott Hughes infoDict = self._getPlist(LAYERINFO_FILENAME, {}) 286*e1fe3e4aSElliott Hughes if validateRead: 287*e1fe3e4aSElliott Hughes if not isinstance(infoDict, dict): 288*e1fe3e4aSElliott Hughes raise GlifLibError("layerinfo.plist is not properly formatted.") 289*e1fe3e4aSElliott Hughes infoDict = validateLayerInfoVersion3Data(infoDict) 290*e1fe3e4aSElliott Hughes # populate the object 291*e1fe3e4aSElliott Hughes for attr, value in infoDict.items(): 292*e1fe3e4aSElliott Hughes try: 293*e1fe3e4aSElliott Hughes setattr(info, attr, value) 294*e1fe3e4aSElliott Hughes except AttributeError: 295*e1fe3e4aSElliott Hughes raise GlifLibError( 296*e1fe3e4aSElliott Hughes "The supplied layer info object does not support setting a necessary attribute (%s)." 297*e1fe3e4aSElliott Hughes % attr 298*e1fe3e4aSElliott Hughes ) 299*e1fe3e4aSElliott Hughes 300*e1fe3e4aSElliott Hughes def writeLayerInfo(self, info, validateWrite=None): 301*e1fe3e4aSElliott Hughes """ 302*e1fe3e4aSElliott Hughes ``validateWrite`` will validate the data, by default it is set to the 303*e1fe3e4aSElliott Hughes class's ``validateWrite`` value, can be overridden. 304*e1fe3e4aSElliott Hughes """ 305*e1fe3e4aSElliott Hughes if validateWrite is None: 306*e1fe3e4aSElliott Hughes validateWrite = self._validateWrite 307*e1fe3e4aSElliott Hughes if self.ufoFormatVersionTuple.major < 3: 308*e1fe3e4aSElliott Hughes raise GlifLibError( 309*e1fe3e4aSElliott Hughes "layerinfo.plist is not allowed in UFO %d." 310*e1fe3e4aSElliott Hughes % self.ufoFormatVersionTuple.major 311*e1fe3e4aSElliott Hughes ) 312*e1fe3e4aSElliott Hughes # gather data 313*e1fe3e4aSElliott Hughes infoData = {} 314*e1fe3e4aSElliott Hughes for attr in layerInfoVersion3ValueData.keys(): 315*e1fe3e4aSElliott Hughes if hasattr(info, attr): 316*e1fe3e4aSElliott Hughes try: 317*e1fe3e4aSElliott Hughes value = getattr(info, attr) 318*e1fe3e4aSElliott Hughes except AttributeError: 319*e1fe3e4aSElliott Hughes raise GlifLibError( 320*e1fe3e4aSElliott Hughes "The supplied info object does not support getting a necessary attribute (%s)." 321*e1fe3e4aSElliott Hughes % attr 322*e1fe3e4aSElliott Hughes ) 323*e1fe3e4aSElliott Hughes if value is None or (attr == "lib" and not value): 324*e1fe3e4aSElliott Hughes continue 325*e1fe3e4aSElliott Hughes infoData[attr] = value 326*e1fe3e4aSElliott Hughes if infoData: 327*e1fe3e4aSElliott Hughes # validate 328*e1fe3e4aSElliott Hughes if validateWrite: 329*e1fe3e4aSElliott Hughes infoData = validateLayerInfoVersion3Data(infoData) 330*e1fe3e4aSElliott Hughes # write file 331*e1fe3e4aSElliott Hughes self._writePlist(LAYERINFO_FILENAME, infoData) 332*e1fe3e4aSElliott Hughes elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME): 333*e1fe3e4aSElliott Hughes # data empty, remove existing file 334*e1fe3e4aSElliott Hughes self.fs.remove(LAYERINFO_FILENAME) 335*e1fe3e4aSElliott Hughes 336*e1fe3e4aSElliott Hughes def getGLIF(self, glyphName): 337*e1fe3e4aSElliott Hughes """ 338*e1fe3e4aSElliott Hughes Get the raw GLIF text for a given glyph name. This only works 339*e1fe3e4aSElliott Hughes for GLIF files that are already on disk. 340*e1fe3e4aSElliott Hughes 341*e1fe3e4aSElliott Hughes This method is useful in situations when the raw XML needs to be 342*e1fe3e4aSElliott Hughes read from a glyph set for a particular glyph before fully parsing 343*e1fe3e4aSElliott Hughes it into an object structure via the readGlyph method. 344*e1fe3e4aSElliott Hughes 345*e1fe3e4aSElliott Hughes Raises KeyError if 'glyphName' is not in contents.plist, or 346*e1fe3e4aSElliott Hughes GlifLibError if the file associated with can't be found. 347*e1fe3e4aSElliott Hughes """ 348*e1fe3e4aSElliott Hughes fileName = self.contents[glyphName] 349*e1fe3e4aSElliott Hughes try: 350*e1fe3e4aSElliott Hughes return self.fs.readbytes(fileName) 351*e1fe3e4aSElliott Hughes except fs.errors.ResourceNotFound: 352*e1fe3e4aSElliott Hughes raise GlifLibError( 353*e1fe3e4aSElliott Hughes "The file '%s' associated with glyph '%s' in contents.plist " 354*e1fe3e4aSElliott Hughes "does not exist on %s" % (fileName, glyphName, self.fs) 355*e1fe3e4aSElliott Hughes ) 356*e1fe3e4aSElliott Hughes 357*e1fe3e4aSElliott Hughes def getGLIFModificationTime(self, glyphName): 358*e1fe3e4aSElliott Hughes """ 359*e1fe3e4aSElliott Hughes Returns the modification time for the GLIF file with 'glyphName', as 360*e1fe3e4aSElliott Hughes a floating point number giving the number of seconds since the epoch. 361*e1fe3e4aSElliott Hughes Return None if the associated file does not exist or the underlying 362*e1fe3e4aSElliott Hughes filesystem does not support getting modified times. 363*e1fe3e4aSElliott Hughes Raises KeyError if the glyphName is not in contents.plist. 364*e1fe3e4aSElliott Hughes """ 365*e1fe3e4aSElliott Hughes fileName = self.contents[glyphName] 366*e1fe3e4aSElliott Hughes return self.getFileModificationTime(fileName) 367*e1fe3e4aSElliott Hughes 368*e1fe3e4aSElliott Hughes # reading/writing API 369*e1fe3e4aSElliott Hughes 370*e1fe3e4aSElliott Hughes def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=None): 371*e1fe3e4aSElliott Hughes """ 372*e1fe3e4aSElliott Hughes Read a .glif file for 'glyphName' from the glyph set. The 373*e1fe3e4aSElliott Hughes 'glyphObject' argument can be any kind of object (even None); 374*e1fe3e4aSElliott Hughes the readGlyph() method will attempt to set the following 375*e1fe3e4aSElliott Hughes attributes on it: 376*e1fe3e4aSElliott Hughes 377*e1fe3e4aSElliott Hughes width 378*e1fe3e4aSElliott Hughes the advance width of the glyph 379*e1fe3e4aSElliott Hughes height 380*e1fe3e4aSElliott Hughes the advance height of the glyph 381*e1fe3e4aSElliott Hughes unicodes 382*e1fe3e4aSElliott Hughes a list of unicode values for this glyph 383*e1fe3e4aSElliott Hughes note 384*e1fe3e4aSElliott Hughes a string 385*e1fe3e4aSElliott Hughes lib 386*e1fe3e4aSElliott Hughes a dictionary containing custom data 387*e1fe3e4aSElliott Hughes image 388*e1fe3e4aSElliott Hughes a dictionary containing image data 389*e1fe3e4aSElliott Hughes guidelines 390*e1fe3e4aSElliott Hughes a list of guideline data dictionaries 391*e1fe3e4aSElliott Hughes anchors 392*e1fe3e4aSElliott Hughes a list of anchor data dictionaries 393*e1fe3e4aSElliott Hughes 394*e1fe3e4aSElliott Hughes All attributes are optional, in two ways: 395*e1fe3e4aSElliott Hughes 396*e1fe3e4aSElliott Hughes 1) An attribute *won't* be set if the .glif file doesn't 397*e1fe3e4aSElliott Hughes contain data for it. 'glyphObject' will have to deal 398*e1fe3e4aSElliott Hughes with default values itself. 399*e1fe3e4aSElliott Hughes 2) If setting the attribute fails with an AttributeError 400*e1fe3e4aSElliott Hughes (for example if the 'glyphObject' attribute is read- 401*e1fe3e4aSElliott Hughes only), readGlyph() will not propagate that exception, 402*e1fe3e4aSElliott Hughes but ignore that attribute. 403*e1fe3e4aSElliott Hughes 404*e1fe3e4aSElliott Hughes To retrieve outline information, you need to pass an object 405*e1fe3e4aSElliott Hughes conforming to the PointPen protocol as the 'pointPen' argument. 406*e1fe3e4aSElliott Hughes This argument may be None if you don't need the outline data. 407*e1fe3e4aSElliott Hughes 408*e1fe3e4aSElliott Hughes readGlyph() will raise KeyError if the glyph is not present in 409*e1fe3e4aSElliott Hughes the glyph set. 410*e1fe3e4aSElliott Hughes 411*e1fe3e4aSElliott Hughes ``validate`` will validate the data, by default it is set to the 412*e1fe3e4aSElliott Hughes class's ``validateRead`` value, can be overridden. 413*e1fe3e4aSElliott Hughes """ 414*e1fe3e4aSElliott Hughes if validate is None: 415*e1fe3e4aSElliott Hughes validate = self._validateRead 416*e1fe3e4aSElliott Hughes text = self.getGLIF(glyphName) 417*e1fe3e4aSElliott Hughes try: 418*e1fe3e4aSElliott Hughes tree = _glifTreeFromString(text) 419*e1fe3e4aSElliott Hughes formatVersions = GLIFFormatVersion.supported_versions( 420*e1fe3e4aSElliott Hughes self.ufoFormatVersionTuple 421*e1fe3e4aSElliott Hughes ) 422*e1fe3e4aSElliott Hughes _readGlyphFromTree( 423*e1fe3e4aSElliott Hughes tree, 424*e1fe3e4aSElliott Hughes glyphObject, 425*e1fe3e4aSElliott Hughes pointPen, 426*e1fe3e4aSElliott Hughes formatVersions=formatVersions, 427*e1fe3e4aSElliott Hughes validate=validate, 428*e1fe3e4aSElliott Hughes ) 429*e1fe3e4aSElliott Hughes except GlifLibError as glifLibError: 430*e1fe3e4aSElliott Hughes # Re-raise with a note that gives extra context, describing where 431*e1fe3e4aSElliott Hughes # the error occurred. 432*e1fe3e4aSElliott Hughes fileName = self.contents[glyphName] 433*e1fe3e4aSElliott Hughes try: 434*e1fe3e4aSElliott Hughes glifLocation = f"'{self.fs.getsyspath(fileName)}'" 435*e1fe3e4aSElliott Hughes except fs.errors.NoSysPath: 436*e1fe3e4aSElliott Hughes # Network or in-memory FS may not map to a local path, so use 437*e1fe3e4aSElliott Hughes # the best string representation we have. 438*e1fe3e4aSElliott Hughes glifLocation = f"'{fileName}' from '{str(self.fs)}'" 439*e1fe3e4aSElliott Hughes 440*e1fe3e4aSElliott Hughes glifLibError._add_note( 441*e1fe3e4aSElliott Hughes f"The issue is in glyph '{glyphName}', located in {glifLocation}." 442*e1fe3e4aSElliott Hughes ) 443*e1fe3e4aSElliott Hughes raise 444*e1fe3e4aSElliott Hughes 445*e1fe3e4aSElliott Hughes def writeGlyph( 446*e1fe3e4aSElliott Hughes self, 447*e1fe3e4aSElliott Hughes glyphName, 448*e1fe3e4aSElliott Hughes glyphObject=None, 449*e1fe3e4aSElliott Hughes drawPointsFunc=None, 450*e1fe3e4aSElliott Hughes formatVersion=None, 451*e1fe3e4aSElliott Hughes validate=None, 452*e1fe3e4aSElliott Hughes ): 453*e1fe3e4aSElliott Hughes """ 454*e1fe3e4aSElliott Hughes Write a .glif file for 'glyphName' to the glyph set. The 455*e1fe3e4aSElliott Hughes 'glyphObject' argument can be any kind of object (even None); 456*e1fe3e4aSElliott Hughes the writeGlyph() method will attempt to get the following 457*e1fe3e4aSElliott Hughes attributes from it: 458*e1fe3e4aSElliott Hughes 459*e1fe3e4aSElliott Hughes width 460*e1fe3e4aSElliott Hughes the advance width of the glyph 461*e1fe3e4aSElliott Hughes height 462*e1fe3e4aSElliott Hughes the advance height of the glyph 463*e1fe3e4aSElliott Hughes unicodes 464*e1fe3e4aSElliott Hughes a list of unicode values for this glyph 465*e1fe3e4aSElliott Hughes note 466*e1fe3e4aSElliott Hughes a string 467*e1fe3e4aSElliott Hughes lib 468*e1fe3e4aSElliott Hughes a dictionary containing custom data 469*e1fe3e4aSElliott Hughes image 470*e1fe3e4aSElliott Hughes a dictionary containing image data 471*e1fe3e4aSElliott Hughes guidelines 472*e1fe3e4aSElliott Hughes a list of guideline data dictionaries 473*e1fe3e4aSElliott Hughes anchors 474*e1fe3e4aSElliott Hughes a list of anchor data dictionaries 475*e1fe3e4aSElliott Hughes 476*e1fe3e4aSElliott Hughes All attributes are optional: if 'glyphObject' doesn't 477*e1fe3e4aSElliott Hughes have the attribute, it will simply be skipped. 478*e1fe3e4aSElliott Hughes 479*e1fe3e4aSElliott Hughes To write outline data to the .glif file, writeGlyph() needs 480*e1fe3e4aSElliott Hughes a function (any callable object actually) that will take one 481*e1fe3e4aSElliott Hughes argument: an object that conforms to the PointPen protocol. 482*e1fe3e4aSElliott Hughes The function will be called by writeGlyph(); it has to call the 483*e1fe3e4aSElliott Hughes proper PointPen methods to transfer the outline to the .glif file. 484*e1fe3e4aSElliott Hughes 485*e1fe3e4aSElliott Hughes The GLIF format version will be chosen based on the ufoFormatVersion 486*e1fe3e4aSElliott Hughes passed during the creation of this object. If a particular format 487*e1fe3e4aSElliott Hughes version is desired, it can be passed with the formatVersion argument. 488*e1fe3e4aSElliott Hughes The formatVersion argument accepts either a tuple of integers for 489*e1fe3e4aSElliott Hughes (major, minor), or a single integer for the major digit only (with 490*e1fe3e4aSElliott Hughes minor digit implied as 0). 491*e1fe3e4aSElliott Hughes 492*e1fe3e4aSElliott Hughes An UnsupportedGLIFFormat exception is raised if the requested GLIF 493*e1fe3e4aSElliott Hughes formatVersion is not supported. 494*e1fe3e4aSElliott Hughes 495*e1fe3e4aSElliott Hughes ``validate`` will validate the data, by default it is set to the 496*e1fe3e4aSElliott Hughes class's ``validateWrite`` value, can be overridden. 497*e1fe3e4aSElliott Hughes """ 498*e1fe3e4aSElliott Hughes if formatVersion is None: 499*e1fe3e4aSElliott Hughes formatVersion = GLIFFormatVersion.default(self.ufoFormatVersionTuple) 500*e1fe3e4aSElliott Hughes else: 501*e1fe3e4aSElliott Hughes try: 502*e1fe3e4aSElliott Hughes formatVersion = GLIFFormatVersion(formatVersion) 503*e1fe3e4aSElliott Hughes except ValueError as e: 504*e1fe3e4aSElliott Hughes from fontTools.ufoLib.errors import UnsupportedGLIFFormat 505*e1fe3e4aSElliott Hughes 506*e1fe3e4aSElliott Hughes raise UnsupportedGLIFFormat( 507*e1fe3e4aSElliott Hughes f"Unsupported GLIF format version: {formatVersion!r}" 508*e1fe3e4aSElliott Hughes ) from e 509*e1fe3e4aSElliott Hughes if formatVersion not in GLIFFormatVersion.supported_versions( 510*e1fe3e4aSElliott Hughes self.ufoFormatVersionTuple 511*e1fe3e4aSElliott Hughes ): 512*e1fe3e4aSElliott Hughes from fontTools.ufoLib.errors import UnsupportedGLIFFormat 513*e1fe3e4aSElliott Hughes 514*e1fe3e4aSElliott Hughes raise UnsupportedGLIFFormat( 515*e1fe3e4aSElliott Hughes f"Unsupported GLIF format version ({formatVersion!s}) " 516*e1fe3e4aSElliott Hughes f"for UFO format version {self.ufoFormatVersionTuple!s}." 517*e1fe3e4aSElliott Hughes ) 518*e1fe3e4aSElliott Hughes if validate is None: 519*e1fe3e4aSElliott Hughes validate = self._validateWrite 520*e1fe3e4aSElliott Hughes fileName = self.contents.get(glyphName) 521*e1fe3e4aSElliott Hughes if fileName is None: 522*e1fe3e4aSElliott Hughes if self._existingFileNames is None: 523*e1fe3e4aSElliott Hughes self._existingFileNames = { 524*e1fe3e4aSElliott Hughes fileName.lower() for fileName in self.contents.values() 525*e1fe3e4aSElliott Hughes } 526*e1fe3e4aSElliott Hughes fileName = self.glyphNameToFileName(glyphName, self._existingFileNames) 527*e1fe3e4aSElliott Hughes self.contents[glyphName] = fileName 528*e1fe3e4aSElliott Hughes self._existingFileNames.add(fileName.lower()) 529*e1fe3e4aSElliott Hughes if self._reverseContents is not None: 530*e1fe3e4aSElliott Hughes self._reverseContents[fileName.lower()] = glyphName 531*e1fe3e4aSElliott Hughes data = _writeGlyphToBytes( 532*e1fe3e4aSElliott Hughes glyphName, 533*e1fe3e4aSElliott Hughes glyphObject, 534*e1fe3e4aSElliott Hughes drawPointsFunc, 535*e1fe3e4aSElliott Hughes formatVersion=formatVersion, 536*e1fe3e4aSElliott Hughes validate=validate, 537*e1fe3e4aSElliott Hughes ) 538*e1fe3e4aSElliott Hughes if ( 539*e1fe3e4aSElliott Hughes self._havePreviousFile 540*e1fe3e4aSElliott Hughes and self.fs.exists(fileName) 541*e1fe3e4aSElliott Hughes and data == self.fs.readbytes(fileName) 542*e1fe3e4aSElliott Hughes ): 543*e1fe3e4aSElliott Hughes return 544*e1fe3e4aSElliott Hughes self.fs.writebytes(fileName, data) 545*e1fe3e4aSElliott Hughes 546*e1fe3e4aSElliott Hughes def deleteGlyph(self, glyphName): 547*e1fe3e4aSElliott Hughes """Permanently delete the glyph from the glyph set on disk. Will 548*e1fe3e4aSElliott Hughes raise KeyError if the glyph is not present in the glyph set. 549*e1fe3e4aSElliott Hughes """ 550*e1fe3e4aSElliott Hughes fileName = self.contents[glyphName] 551*e1fe3e4aSElliott Hughes self.fs.remove(fileName) 552*e1fe3e4aSElliott Hughes if self._existingFileNames is not None: 553*e1fe3e4aSElliott Hughes self._existingFileNames.remove(fileName.lower()) 554*e1fe3e4aSElliott Hughes if self._reverseContents is not None: 555*e1fe3e4aSElliott Hughes del self._reverseContents[fileName.lower()] 556*e1fe3e4aSElliott Hughes del self.contents[glyphName] 557*e1fe3e4aSElliott Hughes 558*e1fe3e4aSElliott Hughes # dict-like support 559*e1fe3e4aSElliott Hughes 560*e1fe3e4aSElliott Hughes def keys(self): 561*e1fe3e4aSElliott Hughes return list(self.contents.keys()) 562*e1fe3e4aSElliott Hughes 563*e1fe3e4aSElliott Hughes def has_key(self, glyphName): 564*e1fe3e4aSElliott Hughes return glyphName in self.contents 565*e1fe3e4aSElliott Hughes 566*e1fe3e4aSElliott Hughes __contains__ = has_key 567*e1fe3e4aSElliott Hughes 568*e1fe3e4aSElliott Hughes def __len__(self): 569*e1fe3e4aSElliott Hughes return len(self.contents) 570*e1fe3e4aSElliott Hughes 571*e1fe3e4aSElliott Hughes def __getitem__(self, glyphName): 572*e1fe3e4aSElliott Hughes if glyphName not in self.contents: 573*e1fe3e4aSElliott Hughes raise KeyError(glyphName) 574*e1fe3e4aSElliott Hughes return self.glyphClass(glyphName, self) 575*e1fe3e4aSElliott Hughes 576*e1fe3e4aSElliott Hughes # quickly fetch unicode values 577*e1fe3e4aSElliott Hughes 578*e1fe3e4aSElliott Hughes def getUnicodes(self, glyphNames=None): 579*e1fe3e4aSElliott Hughes """ 580*e1fe3e4aSElliott Hughes Return a dictionary that maps glyph names to lists containing 581*e1fe3e4aSElliott Hughes the unicode value[s] for that glyph, if any. This parses the .glif 582*e1fe3e4aSElliott Hughes files partially, so it is a lot faster than parsing all files completely. 583*e1fe3e4aSElliott Hughes By default this checks all glyphs, but a subset can be passed with glyphNames. 584*e1fe3e4aSElliott Hughes """ 585*e1fe3e4aSElliott Hughes unicodes = {} 586*e1fe3e4aSElliott Hughes if glyphNames is None: 587*e1fe3e4aSElliott Hughes glyphNames = self.contents.keys() 588*e1fe3e4aSElliott Hughes for glyphName in glyphNames: 589*e1fe3e4aSElliott Hughes text = self.getGLIF(glyphName) 590*e1fe3e4aSElliott Hughes unicodes[glyphName] = _fetchUnicodes(text) 591*e1fe3e4aSElliott Hughes return unicodes 592*e1fe3e4aSElliott Hughes 593*e1fe3e4aSElliott Hughes def getComponentReferences(self, glyphNames=None): 594*e1fe3e4aSElliott Hughes """ 595*e1fe3e4aSElliott Hughes Return a dictionary that maps glyph names to lists containing the 596*e1fe3e4aSElliott Hughes base glyph name of components in the glyph. This parses the .glif 597*e1fe3e4aSElliott Hughes files partially, so it is a lot faster than parsing all files completely. 598*e1fe3e4aSElliott Hughes By default this checks all glyphs, but a subset can be passed with glyphNames. 599*e1fe3e4aSElliott Hughes """ 600*e1fe3e4aSElliott Hughes components = {} 601*e1fe3e4aSElliott Hughes if glyphNames is None: 602*e1fe3e4aSElliott Hughes glyphNames = self.contents.keys() 603*e1fe3e4aSElliott Hughes for glyphName in glyphNames: 604*e1fe3e4aSElliott Hughes text = self.getGLIF(glyphName) 605*e1fe3e4aSElliott Hughes components[glyphName] = _fetchComponentBases(text) 606*e1fe3e4aSElliott Hughes return components 607*e1fe3e4aSElliott Hughes 608*e1fe3e4aSElliott Hughes def getImageReferences(self, glyphNames=None): 609*e1fe3e4aSElliott Hughes """ 610*e1fe3e4aSElliott Hughes Return a dictionary that maps glyph names to the file name of the image 611*e1fe3e4aSElliott Hughes referenced by the glyph. This parses the .glif files partially, so it is a 612*e1fe3e4aSElliott Hughes lot faster than parsing all files completely. 613*e1fe3e4aSElliott Hughes By default this checks all glyphs, but a subset can be passed with glyphNames. 614*e1fe3e4aSElliott Hughes """ 615*e1fe3e4aSElliott Hughes images = {} 616*e1fe3e4aSElliott Hughes if glyphNames is None: 617*e1fe3e4aSElliott Hughes glyphNames = self.contents.keys() 618*e1fe3e4aSElliott Hughes for glyphName in glyphNames: 619*e1fe3e4aSElliott Hughes text = self.getGLIF(glyphName) 620*e1fe3e4aSElliott Hughes images[glyphName] = _fetchImageFileName(text) 621*e1fe3e4aSElliott Hughes return images 622*e1fe3e4aSElliott Hughes 623*e1fe3e4aSElliott Hughes def close(self): 624*e1fe3e4aSElliott Hughes if self._shouldClose: 625*e1fe3e4aSElliott Hughes self.fs.close() 626*e1fe3e4aSElliott Hughes 627*e1fe3e4aSElliott Hughes def __enter__(self): 628*e1fe3e4aSElliott Hughes return self 629*e1fe3e4aSElliott Hughes 630*e1fe3e4aSElliott Hughes def __exit__(self, exc_type, exc_value, exc_tb): 631*e1fe3e4aSElliott Hughes self.close() 632*e1fe3e4aSElliott Hughes 633*e1fe3e4aSElliott Hughes 634*e1fe3e4aSElliott Hughes# ----------------------- 635*e1fe3e4aSElliott Hughes# Glyph Name to File Name 636*e1fe3e4aSElliott Hughes# ----------------------- 637*e1fe3e4aSElliott Hughes 638*e1fe3e4aSElliott Hughes 639*e1fe3e4aSElliott Hughesdef glyphNameToFileName(glyphName, existingFileNames): 640*e1fe3e4aSElliott Hughes """ 641*e1fe3e4aSElliott Hughes Wrapper around the userNameToFileName function in filenames.py 642*e1fe3e4aSElliott Hughes 643*e1fe3e4aSElliott Hughes Note that existingFileNames should be a set for large glyphsets 644*e1fe3e4aSElliott Hughes or performance will suffer. 645*e1fe3e4aSElliott Hughes """ 646*e1fe3e4aSElliott Hughes if existingFileNames is None: 647*e1fe3e4aSElliott Hughes existingFileNames = set() 648*e1fe3e4aSElliott Hughes return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif") 649*e1fe3e4aSElliott Hughes 650*e1fe3e4aSElliott Hughes 651*e1fe3e4aSElliott Hughes# ----------------------- 652*e1fe3e4aSElliott Hughes# GLIF To and From String 653*e1fe3e4aSElliott Hughes# ----------------------- 654*e1fe3e4aSElliott Hughes 655*e1fe3e4aSElliott Hughes 656*e1fe3e4aSElliott Hughesdef readGlyphFromString( 657*e1fe3e4aSElliott Hughes aString, 658*e1fe3e4aSElliott Hughes glyphObject=None, 659*e1fe3e4aSElliott Hughes pointPen=None, 660*e1fe3e4aSElliott Hughes formatVersions=None, 661*e1fe3e4aSElliott Hughes validate=True, 662*e1fe3e4aSElliott Hughes): 663*e1fe3e4aSElliott Hughes """ 664*e1fe3e4aSElliott Hughes Read .glif data from a string into a glyph object. 665*e1fe3e4aSElliott Hughes 666*e1fe3e4aSElliott Hughes The 'glyphObject' argument can be any kind of object (even None); 667*e1fe3e4aSElliott Hughes the readGlyphFromString() method will attempt to set the following 668*e1fe3e4aSElliott Hughes attributes on it: 669*e1fe3e4aSElliott Hughes 670*e1fe3e4aSElliott Hughes width 671*e1fe3e4aSElliott Hughes the advance width of the glyph 672*e1fe3e4aSElliott Hughes height 673*e1fe3e4aSElliott Hughes the advance height of the glyph 674*e1fe3e4aSElliott Hughes unicodes 675*e1fe3e4aSElliott Hughes a list of unicode values for this glyph 676*e1fe3e4aSElliott Hughes note 677*e1fe3e4aSElliott Hughes a string 678*e1fe3e4aSElliott Hughes lib 679*e1fe3e4aSElliott Hughes a dictionary containing custom data 680*e1fe3e4aSElliott Hughes image 681*e1fe3e4aSElliott Hughes a dictionary containing image data 682*e1fe3e4aSElliott Hughes guidelines 683*e1fe3e4aSElliott Hughes a list of guideline data dictionaries 684*e1fe3e4aSElliott Hughes anchors 685*e1fe3e4aSElliott Hughes a list of anchor data dictionaries 686*e1fe3e4aSElliott Hughes 687*e1fe3e4aSElliott Hughes All attributes are optional, in two ways: 688*e1fe3e4aSElliott Hughes 689*e1fe3e4aSElliott Hughes 1) An attribute *won't* be set if the .glif file doesn't 690*e1fe3e4aSElliott Hughes contain data for it. 'glyphObject' will have to deal 691*e1fe3e4aSElliott Hughes with default values itself. 692*e1fe3e4aSElliott Hughes 2) If setting the attribute fails with an AttributeError 693*e1fe3e4aSElliott Hughes (for example if the 'glyphObject' attribute is read- 694*e1fe3e4aSElliott Hughes only), readGlyphFromString() will not propagate that 695*e1fe3e4aSElliott Hughes exception, but ignore that attribute. 696*e1fe3e4aSElliott Hughes 697*e1fe3e4aSElliott Hughes To retrieve outline information, you need to pass an object 698*e1fe3e4aSElliott Hughes conforming to the PointPen protocol as the 'pointPen' argument. 699*e1fe3e4aSElliott Hughes This argument may be None if you don't need the outline data. 700*e1fe3e4aSElliott Hughes 701*e1fe3e4aSElliott Hughes The formatVersions optional argument define the GLIF format versions 702*e1fe3e4aSElliott Hughes that are allowed to be read. 703*e1fe3e4aSElliott Hughes The type is Optional[Iterable[Tuple[int, int], int]]. It can contain 704*e1fe3e4aSElliott Hughes either integers (for the major versions to be allowed, with minor 705*e1fe3e4aSElliott Hughes digits defaulting to 0), or tuples of integers to specify both 706*e1fe3e4aSElliott Hughes (major, minor) versions. 707*e1fe3e4aSElliott Hughes By default when formatVersions is None all the GLIF format versions 708*e1fe3e4aSElliott Hughes currently defined are allowed to be read. 709*e1fe3e4aSElliott Hughes 710*e1fe3e4aSElliott Hughes ``validate`` will validate the read data. It is set to ``True`` by default. 711*e1fe3e4aSElliott Hughes """ 712*e1fe3e4aSElliott Hughes tree = _glifTreeFromString(aString) 713*e1fe3e4aSElliott Hughes 714*e1fe3e4aSElliott Hughes if formatVersions is None: 715*e1fe3e4aSElliott Hughes validFormatVersions = GLIFFormatVersion.supported_versions() 716*e1fe3e4aSElliott Hughes else: 717*e1fe3e4aSElliott Hughes validFormatVersions, invalidFormatVersions = set(), set() 718*e1fe3e4aSElliott Hughes for v in formatVersions: 719*e1fe3e4aSElliott Hughes try: 720*e1fe3e4aSElliott Hughes formatVersion = GLIFFormatVersion(v) 721*e1fe3e4aSElliott Hughes except ValueError: 722*e1fe3e4aSElliott Hughes invalidFormatVersions.add(v) 723*e1fe3e4aSElliott Hughes else: 724*e1fe3e4aSElliott Hughes validFormatVersions.add(formatVersion) 725*e1fe3e4aSElliott Hughes if not validFormatVersions: 726*e1fe3e4aSElliott Hughes raise ValueError( 727*e1fe3e4aSElliott Hughes "None of the requested GLIF formatVersions are supported: " 728*e1fe3e4aSElliott Hughes f"{formatVersions!r}" 729*e1fe3e4aSElliott Hughes ) 730*e1fe3e4aSElliott Hughes 731*e1fe3e4aSElliott Hughes _readGlyphFromTree( 732*e1fe3e4aSElliott Hughes tree, 733*e1fe3e4aSElliott Hughes glyphObject, 734*e1fe3e4aSElliott Hughes pointPen, 735*e1fe3e4aSElliott Hughes formatVersions=validFormatVersions, 736*e1fe3e4aSElliott Hughes validate=validate, 737*e1fe3e4aSElliott Hughes ) 738*e1fe3e4aSElliott Hughes 739*e1fe3e4aSElliott Hughes 740*e1fe3e4aSElliott Hughesdef _writeGlyphToBytes( 741*e1fe3e4aSElliott Hughes glyphName, 742*e1fe3e4aSElliott Hughes glyphObject=None, 743*e1fe3e4aSElliott Hughes drawPointsFunc=None, 744*e1fe3e4aSElliott Hughes writer=None, 745*e1fe3e4aSElliott Hughes formatVersion=None, 746*e1fe3e4aSElliott Hughes validate=True, 747*e1fe3e4aSElliott Hughes): 748*e1fe3e4aSElliott Hughes """Return .glif data for a glyph as a UTF-8 encoded bytes string.""" 749*e1fe3e4aSElliott Hughes try: 750*e1fe3e4aSElliott Hughes formatVersion = GLIFFormatVersion(formatVersion) 751*e1fe3e4aSElliott Hughes except ValueError: 752*e1fe3e4aSElliott Hughes from fontTools.ufoLib.errors import UnsupportedGLIFFormat 753*e1fe3e4aSElliott Hughes 754*e1fe3e4aSElliott Hughes raise UnsupportedGLIFFormat( 755*e1fe3e4aSElliott Hughes "Unsupported GLIF format version: {formatVersion!r}" 756*e1fe3e4aSElliott Hughes ) 757*e1fe3e4aSElliott Hughes # start 758*e1fe3e4aSElliott Hughes if validate and not isinstance(glyphName, str): 759*e1fe3e4aSElliott Hughes raise GlifLibError("The glyph name is not properly formatted.") 760*e1fe3e4aSElliott Hughes if validate and len(glyphName) == 0: 761*e1fe3e4aSElliott Hughes raise GlifLibError("The glyph name is empty.") 762*e1fe3e4aSElliott Hughes glyphAttrs = OrderedDict( 763*e1fe3e4aSElliott Hughes [("name", glyphName), ("format", repr(formatVersion.major))] 764*e1fe3e4aSElliott Hughes ) 765*e1fe3e4aSElliott Hughes if formatVersion.minor != 0: 766*e1fe3e4aSElliott Hughes glyphAttrs["formatMinor"] = repr(formatVersion.minor) 767*e1fe3e4aSElliott Hughes root = etree.Element("glyph", glyphAttrs) 768*e1fe3e4aSElliott Hughes identifiers = set() 769*e1fe3e4aSElliott Hughes # advance 770*e1fe3e4aSElliott Hughes _writeAdvance(glyphObject, root, validate) 771*e1fe3e4aSElliott Hughes # unicodes 772*e1fe3e4aSElliott Hughes if getattr(glyphObject, "unicodes", None): 773*e1fe3e4aSElliott Hughes _writeUnicodes(glyphObject, root, validate) 774*e1fe3e4aSElliott Hughes # note 775*e1fe3e4aSElliott Hughes if getattr(glyphObject, "note", None): 776*e1fe3e4aSElliott Hughes _writeNote(glyphObject, root, validate) 777*e1fe3e4aSElliott Hughes # image 778*e1fe3e4aSElliott Hughes if formatVersion.major >= 2 and getattr(glyphObject, "image", None): 779*e1fe3e4aSElliott Hughes _writeImage(glyphObject, root, validate) 780*e1fe3e4aSElliott Hughes # guidelines 781*e1fe3e4aSElliott Hughes if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None): 782*e1fe3e4aSElliott Hughes _writeGuidelines(glyphObject, root, identifiers, validate) 783*e1fe3e4aSElliott Hughes # anchors 784*e1fe3e4aSElliott Hughes anchors = getattr(glyphObject, "anchors", None) 785*e1fe3e4aSElliott Hughes if formatVersion.major >= 2 and anchors: 786*e1fe3e4aSElliott Hughes _writeAnchors(glyphObject, root, identifiers, validate) 787*e1fe3e4aSElliott Hughes # outline 788*e1fe3e4aSElliott Hughes if drawPointsFunc is not None: 789*e1fe3e4aSElliott Hughes outline = etree.SubElement(root, "outline") 790*e1fe3e4aSElliott Hughes pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate) 791*e1fe3e4aSElliott Hughes drawPointsFunc(pen) 792*e1fe3e4aSElliott Hughes if formatVersion.major == 1 and anchors: 793*e1fe3e4aSElliott Hughes _writeAnchorsFormat1(pen, anchors, validate) 794*e1fe3e4aSElliott Hughes # prevent lxml from writing self-closing tags 795*e1fe3e4aSElliott Hughes if not len(outline): 796*e1fe3e4aSElliott Hughes outline.text = "\n " 797*e1fe3e4aSElliott Hughes # lib 798*e1fe3e4aSElliott Hughes if getattr(glyphObject, "lib", None): 799*e1fe3e4aSElliott Hughes _writeLib(glyphObject, root, validate) 800*e1fe3e4aSElliott Hughes # return the text 801*e1fe3e4aSElliott Hughes data = etree.tostring( 802*e1fe3e4aSElliott Hughes root, encoding="UTF-8", xml_declaration=True, pretty_print=True 803*e1fe3e4aSElliott Hughes ) 804*e1fe3e4aSElliott Hughes return data 805*e1fe3e4aSElliott Hughes 806*e1fe3e4aSElliott Hughes 807*e1fe3e4aSElliott Hughesdef writeGlyphToString( 808*e1fe3e4aSElliott Hughes glyphName, 809*e1fe3e4aSElliott Hughes glyphObject=None, 810*e1fe3e4aSElliott Hughes drawPointsFunc=None, 811*e1fe3e4aSElliott Hughes formatVersion=None, 812*e1fe3e4aSElliott Hughes validate=True, 813*e1fe3e4aSElliott Hughes): 814*e1fe3e4aSElliott Hughes """ 815*e1fe3e4aSElliott Hughes Return .glif data for a glyph as a string. The XML declaration's 816*e1fe3e4aSElliott Hughes encoding is always set to "UTF-8". 817*e1fe3e4aSElliott Hughes The 'glyphObject' argument can be any kind of object (even None); 818*e1fe3e4aSElliott Hughes the writeGlyphToString() method will attempt to get the following 819*e1fe3e4aSElliott Hughes attributes from it: 820*e1fe3e4aSElliott Hughes 821*e1fe3e4aSElliott Hughes width 822*e1fe3e4aSElliott Hughes the advance width of the glyph 823*e1fe3e4aSElliott Hughes height 824*e1fe3e4aSElliott Hughes the advance height of the glyph 825*e1fe3e4aSElliott Hughes unicodes 826*e1fe3e4aSElliott Hughes a list of unicode values for this glyph 827*e1fe3e4aSElliott Hughes note 828*e1fe3e4aSElliott Hughes a string 829*e1fe3e4aSElliott Hughes lib 830*e1fe3e4aSElliott Hughes a dictionary containing custom data 831*e1fe3e4aSElliott Hughes image 832*e1fe3e4aSElliott Hughes a dictionary containing image data 833*e1fe3e4aSElliott Hughes guidelines 834*e1fe3e4aSElliott Hughes a list of guideline data dictionaries 835*e1fe3e4aSElliott Hughes anchors 836*e1fe3e4aSElliott Hughes a list of anchor data dictionaries 837*e1fe3e4aSElliott Hughes 838*e1fe3e4aSElliott Hughes All attributes are optional: if 'glyphObject' doesn't 839*e1fe3e4aSElliott Hughes have the attribute, it will simply be skipped. 840*e1fe3e4aSElliott Hughes 841*e1fe3e4aSElliott Hughes To write outline data to the .glif file, writeGlyphToString() needs 842*e1fe3e4aSElliott Hughes a function (any callable object actually) that will take one 843*e1fe3e4aSElliott Hughes argument: an object that conforms to the PointPen protocol. 844*e1fe3e4aSElliott Hughes The function will be called by writeGlyphToString(); it has to call the 845*e1fe3e4aSElliott Hughes proper PointPen methods to transfer the outline to the .glif file. 846*e1fe3e4aSElliott Hughes 847*e1fe3e4aSElliott Hughes The GLIF format version can be specified with the formatVersion argument. 848*e1fe3e4aSElliott Hughes This accepts either a tuple of integers for (major, minor), or a single 849*e1fe3e4aSElliott Hughes integer for the major digit only (with minor digit implied as 0). 850*e1fe3e4aSElliott Hughes By default when formatVesion is None the latest GLIF format version will 851*e1fe3e4aSElliott Hughes be used; currently it's 2.0, which is equivalent to formatVersion=(2, 0). 852*e1fe3e4aSElliott Hughes 853*e1fe3e4aSElliott Hughes An UnsupportedGLIFFormat exception is raised if the requested UFO 854*e1fe3e4aSElliott Hughes formatVersion is not supported. 855*e1fe3e4aSElliott Hughes 856*e1fe3e4aSElliott Hughes ``validate`` will validate the written data. It is set to ``True`` by default. 857*e1fe3e4aSElliott Hughes """ 858*e1fe3e4aSElliott Hughes data = _writeGlyphToBytes( 859*e1fe3e4aSElliott Hughes glyphName, 860*e1fe3e4aSElliott Hughes glyphObject=glyphObject, 861*e1fe3e4aSElliott Hughes drawPointsFunc=drawPointsFunc, 862*e1fe3e4aSElliott Hughes formatVersion=formatVersion, 863*e1fe3e4aSElliott Hughes validate=validate, 864*e1fe3e4aSElliott Hughes ) 865*e1fe3e4aSElliott Hughes return data.decode("utf-8") 866*e1fe3e4aSElliott Hughes 867*e1fe3e4aSElliott Hughes 868*e1fe3e4aSElliott Hughesdef _writeAdvance(glyphObject, element, validate): 869*e1fe3e4aSElliott Hughes width = getattr(glyphObject, "width", None) 870*e1fe3e4aSElliott Hughes if width is not None: 871*e1fe3e4aSElliott Hughes if validate and not isinstance(width, numberTypes): 872*e1fe3e4aSElliott Hughes raise GlifLibError("width attribute must be int or float") 873*e1fe3e4aSElliott Hughes if width == 0: 874*e1fe3e4aSElliott Hughes width = None 875*e1fe3e4aSElliott Hughes height = getattr(glyphObject, "height", None) 876*e1fe3e4aSElliott Hughes if height is not None: 877*e1fe3e4aSElliott Hughes if validate and not isinstance(height, numberTypes): 878*e1fe3e4aSElliott Hughes raise GlifLibError("height attribute must be int or float") 879*e1fe3e4aSElliott Hughes if height == 0: 880*e1fe3e4aSElliott Hughes height = None 881*e1fe3e4aSElliott Hughes if width is not None and height is not None: 882*e1fe3e4aSElliott Hughes etree.SubElement( 883*e1fe3e4aSElliott Hughes element, 884*e1fe3e4aSElliott Hughes "advance", 885*e1fe3e4aSElliott Hughes OrderedDict([("height", repr(height)), ("width", repr(width))]), 886*e1fe3e4aSElliott Hughes ) 887*e1fe3e4aSElliott Hughes elif width is not None: 888*e1fe3e4aSElliott Hughes etree.SubElement(element, "advance", dict(width=repr(width))) 889*e1fe3e4aSElliott Hughes elif height is not None: 890*e1fe3e4aSElliott Hughes etree.SubElement(element, "advance", dict(height=repr(height))) 891*e1fe3e4aSElliott Hughes 892*e1fe3e4aSElliott Hughes 893*e1fe3e4aSElliott Hughesdef _writeUnicodes(glyphObject, element, validate): 894*e1fe3e4aSElliott Hughes unicodes = getattr(glyphObject, "unicodes", None) 895*e1fe3e4aSElliott Hughes if validate and isinstance(unicodes, int): 896*e1fe3e4aSElliott Hughes unicodes = [unicodes] 897*e1fe3e4aSElliott Hughes seen = set() 898*e1fe3e4aSElliott Hughes for code in unicodes: 899*e1fe3e4aSElliott Hughes if validate and not isinstance(code, int): 900*e1fe3e4aSElliott Hughes raise GlifLibError("unicode values must be int") 901*e1fe3e4aSElliott Hughes if code in seen: 902*e1fe3e4aSElliott Hughes continue 903*e1fe3e4aSElliott Hughes seen.add(code) 904*e1fe3e4aSElliott Hughes hexCode = "%04X" % code 905*e1fe3e4aSElliott Hughes etree.SubElement(element, "unicode", dict(hex=hexCode)) 906*e1fe3e4aSElliott Hughes 907*e1fe3e4aSElliott Hughes 908*e1fe3e4aSElliott Hughesdef _writeNote(glyphObject, element, validate): 909*e1fe3e4aSElliott Hughes note = getattr(glyphObject, "note", None) 910*e1fe3e4aSElliott Hughes if validate and not isinstance(note, str): 911*e1fe3e4aSElliott Hughes raise GlifLibError("note attribute must be str") 912*e1fe3e4aSElliott Hughes note = note.strip() 913*e1fe3e4aSElliott Hughes note = "\n" + note + "\n" 914*e1fe3e4aSElliott Hughes etree.SubElement(element, "note").text = note 915*e1fe3e4aSElliott Hughes 916*e1fe3e4aSElliott Hughes 917*e1fe3e4aSElliott Hughesdef _writeImage(glyphObject, element, validate): 918*e1fe3e4aSElliott Hughes image = getattr(glyphObject, "image", None) 919*e1fe3e4aSElliott Hughes if validate and not imageValidator(image): 920*e1fe3e4aSElliott Hughes raise GlifLibError( 921*e1fe3e4aSElliott Hughes "image attribute must be a dict or dict-like object with the proper structure." 922*e1fe3e4aSElliott Hughes ) 923*e1fe3e4aSElliott Hughes attrs = OrderedDict([("fileName", image["fileName"])]) 924*e1fe3e4aSElliott Hughes for attr, default in _transformationInfo: 925*e1fe3e4aSElliott Hughes value = image.get(attr, default) 926*e1fe3e4aSElliott Hughes if value != default: 927*e1fe3e4aSElliott Hughes attrs[attr] = repr(value) 928*e1fe3e4aSElliott Hughes color = image.get("color") 929*e1fe3e4aSElliott Hughes if color is not None: 930*e1fe3e4aSElliott Hughes attrs["color"] = color 931*e1fe3e4aSElliott Hughes etree.SubElement(element, "image", attrs) 932*e1fe3e4aSElliott Hughes 933*e1fe3e4aSElliott Hughes 934*e1fe3e4aSElliott Hughesdef _writeGuidelines(glyphObject, element, identifiers, validate): 935*e1fe3e4aSElliott Hughes guidelines = getattr(glyphObject, "guidelines", []) 936*e1fe3e4aSElliott Hughes if validate and not guidelinesValidator(guidelines): 937*e1fe3e4aSElliott Hughes raise GlifLibError("guidelines attribute does not have the proper structure.") 938*e1fe3e4aSElliott Hughes for guideline in guidelines: 939*e1fe3e4aSElliott Hughes attrs = OrderedDict() 940*e1fe3e4aSElliott Hughes x = guideline.get("x") 941*e1fe3e4aSElliott Hughes if x is not None: 942*e1fe3e4aSElliott Hughes attrs["x"] = repr(x) 943*e1fe3e4aSElliott Hughes y = guideline.get("y") 944*e1fe3e4aSElliott Hughes if y is not None: 945*e1fe3e4aSElliott Hughes attrs["y"] = repr(y) 946*e1fe3e4aSElliott Hughes angle = guideline.get("angle") 947*e1fe3e4aSElliott Hughes if angle is not None: 948*e1fe3e4aSElliott Hughes attrs["angle"] = repr(angle) 949*e1fe3e4aSElliott Hughes name = guideline.get("name") 950*e1fe3e4aSElliott Hughes if name is not None: 951*e1fe3e4aSElliott Hughes attrs["name"] = name 952*e1fe3e4aSElliott Hughes color = guideline.get("color") 953*e1fe3e4aSElliott Hughes if color is not None: 954*e1fe3e4aSElliott Hughes attrs["color"] = color 955*e1fe3e4aSElliott Hughes identifier = guideline.get("identifier") 956*e1fe3e4aSElliott Hughes if identifier is not None: 957*e1fe3e4aSElliott Hughes if validate and identifier in identifiers: 958*e1fe3e4aSElliott Hughes raise GlifLibError("identifier used more than once: %s" % identifier) 959*e1fe3e4aSElliott Hughes attrs["identifier"] = identifier 960*e1fe3e4aSElliott Hughes identifiers.add(identifier) 961*e1fe3e4aSElliott Hughes etree.SubElement(element, "guideline", attrs) 962*e1fe3e4aSElliott Hughes 963*e1fe3e4aSElliott Hughes 964*e1fe3e4aSElliott Hughesdef _writeAnchorsFormat1(pen, anchors, validate): 965*e1fe3e4aSElliott Hughes if validate and not anchorsValidator(anchors): 966*e1fe3e4aSElliott Hughes raise GlifLibError("anchors attribute does not have the proper structure.") 967*e1fe3e4aSElliott Hughes for anchor in anchors: 968*e1fe3e4aSElliott Hughes attrs = {} 969*e1fe3e4aSElliott Hughes x = anchor["x"] 970*e1fe3e4aSElliott Hughes attrs["x"] = repr(x) 971*e1fe3e4aSElliott Hughes y = anchor["y"] 972*e1fe3e4aSElliott Hughes attrs["y"] = repr(y) 973*e1fe3e4aSElliott Hughes name = anchor.get("name") 974*e1fe3e4aSElliott Hughes if name is not None: 975*e1fe3e4aSElliott Hughes attrs["name"] = name 976*e1fe3e4aSElliott Hughes pen.beginPath() 977*e1fe3e4aSElliott Hughes pen.addPoint((x, y), segmentType="move", name=name) 978*e1fe3e4aSElliott Hughes pen.endPath() 979*e1fe3e4aSElliott Hughes 980*e1fe3e4aSElliott Hughes 981*e1fe3e4aSElliott Hughesdef _writeAnchors(glyphObject, element, identifiers, validate): 982*e1fe3e4aSElliott Hughes anchors = getattr(glyphObject, "anchors", []) 983*e1fe3e4aSElliott Hughes if validate and not anchorsValidator(anchors): 984*e1fe3e4aSElliott Hughes raise GlifLibError("anchors attribute does not have the proper structure.") 985*e1fe3e4aSElliott Hughes for anchor in anchors: 986*e1fe3e4aSElliott Hughes attrs = OrderedDict() 987*e1fe3e4aSElliott Hughes x = anchor["x"] 988*e1fe3e4aSElliott Hughes attrs["x"] = repr(x) 989*e1fe3e4aSElliott Hughes y = anchor["y"] 990*e1fe3e4aSElliott Hughes attrs["y"] = repr(y) 991*e1fe3e4aSElliott Hughes name = anchor.get("name") 992*e1fe3e4aSElliott Hughes if name is not None: 993*e1fe3e4aSElliott Hughes attrs["name"] = name 994*e1fe3e4aSElliott Hughes color = anchor.get("color") 995*e1fe3e4aSElliott Hughes if color is not None: 996*e1fe3e4aSElliott Hughes attrs["color"] = color 997*e1fe3e4aSElliott Hughes identifier = anchor.get("identifier") 998*e1fe3e4aSElliott Hughes if identifier is not None: 999*e1fe3e4aSElliott Hughes if validate and identifier in identifiers: 1000*e1fe3e4aSElliott Hughes raise GlifLibError("identifier used more than once: %s" % identifier) 1001*e1fe3e4aSElliott Hughes attrs["identifier"] = identifier 1002*e1fe3e4aSElliott Hughes identifiers.add(identifier) 1003*e1fe3e4aSElliott Hughes etree.SubElement(element, "anchor", attrs) 1004*e1fe3e4aSElliott Hughes 1005*e1fe3e4aSElliott Hughes 1006*e1fe3e4aSElliott Hughesdef _writeLib(glyphObject, element, validate): 1007*e1fe3e4aSElliott Hughes lib = getattr(glyphObject, "lib", None) 1008*e1fe3e4aSElliott Hughes if not lib: 1009*e1fe3e4aSElliott Hughes # don't write empty lib 1010*e1fe3e4aSElliott Hughes return 1011*e1fe3e4aSElliott Hughes if validate: 1012*e1fe3e4aSElliott Hughes valid, message = glyphLibValidator(lib) 1013*e1fe3e4aSElliott Hughes if not valid: 1014*e1fe3e4aSElliott Hughes raise GlifLibError(message) 1015*e1fe3e4aSElliott Hughes if not isinstance(lib, dict): 1016*e1fe3e4aSElliott Hughes lib = dict(lib) 1017*e1fe3e4aSElliott Hughes # plist inside GLIF begins with 2 levels of indentation 1018*e1fe3e4aSElliott Hughes e = plistlib.totree(lib, indent_level=2) 1019*e1fe3e4aSElliott Hughes etree.SubElement(element, "lib").append(e) 1020*e1fe3e4aSElliott Hughes 1021*e1fe3e4aSElliott Hughes 1022*e1fe3e4aSElliott Hughes# ----------------------- 1023*e1fe3e4aSElliott Hughes# layerinfo.plist Support 1024*e1fe3e4aSElliott Hughes# ----------------------- 1025*e1fe3e4aSElliott Hughes 1026*e1fe3e4aSElliott HugheslayerInfoVersion3ValueData = { 1027*e1fe3e4aSElliott Hughes "color": dict(type=str, valueValidator=colorValidator), 1028*e1fe3e4aSElliott Hughes "lib": dict(type=dict, valueValidator=genericTypeValidator), 1029*e1fe3e4aSElliott Hughes} 1030*e1fe3e4aSElliott Hughes 1031*e1fe3e4aSElliott Hughes 1032*e1fe3e4aSElliott Hughesdef validateLayerInfoVersion3ValueForAttribute(attr, value): 1033*e1fe3e4aSElliott Hughes """ 1034*e1fe3e4aSElliott Hughes This performs very basic validation of the value for attribute 1035*e1fe3e4aSElliott Hughes following the UFO 3 fontinfo.plist specification. The results 1036*e1fe3e4aSElliott Hughes of this should not be interpretted as *correct* for the font 1037*e1fe3e4aSElliott Hughes that they are part of. This merely indicates that the value 1038*e1fe3e4aSElliott Hughes is of the proper type and, where the specification defines 1039*e1fe3e4aSElliott Hughes a set range of possible values for an attribute, that the 1040*e1fe3e4aSElliott Hughes value is in the accepted range. 1041*e1fe3e4aSElliott Hughes """ 1042*e1fe3e4aSElliott Hughes if attr not in layerInfoVersion3ValueData: 1043*e1fe3e4aSElliott Hughes return False 1044*e1fe3e4aSElliott Hughes dataValidationDict = layerInfoVersion3ValueData[attr] 1045*e1fe3e4aSElliott Hughes valueType = dataValidationDict.get("type") 1046*e1fe3e4aSElliott Hughes validator = dataValidationDict.get("valueValidator") 1047*e1fe3e4aSElliott Hughes valueOptions = dataValidationDict.get("valueOptions") 1048*e1fe3e4aSElliott Hughes # have specific options for the validator 1049*e1fe3e4aSElliott Hughes if valueOptions is not None: 1050*e1fe3e4aSElliott Hughes isValidValue = validator(value, valueOptions) 1051*e1fe3e4aSElliott Hughes # no specific options 1052*e1fe3e4aSElliott Hughes else: 1053*e1fe3e4aSElliott Hughes if validator == genericTypeValidator: 1054*e1fe3e4aSElliott Hughes isValidValue = validator(value, valueType) 1055*e1fe3e4aSElliott Hughes else: 1056*e1fe3e4aSElliott Hughes isValidValue = validator(value) 1057*e1fe3e4aSElliott Hughes return isValidValue 1058*e1fe3e4aSElliott Hughes 1059*e1fe3e4aSElliott Hughes 1060*e1fe3e4aSElliott Hughesdef validateLayerInfoVersion3Data(infoData): 1061*e1fe3e4aSElliott Hughes """ 1062*e1fe3e4aSElliott Hughes This performs very basic validation of the value for infoData 1063*e1fe3e4aSElliott Hughes following the UFO 3 layerinfo.plist specification. The results 1064*e1fe3e4aSElliott Hughes of this should not be interpretted as *correct* for the font 1065*e1fe3e4aSElliott Hughes that they are part of. This merely indicates that the values 1066*e1fe3e4aSElliott Hughes are of the proper type and, where the specification defines 1067*e1fe3e4aSElliott Hughes a set range of possible values for an attribute, that the 1068*e1fe3e4aSElliott Hughes value is in the accepted range. 1069*e1fe3e4aSElliott Hughes """ 1070*e1fe3e4aSElliott Hughes for attr, value in infoData.items(): 1071*e1fe3e4aSElliott Hughes if attr not in layerInfoVersion3ValueData: 1072*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown attribute %s." % attr) 1073*e1fe3e4aSElliott Hughes isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value) 1074*e1fe3e4aSElliott Hughes if not isValidValue: 1075*e1fe3e4aSElliott Hughes raise GlifLibError(f"Invalid value for attribute {attr} ({value!r}).") 1076*e1fe3e4aSElliott Hughes return infoData 1077*e1fe3e4aSElliott Hughes 1078*e1fe3e4aSElliott Hughes 1079*e1fe3e4aSElliott Hughes# ----------------- 1080*e1fe3e4aSElliott Hughes# GLIF Tree Support 1081*e1fe3e4aSElliott Hughes# ----------------- 1082*e1fe3e4aSElliott Hughes 1083*e1fe3e4aSElliott Hughes 1084*e1fe3e4aSElliott Hughesdef _glifTreeFromFile(aFile): 1085*e1fe3e4aSElliott Hughes if etree._have_lxml: 1086*e1fe3e4aSElliott Hughes tree = etree.parse(aFile, parser=etree.XMLParser(remove_comments=True)) 1087*e1fe3e4aSElliott Hughes else: 1088*e1fe3e4aSElliott Hughes tree = etree.parse(aFile) 1089*e1fe3e4aSElliott Hughes root = tree.getroot() 1090*e1fe3e4aSElliott Hughes if root.tag != "glyph": 1091*e1fe3e4aSElliott Hughes raise GlifLibError("The GLIF is not properly formatted.") 1092*e1fe3e4aSElliott Hughes if root.text and root.text.strip() != "": 1093*e1fe3e4aSElliott Hughes raise GlifLibError("Invalid GLIF structure.") 1094*e1fe3e4aSElliott Hughes return root 1095*e1fe3e4aSElliott Hughes 1096*e1fe3e4aSElliott Hughes 1097*e1fe3e4aSElliott Hughesdef _glifTreeFromString(aString): 1098*e1fe3e4aSElliott Hughes data = tobytes(aString, encoding="utf-8") 1099*e1fe3e4aSElliott Hughes try: 1100*e1fe3e4aSElliott Hughes if etree._have_lxml: 1101*e1fe3e4aSElliott Hughes root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True)) 1102*e1fe3e4aSElliott Hughes else: 1103*e1fe3e4aSElliott Hughes root = etree.fromstring(data) 1104*e1fe3e4aSElliott Hughes except Exception as etree_exception: 1105*e1fe3e4aSElliott Hughes raise GlifLibError("GLIF contains invalid XML.") from etree_exception 1106*e1fe3e4aSElliott Hughes 1107*e1fe3e4aSElliott Hughes if root.tag != "glyph": 1108*e1fe3e4aSElliott Hughes raise GlifLibError("The GLIF is not properly formatted.") 1109*e1fe3e4aSElliott Hughes if root.text and root.text.strip() != "": 1110*e1fe3e4aSElliott Hughes raise GlifLibError("Invalid GLIF structure.") 1111*e1fe3e4aSElliott Hughes return root 1112*e1fe3e4aSElliott Hughes 1113*e1fe3e4aSElliott Hughes 1114*e1fe3e4aSElliott Hughesdef _readGlyphFromTree( 1115*e1fe3e4aSElliott Hughes tree, 1116*e1fe3e4aSElliott Hughes glyphObject=None, 1117*e1fe3e4aSElliott Hughes pointPen=None, 1118*e1fe3e4aSElliott Hughes formatVersions=GLIFFormatVersion.supported_versions(), 1119*e1fe3e4aSElliott Hughes validate=True, 1120*e1fe3e4aSElliott Hughes): 1121*e1fe3e4aSElliott Hughes # check the format version 1122*e1fe3e4aSElliott Hughes formatVersionMajor = tree.get("format") 1123*e1fe3e4aSElliott Hughes if validate and formatVersionMajor is None: 1124*e1fe3e4aSElliott Hughes raise GlifLibError("Unspecified format version in GLIF.") 1125*e1fe3e4aSElliott Hughes formatVersionMinor = tree.get("formatMinor", 0) 1126*e1fe3e4aSElliott Hughes try: 1127*e1fe3e4aSElliott Hughes formatVersion = GLIFFormatVersion( 1128*e1fe3e4aSElliott Hughes (int(formatVersionMajor), int(formatVersionMinor)) 1129*e1fe3e4aSElliott Hughes ) 1130*e1fe3e4aSElliott Hughes except ValueError as e: 1131*e1fe3e4aSElliott Hughes msg = "Unsupported GLIF format: %s.%s" % ( 1132*e1fe3e4aSElliott Hughes formatVersionMajor, 1133*e1fe3e4aSElliott Hughes formatVersionMinor, 1134*e1fe3e4aSElliott Hughes ) 1135*e1fe3e4aSElliott Hughes if validate: 1136*e1fe3e4aSElliott Hughes from fontTools.ufoLib.errors import UnsupportedGLIFFormat 1137*e1fe3e4aSElliott Hughes 1138*e1fe3e4aSElliott Hughes raise UnsupportedGLIFFormat(msg) from e 1139*e1fe3e4aSElliott Hughes # warn but continue using the latest supported format 1140*e1fe3e4aSElliott Hughes formatVersion = GLIFFormatVersion.default() 1141*e1fe3e4aSElliott Hughes logger.warning( 1142*e1fe3e4aSElliott Hughes "%s. Assuming the latest supported version (%s). " 1143*e1fe3e4aSElliott Hughes "Some data may be skipped or parsed incorrectly.", 1144*e1fe3e4aSElliott Hughes msg, 1145*e1fe3e4aSElliott Hughes formatVersion, 1146*e1fe3e4aSElliott Hughes ) 1147*e1fe3e4aSElliott Hughes 1148*e1fe3e4aSElliott Hughes if validate and formatVersion not in formatVersions: 1149*e1fe3e4aSElliott Hughes raise GlifLibError(f"Forbidden GLIF format version: {formatVersion!s}") 1150*e1fe3e4aSElliott Hughes 1151*e1fe3e4aSElliott Hughes try: 1152*e1fe3e4aSElliott Hughes readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[formatVersion] 1153*e1fe3e4aSElliott Hughes except KeyError: 1154*e1fe3e4aSElliott Hughes raise NotImplementedError(formatVersion) 1155*e1fe3e4aSElliott Hughes 1156*e1fe3e4aSElliott Hughes readGlyphFromTree( 1157*e1fe3e4aSElliott Hughes tree=tree, 1158*e1fe3e4aSElliott Hughes glyphObject=glyphObject, 1159*e1fe3e4aSElliott Hughes pointPen=pointPen, 1160*e1fe3e4aSElliott Hughes validate=validate, 1161*e1fe3e4aSElliott Hughes formatMinor=formatVersion.minor, 1162*e1fe3e4aSElliott Hughes ) 1163*e1fe3e4aSElliott Hughes 1164*e1fe3e4aSElliott Hughes 1165*e1fe3e4aSElliott Hughesdef _readGlyphFromTreeFormat1( 1166*e1fe3e4aSElliott Hughes tree, glyphObject=None, pointPen=None, validate=None, **kwargs 1167*e1fe3e4aSElliott Hughes): 1168*e1fe3e4aSElliott Hughes # get the name 1169*e1fe3e4aSElliott Hughes _readName(glyphObject, tree, validate) 1170*e1fe3e4aSElliott Hughes # populate the sub elements 1171*e1fe3e4aSElliott Hughes unicodes = [] 1172*e1fe3e4aSElliott Hughes haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False 1173*e1fe3e4aSElliott Hughes for element in tree: 1174*e1fe3e4aSElliott Hughes if element.tag == "outline": 1175*e1fe3e4aSElliott Hughes if validate: 1176*e1fe3e4aSElliott Hughes if haveSeenOutline: 1177*e1fe3e4aSElliott Hughes raise GlifLibError("The outline element occurs more than once.") 1178*e1fe3e4aSElliott Hughes if element.attrib: 1179*e1fe3e4aSElliott Hughes raise GlifLibError( 1180*e1fe3e4aSElliott Hughes "The outline element contains unknown attributes." 1181*e1fe3e4aSElliott Hughes ) 1182*e1fe3e4aSElliott Hughes if element.text and element.text.strip() != "": 1183*e1fe3e4aSElliott Hughes raise GlifLibError("Invalid outline structure.") 1184*e1fe3e4aSElliott Hughes haveSeenOutline = True 1185*e1fe3e4aSElliott Hughes buildOutlineFormat1(glyphObject, pointPen, element, validate) 1186*e1fe3e4aSElliott Hughes elif glyphObject is None: 1187*e1fe3e4aSElliott Hughes continue 1188*e1fe3e4aSElliott Hughes elif element.tag == "advance": 1189*e1fe3e4aSElliott Hughes if validate and haveSeenAdvance: 1190*e1fe3e4aSElliott Hughes raise GlifLibError("The advance element occurs more than once.") 1191*e1fe3e4aSElliott Hughes haveSeenAdvance = True 1192*e1fe3e4aSElliott Hughes _readAdvance(glyphObject, element) 1193*e1fe3e4aSElliott Hughes elif element.tag == "unicode": 1194*e1fe3e4aSElliott Hughes try: 1195*e1fe3e4aSElliott Hughes v = element.get("hex") 1196*e1fe3e4aSElliott Hughes v = int(v, 16) 1197*e1fe3e4aSElliott Hughes if v not in unicodes: 1198*e1fe3e4aSElliott Hughes unicodes.append(v) 1199*e1fe3e4aSElliott Hughes except ValueError: 1200*e1fe3e4aSElliott Hughes raise GlifLibError( 1201*e1fe3e4aSElliott Hughes "Illegal value for hex attribute of unicode element." 1202*e1fe3e4aSElliott Hughes ) 1203*e1fe3e4aSElliott Hughes elif element.tag == "note": 1204*e1fe3e4aSElliott Hughes if validate and haveSeenNote: 1205*e1fe3e4aSElliott Hughes raise GlifLibError("The note element occurs more than once.") 1206*e1fe3e4aSElliott Hughes haveSeenNote = True 1207*e1fe3e4aSElliott Hughes _readNote(glyphObject, element) 1208*e1fe3e4aSElliott Hughes elif element.tag == "lib": 1209*e1fe3e4aSElliott Hughes if validate and haveSeenLib: 1210*e1fe3e4aSElliott Hughes raise GlifLibError("The lib element occurs more than once.") 1211*e1fe3e4aSElliott Hughes haveSeenLib = True 1212*e1fe3e4aSElliott Hughes _readLib(glyphObject, element, validate) 1213*e1fe3e4aSElliott Hughes else: 1214*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown element in GLIF: %s" % element) 1215*e1fe3e4aSElliott Hughes # set the collected unicodes 1216*e1fe3e4aSElliott Hughes if unicodes: 1217*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "unicodes", unicodes) 1218*e1fe3e4aSElliott Hughes 1219*e1fe3e4aSElliott Hughes 1220*e1fe3e4aSElliott Hughesdef _readGlyphFromTreeFormat2( 1221*e1fe3e4aSElliott Hughes tree, glyphObject=None, pointPen=None, validate=None, formatMinor=0 1222*e1fe3e4aSElliott Hughes): 1223*e1fe3e4aSElliott Hughes # get the name 1224*e1fe3e4aSElliott Hughes _readName(glyphObject, tree, validate) 1225*e1fe3e4aSElliott Hughes # populate the sub elements 1226*e1fe3e4aSElliott Hughes unicodes = [] 1227*e1fe3e4aSElliott Hughes guidelines = [] 1228*e1fe3e4aSElliott Hughes anchors = [] 1229*e1fe3e4aSElliott Hughes haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = ( 1230*e1fe3e4aSElliott Hughes False 1231*e1fe3e4aSElliott Hughes ) 1232*e1fe3e4aSElliott Hughes identifiers = set() 1233*e1fe3e4aSElliott Hughes for element in tree: 1234*e1fe3e4aSElliott Hughes if element.tag == "outline": 1235*e1fe3e4aSElliott Hughes if validate: 1236*e1fe3e4aSElliott Hughes if haveSeenOutline: 1237*e1fe3e4aSElliott Hughes raise GlifLibError("The outline element occurs more than once.") 1238*e1fe3e4aSElliott Hughes if element.attrib: 1239*e1fe3e4aSElliott Hughes raise GlifLibError( 1240*e1fe3e4aSElliott Hughes "The outline element contains unknown attributes." 1241*e1fe3e4aSElliott Hughes ) 1242*e1fe3e4aSElliott Hughes if element.text and element.text.strip() != "": 1243*e1fe3e4aSElliott Hughes raise GlifLibError("Invalid outline structure.") 1244*e1fe3e4aSElliott Hughes haveSeenOutline = True 1245*e1fe3e4aSElliott Hughes if pointPen is not None: 1246*e1fe3e4aSElliott Hughes buildOutlineFormat2( 1247*e1fe3e4aSElliott Hughes glyphObject, pointPen, element, identifiers, validate 1248*e1fe3e4aSElliott Hughes ) 1249*e1fe3e4aSElliott Hughes elif glyphObject is None: 1250*e1fe3e4aSElliott Hughes continue 1251*e1fe3e4aSElliott Hughes elif element.tag == "advance": 1252*e1fe3e4aSElliott Hughes if validate and haveSeenAdvance: 1253*e1fe3e4aSElliott Hughes raise GlifLibError("The advance element occurs more than once.") 1254*e1fe3e4aSElliott Hughes haveSeenAdvance = True 1255*e1fe3e4aSElliott Hughes _readAdvance(glyphObject, element) 1256*e1fe3e4aSElliott Hughes elif element.tag == "unicode": 1257*e1fe3e4aSElliott Hughes try: 1258*e1fe3e4aSElliott Hughes v = element.get("hex") 1259*e1fe3e4aSElliott Hughes v = int(v, 16) 1260*e1fe3e4aSElliott Hughes if v not in unicodes: 1261*e1fe3e4aSElliott Hughes unicodes.append(v) 1262*e1fe3e4aSElliott Hughes except ValueError: 1263*e1fe3e4aSElliott Hughes raise GlifLibError( 1264*e1fe3e4aSElliott Hughes "Illegal value for hex attribute of unicode element." 1265*e1fe3e4aSElliott Hughes ) 1266*e1fe3e4aSElliott Hughes elif element.tag == "guideline": 1267*e1fe3e4aSElliott Hughes if validate and len(element): 1268*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown children in guideline element.") 1269*e1fe3e4aSElliott Hughes attrib = dict(element.attrib) 1270*e1fe3e4aSElliott Hughes for attr in ("x", "y", "angle"): 1271*e1fe3e4aSElliott Hughes if attr in attrib: 1272*e1fe3e4aSElliott Hughes attrib[attr] = _number(attrib[attr]) 1273*e1fe3e4aSElliott Hughes guidelines.append(attrib) 1274*e1fe3e4aSElliott Hughes elif element.tag == "anchor": 1275*e1fe3e4aSElliott Hughes if validate and len(element): 1276*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown children in anchor element.") 1277*e1fe3e4aSElliott Hughes attrib = dict(element.attrib) 1278*e1fe3e4aSElliott Hughes for attr in ("x", "y"): 1279*e1fe3e4aSElliott Hughes if attr in element.attrib: 1280*e1fe3e4aSElliott Hughes attrib[attr] = _number(attrib[attr]) 1281*e1fe3e4aSElliott Hughes anchors.append(attrib) 1282*e1fe3e4aSElliott Hughes elif element.tag == "image": 1283*e1fe3e4aSElliott Hughes if validate: 1284*e1fe3e4aSElliott Hughes if haveSeenImage: 1285*e1fe3e4aSElliott Hughes raise GlifLibError("The image element occurs more than once.") 1286*e1fe3e4aSElliott Hughes if len(element): 1287*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown children in image element.") 1288*e1fe3e4aSElliott Hughes haveSeenImage = True 1289*e1fe3e4aSElliott Hughes _readImage(glyphObject, element, validate) 1290*e1fe3e4aSElliott Hughes elif element.tag == "note": 1291*e1fe3e4aSElliott Hughes if validate and haveSeenNote: 1292*e1fe3e4aSElliott Hughes raise GlifLibError("The note element occurs more than once.") 1293*e1fe3e4aSElliott Hughes haveSeenNote = True 1294*e1fe3e4aSElliott Hughes _readNote(glyphObject, element) 1295*e1fe3e4aSElliott Hughes elif element.tag == "lib": 1296*e1fe3e4aSElliott Hughes if validate and haveSeenLib: 1297*e1fe3e4aSElliott Hughes raise GlifLibError("The lib element occurs more than once.") 1298*e1fe3e4aSElliott Hughes haveSeenLib = True 1299*e1fe3e4aSElliott Hughes _readLib(glyphObject, element, validate) 1300*e1fe3e4aSElliott Hughes else: 1301*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown element in GLIF: %s" % element) 1302*e1fe3e4aSElliott Hughes # set the collected unicodes 1303*e1fe3e4aSElliott Hughes if unicodes: 1304*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "unicodes", unicodes) 1305*e1fe3e4aSElliott Hughes # set the collected guidelines 1306*e1fe3e4aSElliott Hughes if guidelines: 1307*e1fe3e4aSElliott Hughes if validate and not guidelinesValidator(guidelines, identifiers): 1308*e1fe3e4aSElliott Hughes raise GlifLibError("The guidelines are improperly formatted.") 1309*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "guidelines", guidelines) 1310*e1fe3e4aSElliott Hughes # set the collected anchors 1311*e1fe3e4aSElliott Hughes if anchors: 1312*e1fe3e4aSElliott Hughes if validate and not anchorsValidator(anchors, identifiers): 1313*e1fe3e4aSElliott Hughes raise GlifLibError("The anchors are improperly formatted.") 1314*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "anchors", anchors) 1315*e1fe3e4aSElliott Hughes 1316*e1fe3e4aSElliott Hughes 1317*e1fe3e4aSElliott Hughes_READ_GLYPH_FROM_TREE_FUNCS = { 1318*e1fe3e4aSElliott Hughes GLIFFormatVersion.FORMAT_1_0: _readGlyphFromTreeFormat1, 1319*e1fe3e4aSElliott Hughes GLIFFormatVersion.FORMAT_2_0: _readGlyphFromTreeFormat2, 1320*e1fe3e4aSElliott Hughes} 1321*e1fe3e4aSElliott Hughes 1322*e1fe3e4aSElliott Hughes 1323*e1fe3e4aSElliott Hughesdef _readName(glyphObject, root, validate): 1324*e1fe3e4aSElliott Hughes glyphName = root.get("name") 1325*e1fe3e4aSElliott Hughes if validate and not glyphName: 1326*e1fe3e4aSElliott Hughes raise GlifLibError("Empty glyph name in GLIF.") 1327*e1fe3e4aSElliott Hughes if glyphName and glyphObject is not None: 1328*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "name", glyphName) 1329*e1fe3e4aSElliott Hughes 1330*e1fe3e4aSElliott Hughes 1331*e1fe3e4aSElliott Hughesdef _readAdvance(glyphObject, advance): 1332*e1fe3e4aSElliott Hughes width = _number(advance.get("width", 0)) 1333*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "width", width) 1334*e1fe3e4aSElliott Hughes height = _number(advance.get("height", 0)) 1335*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "height", height) 1336*e1fe3e4aSElliott Hughes 1337*e1fe3e4aSElliott Hughes 1338*e1fe3e4aSElliott Hughesdef _readNote(glyphObject, note): 1339*e1fe3e4aSElliott Hughes lines = note.text.split("\n") 1340*e1fe3e4aSElliott Hughes note = "\n".join(line.strip() for line in lines if line.strip()) 1341*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "note", note) 1342*e1fe3e4aSElliott Hughes 1343*e1fe3e4aSElliott Hughes 1344*e1fe3e4aSElliott Hughesdef _readLib(glyphObject, lib, validate): 1345*e1fe3e4aSElliott Hughes assert len(lib) == 1 1346*e1fe3e4aSElliott Hughes child = lib[0] 1347*e1fe3e4aSElliott Hughes plist = plistlib.fromtree(child) 1348*e1fe3e4aSElliott Hughes if validate: 1349*e1fe3e4aSElliott Hughes valid, message = glyphLibValidator(plist) 1350*e1fe3e4aSElliott Hughes if not valid: 1351*e1fe3e4aSElliott Hughes raise GlifLibError(message) 1352*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "lib", plist) 1353*e1fe3e4aSElliott Hughes 1354*e1fe3e4aSElliott Hughes 1355*e1fe3e4aSElliott Hughesdef _readImage(glyphObject, image, validate): 1356*e1fe3e4aSElliott Hughes imageData = dict(image.attrib) 1357*e1fe3e4aSElliott Hughes for attr, default in _transformationInfo: 1358*e1fe3e4aSElliott Hughes value = imageData.get(attr, default) 1359*e1fe3e4aSElliott Hughes imageData[attr] = _number(value) 1360*e1fe3e4aSElliott Hughes if validate and not imageValidator(imageData): 1361*e1fe3e4aSElliott Hughes raise GlifLibError("The image element is not properly formatted.") 1362*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "image", imageData) 1363*e1fe3e4aSElliott Hughes 1364*e1fe3e4aSElliott Hughes 1365*e1fe3e4aSElliott Hughes# ---------------- 1366*e1fe3e4aSElliott Hughes# GLIF to PointPen 1367*e1fe3e4aSElliott Hughes# ---------------- 1368*e1fe3e4aSElliott Hughes 1369*e1fe3e4aSElliott HughescontourAttributesFormat2 = {"identifier"} 1370*e1fe3e4aSElliott HughescomponentAttributesFormat1 = { 1371*e1fe3e4aSElliott Hughes "base", 1372*e1fe3e4aSElliott Hughes "xScale", 1373*e1fe3e4aSElliott Hughes "xyScale", 1374*e1fe3e4aSElliott Hughes "yxScale", 1375*e1fe3e4aSElliott Hughes "yScale", 1376*e1fe3e4aSElliott Hughes "xOffset", 1377*e1fe3e4aSElliott Hughes "yOffset", 1378*e1fe3e4aSElliott Hughes} 1379*e1fe3e4aSElliott HughescomponentAttributesFormat2 = componentAttributesFormat1 | {"identifier"} 1380*e1fe3e4aSElliott HughespointAttributesFormat1 = {"x", "y", "type", "smooth", "name"} 1381*e1fe3e4aSElliott HughespointAttributesFormat2 = pointAttributesFormat1 | {"identifier"} 1382*e1fe3e4aSElliott HughespointSmoothOptions = {"no", "yes"} 1383*e1fe3e4aSElliott HughespointTypeOptions = {"move", "line", "offcurve", "curve", "qcurve"} 1384*e1fe3e4aSElliott Hughes 1385*e1fe3e4aSElliott Hughes# format 1 1386*e1fe3e4aSElliott Hughes 1387*e1fe3e4aSElliott Hughes 1388*e1fe3e4aSElliott Hughesdef buildOutlineFormat1(glyphObject, pen, outline, validate): 1389*e1fe3e4aSElliott Hughes anchors = [] 1390*e1fe3e4aSElliott Hughes for element in outline: 1391*e1fe3e4aSElliott Hughes if element.tag == "contour": 1392*e1fe3e4aSElliott Hughes if len(element) == 1: 1393*e1fe3e4aSElliott Hughes point = element[0] 1394*e1fe3e4aSElliott Hughes if point.tag == "point": 1395*e1fe3e4aSElliott Hughes anchor = _buildAnchorFormat1(point, validate) 1396*e1fe3e4aSElliott Hughes if anchor is not None: 1397*e1fe3e4aSElliott Hughes anchors.append(anchor) 1398*e1fe3e4aSElliott Hughes continue 1399*e1fe3e4aSElliott Hughes if pen is not None: 1400*e1fe3e4aSElliott Hughes _buildOutlineContourFormat1(pen, element, validate) 1401*e1fe3e4aSElliott Hughes elif element.tag == "component": 1402*e1fe3e4aSElliott Hughes if pen is not None: 1403*e1fe3e4aSElliott Hughes _buildOutlineComponentFormat1(pen, element, validate) 1404*e1fe3e4aSElliott Hughes else: 1405*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown element in outline element: %s" % element) 1406*e1fe3e4aSElliott Hughes if glyphObject is not None and anchors: 1407*e1fe3e4aSElliott Hughes if validate and not anchorsValidator(anchors): 1408*e1fe3e4aSElliott Hughes raise GlifLibError("GLIF 1 anchors are not properly formatted.") 1409*e1fe3e4aSElliott Hughes _relaxedSetattr(glyphObject, "anchors", anchors) 1410*e1fe3e4aSElliott Hughes 1411*e1fe3e4aSElliott Hughes 1412*e1fe3e4aSElliott Hughesdef _buildAnchorFormat1(point, validate): 1413*e1fe3e4aSElliott Hughes if point.get("type") != "move": 1414*e1fe3e4aSElliott Hughes return None 1415*e1fe3e4aSElliott Hughes name = point.get("name") 1416*e1fe3e4aSElliott Hughes if name is None: 1417*e1fe3e4aSElliott Hughes return None 1418*e1fe3e4aSElliott Hughes x = point.get("x") 1419*e1fe3e4aSElliott Hughes y = point.get("y") 1420*e1fe3e4aSElliott Hughes if validate and x is None: 1421*e1fe3e4aSElliott Hughes raise GlifLibError("Required x attribute is missing in point element.") 1422*e1fe3e4aSElliott Hughes if validate and y is None: 1423*e1fe3e4aSElliott Hughes raise GlifLibError("Required y attribute is missing in point element.") 1424*e1fe3e4aSElliott Hughes x = _number(x) 1425*e1fe3e4aSElliott Hughes y = _number(y) 1426*e1fe3e4aSElliott Hughes anchor = dict(x=x, y=y, name=name) 1427*e1fe3e4aSElliott Hughes return anchor 1428*e1fe3e4aSElliott Hughes 1429*e1fe3e4aSElliott Hughes 1430*e1fe3e4aSElliott Hughesdef _buildOutlineContourFormat1(pen, contour, validate): 1431*e1fe3e4aSElliott Hughes if validate and contour.attrib: 1432*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown attributes in contour element.") 1433*e1fe3e4aSElliott Hughes pen.beginPath() 1434*e1fe3e4aSElliott Hughes if len(contour): 1435*e1fe3e4aSElliott Hughes massaged = _validateAndMassagePointStructures( 1436*e1fe3e4aSElliott Hughes contour, 1437*e1fe3e4aSElliott Hughes pointAttributesFormat1, 1438*e1fe3e4aSElliott Hughes openContourOffCurveLeniency=True, 1439*e1fe3e4aSElliott Hughes validate=validate, 1440*e1fe3e4aSElliott Hughes ) 1441*e1fe3e4aSElliott Hughes _buildOutlinePointsFormat1(pen, massaged) 1442*e1fe3e4aSElliott Hughes pen.endPath() 1443*e1fe3e4aSElliott Hughes 1444*e1fe3e4aSElliott Hughes 1445*e1fe3e4aSElliott Hughesdef _buildOutlinePointsFormat1(pen, contour): 1446*e1fe3e4aSElliott Hughes for point in contour: 1447*e1fe3e4aSElliott Hughes x = point["x"] 1448*e1fe3e4aSElliott Hughes y = point["y"] 1449*e1fe3e4aSElliott Hughes segmentType = point["segmentType"] 1450*e1fe3e4aSElliott Hughes smooth = point["smooth"] 1451*e1fe3e4aSElliott Hughes name = point["name"] 1452*e1fe3e4aSElliott Hughes pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name) 1453*e1fe3e4aSElliott Hughes 1454*e1fe3e4aSElliott Hughes 1455*e1fe3e4aSElliott Hughesdef _buildOutlineComponentFormat1(pen, component, validate): 1456*e1fe3e4aSElliott Hughes if validate: 1457*e1fe3e4aSElliott Hughes if len(component): 1458*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown child elements of component element.") 1459*e1fe3e4aSElliott Hughes for attr in component.attrib.keys(): 1460*e1fe3e4aSElliott Hughes if attr not in componentAttributesFormat1: 1461*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown attribute in component element: %s" % attr) 1462*e1fe3e4aSElliott Hughes baseGlyphName = component.get("base") 1463*e1fe3e4aSElliott Hughes if validate and baseGlyphName is None: 1464*e1fe3e4aSElliott Hughes raise GlifLibError("The base attribute is not defined in the component.") 1465*e1fe3e4aSElliott Hughes transformation = [] 1466*e1fe3e4aSElliott Hughes for attr, default in _transformationInfo: 1467*e1fe3e4aSElliott Hughes value = component.get(attr) 1468*e1fe3e4aSElliott Hughes if value is None: 1469*e1fe3e4aSElliott Hughes value = default 1470*e1fe3e4aSElliott Hughes else: 1471*e1fe3e4aSElliott Hughes value = _number(value) 1472*e1fe3e4aSElliott Hughes transformation.append(value) 1473*e1fe3e4aSElliott Hughes pen.addComponent(baseGlyphName, tuple(transformation)) 1474*e1fe3e4aSElliott Hughes 1475*e1fe3e4aSElliott Hughes 1476*e1fe3e4aSElliott Hughes# format 2 1477*e1fe3e4aSElliott Hughes 1478*e1fe3e4aSElliott Hughes 1479*e1fe3e4aSElliott Hughesdef buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate): 1480*e1fe3e4aSElliott Hughes for element in outline: 1481*e1fe3e4aSElliott Hughes if element.tag == "contour": 1482*e1fe3e4aSElliott Hughes _buildOutlineContourFormat2(pen, element, identifiers, validate) 1483*e1fe3e4aSElliott Hughes elif element.tag == "component": 1484*e1fe3e4aSElliott Hughes _buildOutlineComponentFormat2(pen, element, identifiers, validate) 1485*e1fe3e4aSElliott Hughes else: 1486*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown element in outline element: %s" % element.tag) 1487*e1fe3e4aSElliott Hughes 1488*e1fe3e4aSElliott Hughes 1489*e1fe3e4aSElliott Hughesdef _buildOutlineContourFormat2(pen, contour, identifiers, validate): 1490*e1fe3e4aSElliott Hughes if validate: 1491*e1fe3e4aSElliott Hughes for attr in contour.attrib.keys(): 1492*e1fe3e4aSElliott Hughes if attr not in contourAttributesFormat2: 1493*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown attribute in contour element: %s" % attr) 1494*e1fe3e4aSElliott Hughes identifier = contour.get("identifier") 1495*e1fe3e4aSElliott Hughes if identifier is not None: 1496*e1fe3e4aSElliott Hughes if validate: 1497*e1fe3e4aSElliott Hughes if identifier in identifiers: 1498*e1fe3e4aSElliott Hughes raise GlifLibError( 1499*e1fe3e4aSElliott Hughes "The identifier %s is used more than once." % identifier 1500*e1fe3e4aSElliott Hughes ) 1501*e1fe3e4aSElliott Hughes if not identifierValidator(identifier): 1502*e1fe3e4aSElliott Hughes raise GlifLibError( 1503*e1fe3e4aSElliott Hughes "The contour identifier %s is not valid." % identifier 1504*e1fe3e4aSElliott Hughes ) 1505*e1fe3e4aSElliott Hughes identifiers.add(identifier) 1506*e1fe3e4aSElliott Hughes try: 1507*e1fe3e4aSElliott Hughes pen.beginPath(identifier=identifier) 1508*e1fe3e4aSElliott Hughes except TypeError: 1509*e1fe3e4aSElliott Hughes pen.beginPath() 1510*e1fe3e4aSElliott Hughes warn( 1511*e1fe3e4aSElliott Hughes "The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.", 1512*e1fe3e4aSElliott Hughes DeprecationWarning, 1513*e1fe3e4aSElliott Hughes ) 1514*e1fe3e4aSElliott Hughes if len(contour): 1515*e1fe3e4aSElliott Hughes massaged = _validateAndMassagePointStructures( 1516*e1fe3e4aSElliott Hughes contour, pointAttributesFormat2, validate=validate 1517*e1fe3e4aSElliott Hughes ) 1518*e1fe3e4aSElliott Hughes _buildOutlinePointsFormat2(pen, massaged, identifiers, validate) 1519*e1fe3e4aSElliott Hughes pen.endPath() 1520*e1fe3e4aSElliott Hughes 1521*e1fe3e4aSElliott Hughes 1522*e1fe3e4aSElliott Hughesdef _buildOutlinePointsFormat2(pen, contour, identifiers, validate): 1523*e1fe3e4aSElliott Hughes for point in contour: 1524*e1fe3e4aSElliott Hughes x = point["x"] 1525*e1fe3e4aSElliott Hughes y = point["y"] 1526*e1fe3e4aSElliott Hughes segmentType = point["segmentType"] 1527*e1fe3e4aSElliott Hughes smooth = point["smooth"] 1528*e1fe3e4aSElliott Hughes name = point["name"] 1529*e1fe3e4aSElliott Hughes identifier = point.get("identifier") 1530*e1fe3e4aSElliott Hughes if identifier is not None: 1531*e1fe3e4aSElliott Hughes if validate: 1532*e1fe3e4aSElliott Hughes if identifier in identifiers: 1533*e1fe3e4aSElliott Hughes raise GlifLibError( 1534*e1fe3e4aSElliott Hughes "The identifier %s is used more than once." % identifier 1535*e1fe3e4aSElliott Hughes ) 1536*e1fe3e4aSElliott Hughes if not identifierValidator(identifier): 1537*e1fe3e4aSElliott Hughes raise GlifLibError("The identifier %s is not valid." % identifier) 1538*e1fe3e4aSElliott Hughes identifiers.add(identifier) 1539*e1fe3e4aSElliott Hughes try: 1540*e1fe3e4aSElliott Hughes pen.addPoint( 1541*e1fe3e4aSElliott Hughes (x, y), 1542*e1fe3e4aSElliott Hughes segmentType=segmentType, 1543*e1fe3e4aSElliott Hughes smooth=smooth, 1544*e1fe3e4aSElliott Hughes name=name, 1545*e1fe3e4aSElliott Hughes identifier=identifier, 1546*e1fe3e4aSElliott Hughes ) 1547*e1fe3e4aSElliott Hughes except TypeError: 1548*e1fe3e4aSElliott Hughes pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name) 1549*e1fe3e4aSElliott Hughes warn( 1550*e1fe3e4aSElliott Hughes "The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.", 1551*e1fe3e4aSElliott Hughes DeprecationWarning, 1552*e1fe3e4aSElliott Hughes ) 1553*e1fe3e4aSElliott Hughes 1554*e1fe3e4aSElliott Hughes 1555*e1fe3e4aSElliott Hughesdef _buildOutlineComponentFormat2(pen, component, identifiers, validate): 1556*e1fe3e4aSElliott Hughes if validate: 1557*e1fe3e4aSElliott Hughes if len(component): 1558*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown child elements of component element.") 1559*e1fe3e4aSElliott Hughes for attr in component.attrib.keys(): 1560*e1fe3e4aSElliott Hughes if attr not in componentAttributesFormat2: 1561*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown attribute in component element: %s" % attr) 1562*e1fe3e4aSElliott Hughes baseGlyphName = component.get("base") 1563*e1fe3e4aSElliott Hughes if validate and baseGlyphName is None: 1564*e1fe3e4aSElliott Hughes raise GlifLibError("The base attribute is not defined in the component.") 1565*e1fe3e4aSElliott Hughes transformation = [] 1566*e1fe3e4aSElliott Hughes for attr, default in _transformationInfo: 1567*e1fe3e4aSElliott Hughes value = component.get(attr) 1568*e1fe3e4aSElliott Hughes if value is None: 1569*e1fe3e4aSElliott Hughes value = default 1570*e1fe3e4aSElliott Hughes else: 1571*e1fe3e4aSElliott Hughes value = _number(value) 1572*e1fe3e4aSElliott Hughes transformation.append(value) 1573*e1fe3e4aSElliott Hughes identifier = component.get("identifier") 1574*e1fe3e4aSElliott Hughes if identifier is not None: 1575*e1fe3e4aSElliott Hughes if validate: 1576*e1fe3e4aSElliott Hughes if identifier in identifiers: 1577*e1fe3e4aSElliott Hughes raise GlifLibError( 1578*e1fe3e4aSElliott Hughes "The identifier %s is used more than once." % identifier 1579*e1fe3e4aSElliott Hughes ) 1580*e1fe3e4aSElliott Hughes if validate and not identifierValidator(identifier): 1581*e1fe3e4aSElliott Hughes raise GlifLibError("The identifier %s is not valid." % identifier) 1582*e1fe3e4aSElliott Hughes identifiers.add(identifier) 1583*e1fe3e4aSElliott Hughes try: 1584*e1fe3e4aSElliott Hughes pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier) 1585*e1fe3e4aSElliott Hughes except TypeError: 1586*e1fe3e4aSElliott Hughes pen.addComponent(baseGlyphName, tuple(transformation)) 1587*e1fe3e4aSElliott Hughes warn( 1588*e1fe3e4aSElliott Hughes "The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.", 1589*e1fe3e4aSElliott Hughes DeprecationWarning, 1590*e1fe3e4aSElliott Hughes ) 1591*e1fe3e4aSElliott Hughes 1592*e1fe3e4aSElliott Hughes 1593*e1fe3e4aSElliott Hughes# all formats 1594*e1fe3e4aSElliott Hughes 1595*e1fe3e4aSElliott Hughes 1596*e1fe3e4aSElliott Hughesdef _validateAndMassagePointStructures( 1597*e1fe3e4aSElliott Hughes contour, pointAttributes, openContourOffCurveLeniency=False, validate=True 1598*e1fe3e4aSElliott Hughes): 1599*e1fe3e4aSElliott Hughes if not len(contour): 1600*e1fe3e4aSElliott Hughes return 1601*e1fe3e4aSElliott Hughes # store some data for later validation 1602*e1fe3e4aSElliott Hughes lastOnCurvePoint = None 1603*e1fe3e4aSElliott Hughes haveOffCurvePoint = False 1604*e1fe3e4aSElliott Hughes # validate and massage the individual point elements 1605*e1fe3e4aSElliott Hughes massaged = [] 1606*e1fe3e4aSElliott Hughes for index, element in enumerate(contour): 1607*e1fe3e4aSElliott Hughes # not <point> 1608*e1fe3e4aSElliott Hughes if element.tag != "point": 1609*e1fe3e4aSElliott Hughes raise GlifLibError( 1610*e1fe3e4aSElliott Hughes "Unknown child element (%s) of contour element." % element.tag 1611*e1fe3e4aSElliott Hughes ) 1612*e1fe3e4aSElliott Hughes point = dict(element.attrib) 1613*e1fe3e4aSElliott Hughes massaged.append(point) 1614*e1fe3e4aSElliott Hughes if validate: 1615*e1fe3e4aSElliott Hughes # unknown attributes 1616*e1fe3e4aSElliott Hughes for attr in point.keys(): 1617*e1fe3e4aSElliott Hughes if attr not in pointAttributes: 1618*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown attribute in point element: %s" % attr) 1619*e1fe3e4aSElliott Hughes # search for unknown children 1620*e1fe3e4aSElliott Hughes if len(element): 1621*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown child elements in point element.") 1622*e1fe3e4aSElliott Hughes # x and y are required 1623*e1fe3e4aSElliott Hughes for attr in ("x", "y"): 1624*e1fe3e4aSElliott Hughes try: 1625*e1fe3e4aSElliott Hughes point[attr] = _number(point[attr]) 1626*e1fe3e4aSElliott Hughes except KeyError as e: 1627*e1fe3e4aSElliott Hughes raise GlifLibError( 1628*e1fe3e4aSElliott Hughes f"Required {attr} attribute is missing in point element." 1629*e1fe3e4aSElliott Hughes ) from e 1630*e1fe3e4aSElliott Hughes # segment type 1631*e1fe3e4aSElliott Hughes pointType = point.pop("type", "offcurve") 1632*e1fe3e4aSElliott Hughes if validate and pointType not in pointTypeOptions: 1633*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown point type: %s" % pointType) 1634*e1fe3e4aSElliott Hughes if pointType == "offcurve": 1635*e1fe3e4aSElliott Hughes pointType = None 1636*e1fe3e4aSElliott Hughes point["segmentType"] = pointType 1637*e1fe3e4aSElliott Hughes if pointType is None: 1638*e1fe3e4aSElliott Hughes haveOffCurvePoint = True 1639*e1fe3e4aSElliott Hughes else: 1640*e1fe3e4aSElliott Hughes lastOnCurvePoint = index 1641*e1fe3e4aSElliott Hughes # move can only occur as the first point 1642*e1fe3e4aSElliott Hughes if validate and pointType == "move" and index != 0: 1643*e1fe3e4aSElliott Hughes raise GlifLibError( 1644*e1fe3e4aSElliott Hughes "A move point occurs after the first point in the contour." 1645*e1fe3e4aSElliott Hughes ) 1646*e1fe3e4aSElliott Hughes # smooth is optional 1647*e1fe3e4aSElliott Hughes smooth = point.get("smooth", "no") 1648*e1fe3e4aSElliott Hughes if validate and smooth is not None: 1649*e1fe3e4aSElliott Hughes if smooth not in pointSmoothOptions: 1650*e1fe3e4aSElliott Hughes raise GlifLibError("Unknown point smooth value: %s" % smooth) 1651*e1fe3e4aSElliott Hughes smooth = smooth == "yes" 1652*e1fe3e4aSElliott Hughes point["smooth"] = smooth 1653*e1fe3e4aSElliott Hughes # smooth can only be applied to curve and qcurve 1654*e1fe3e4aSElliott Hughes if validate and smooth and pointType is None: 1655*e1fe3e4aSElliott Hughes raise GlifLibError("smooth attribute set in an offcurve point.") 1656*e1fe3e4aSElliott Hughes # name is optional 1657*e1fe3e4aSElliott Hughes if "name" not in element.attrib: 1658*e1fe3e4aSElliott Hughes point["name"] = None 1659*e1fe3e4aSElliott Hughes if openContourOffCurveLeniency: 1660*e1fe3e4aSElliott Hughes # remove offcurves that precede a move. this is technically illegal, 1661*e1fe3e4aSElliott Hughes # but we let it slide because there are fonts out there in the wild like this. 1662*e1fe3e4aSElliott Hughes if massaged[0]["segmentType"] == "move": 1663*e1fe3e4aSElliott Hughes count = 0 1664*e1fe3e4aSElliott Hughes for point in reversed(massaged): 1665*e1fe3e4aSElliott Hughes if point["segmentType"] is None: 1666*e1fe3e4aSElliott Hughes count += 1 1667*e1fe3e4aSElliott Hughes else: 1668*e1fe3e4aSElliott Hughes break 1669*e1fe3e4aSElliott Hughes if count: 1670*e1fe3e4aSElliott Hughes massaged = massaged[:-count] 1671*e1fe3e4aSElliott Hughes # validate the off-curves in the segments 1672*e1fe3e4aSElliott Hughes if validate and haveOffCurvePoint and lastOnCurvePoint is not None: 1673*e1fe3e4aSElliott Hughes # we only care about how many offCurves there are before an onCurve 1674*e1fe3e4aSElliott Hughes # filter out the trailing offCurves 1675*e1fe3e4aSElliott Hughes offCurvesCount = len(massaged) - 1 - lastOnCurvePoint 1676*e1fe3e4aSElliott Hughes for point in massaged: 1677*e1fe3e4aSElliott Hughes segmentType = point["segmentType"] 1678*e1fe3e4aSElliott Hughes if segmentType is None: 1679*e1fe3e4aSElliott Hughes offCurvesCount += 1 1680*e1fe3e4aSElliott Hughes else: 1681*e1fe3e4aSElliott Hughes if offCurvesCount: 1682*e1fe3e4aSElliott Hughes # move and line can't be preceded by off-curves 1683*e1fe3e4aSElliott Hughes if segmentType == "move": 1684*e1fe3e4aSElliott Hughes # this will have been filtered out already 1685*e1fe3e4aSElliott Hughes raise GlifLibError("move can not have an offcurve.") 1686*e1fe3e4aSElliott Hughes elif segmentType == "line": 1687*e1fe3e4aSElliott Hughes raise GlifLibError("line can not have an offcurve.") 1688*e1fe3e4aSElliott Hughes elif segmentType == "curve": 1689*e1fe3e4aSElliott Hughes if offCurvesCount > 2: 1690*e1fe3e4aSElliott Hughes raise GlifLibError("Too many offcurves defined for curve.") 1691*e1fe3e4aSElliott Hughes elif segmentType == "qcurve": 1692*e1fe3e4aSElliott Hughes pass 1693*e1fe3e4aSElliott Hughes else: 1694*e1fe3e4aSElliott Hughes # unknown segment type. it'll be caught later. 1695*e1fe3e4aSElliott Hughes pass 1696*e1fe3e4aSElliott Hughes offCurvesCount = 0 1697*e1fe3e4aSElliott Hughes return massaged 1698*e1fe3e4aSElliott Hughes 1699*e1fe3e4aSElliott Hughes 1700*e1fe3e4aSElliott Hughes# --------------------- 1701*e1fe3e4aSElliott Hughes# Misc Helper Functions 1702*e1fe3e4aSElliott Hughes# --------------------- 1703*e1fe3e4aSElliott Hughes 1704*e1fe3e4aSElliott Hughes 1705*e1fe3e4aSElliott Hughesdef _relaxedSetattr(object, attr, value): 1706*e1fe3e4aSElliott Hughes try: 1707*e1fe3e4aSElliott Hughes setattr(object, attr, value) 1708*e1fe3e4aSElliott Hughes except AttributeError: 1709*e1fe3e4aSElliott Hughes pass 1710*e1fe3e4aSElliott Hughes 1711*e1fe3e4aSElliott Hughes 1712*e1fe3e4aSElliott Hughesdef _number(s): 1713*e1fe3e4aSElliott Hughes """ 1714*e1fe3e4aSElliott Hughes Given a numeric string, return an integer or a float, whichever 1715*e1fe3e4aSElliott Hughes the string indicates. _number("1") will return the integer 1, 1716*e1fe3e4aSElliott Hughes _number("1.0") will return the float 1.0. 1717*e1fe3e4aSElliott Hughes 1718*e1fe3e4aSElliott Hughes >>> _number("1") 1719*e1fe3e4aSElliott Hughes 1 1720*e1fe3e4aSElliott Hughes >>> _number("1.0") 1721*e1fe3e4aSElliott Hughes 1.0 1722*e1fe3e4aSElliott Hughes >>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL 1723*e1fe3e4aSElliott Hughes Traceback (most recent call last): 1724*e1fe3e4aSElliott Hughes ... 1725*e1fe3e4aSElliott Hughes GlifLibError: Could not convert a to an int or float. 1726*e1fe3e4aSElliott Hughes """ 1727*e1fe3e4aSElliott Hughes try: 1728*e1fe3e4aSElliott Hughes n = int(s) 1729*e1fe3e4aSElliott Hughes return n 1730*e1fe3e4aSElliott Hughes except ValueError: 1731*e1fe3e4aSElliott Hughes pass 1732*e1fe3e4aSElliott Hughes try: 1733*e1fe3e4aSElliott Hughes n = float(s) 1734*e1fe3e4aSElliott Hughes return n 1735*e1fe3e4aSElliott Hughes except ValueError: 1736*e1fe3e4aSElliott Hughes raise GlifLibError("Could not convert %s to an int or float." % s) 1737*e1fe3e4aSElliott Hughes 1738*e1fe3e4aSElliott Hughes 1739*e1fe3e4aSElliott Hughes# -------------------- 1740*e1fe3e4aSElliott Hughes# Rapid Value Fetching 1741*e1fe3e4aSElliott Hughes# -------------------- 1742*e1fe3e4aSElliott Hughes 1743*e1fe3e4aSElliott Hughes# base 1744*e1fe3e4aSElliott Hughes 1745*e1fe3e4aSElliott Hughes 1746*e1fe3e4aSElliott Hughesclass _DoneParsing(Exception): 1747*e1fe3e4aSElliott Hughes pass 1748*e1fe3e4aSElliott Hughes 1749*e1fe3e4aSElliott Hughes 1750*e1fe3e4aSElliott Hughesclass _BaseParser: 1751*e1fe3e4aSElliott Hughes def __init__(self): 1752*e1fe3e4aSElliott Hughes self._elementStack = [] 1753*e1fe3e4aSElliott Hughes 1754*e1fe3e4aSElliott Hughes def parse(self, text): 1755*e1fe3e4aSElliott Hughes from xml.parsers.expat import ParserCreate 1756*e1fe3e4aSElliott Hughes 1757*e1fe3e4aSElliott Hughes parser = ParserCreate() 1758*e1fe3e4aSElliott Hughes parser.StartElementHandler = self.startElementHandler 1759*e1fe3e4aSElliott Hughes parser.EndElementHandler = self.endElementHandler 1760*e1fe3e4aSElliott Hughes parser.Parse(text) 1761*e1fe3e4aSElliott Hughes 1762*e1fe3e4aSElliott Hughes def startElementHandler(self, name, attrs): 1763*e1fe3e4aSElliott Hughes self._elementStack.append(name) 1764*e1fe3e4aSElliott Hughes 1765*e1fe3e4aSElliott Hughes def endElementHandler(self, name): 1766*e1fe3e4aSElliott Hughes other = self._elementStack.pop(-1) 1767*e1fe3e4aSElliott Hughes assert other == name 1768*e1fe3e4aSElliott Hughes 1769*e1fe3e4aSElliott Hughes 1770*e1fe3e4aSElliott Hughes# unicodes 1771*e1fe3e4aSElliott Hughes 1772*e1fe3e4aSElliott Hughes 1773*e1fe3e4aSElliott Hughesdef _fetchUnicodes(glif): 1774*e1fe3e4aSElliott Hughes """ 1775*e1fe3e4aSElliott Hughes Get a list of unicodes listed in glif. 1776*e1fe3e4aSElliott Hughes """ 1777*e1fe3e4aSElliott Hughes parser = _FetchUnicodesParser() 1778*e1fe3e4aSElliott Hughes parser.parse(glif) 1779*e1fe3e4aSElliott Hughes return parser.unicodes 1780*e1fe3e4aSElliott Hughes 1781*e1fe3e4aSElliott Hughes 1782*e1fe3e4aSElliott Hughesclass _FetchUnicodesParser(_BaseParser): 1783*e1fe3e4aSElliott Hughes def __init__(self): 1784*e1fe3e4aSElliott Hughes self.unicodes = [] 1785*e1fe3e4aSElliott Hughes super().__init__() 1786*e1fe3e4aSElliott Hughes 1787*e1fe3e4aSElliott Hughes def startElementHandler(self, name, attrs): 1788*e1fe3e4aSElliott Hughes if ( 1789*e1fe3e4aSElliott Hughes name == "unicode" 1790*e1fe3e4aSElliott Hughes and self._elementStack 1791*e1fe3e4aSElliott Hughes and self._elementStack[-1] == "glyph" 1792*e1fe3e4aSElliott Hughes ): 1793*e1fe3e4aSElliott Hughes value = attrs.get("hex") 1794*e1fe3e4aSElliott Hughes if value is not None: 1795*e1fe3e4aSElliott Hughes try: 1796*e1fe3e4aSElliott Hughes value = int(value, 16) 1797*e1fe3e4aSElliott Hughes if value not in self.unicodes: 1798*e1fe3e4aSElliott Hughes self.unicodes.append(value) 1799*e1fe3e4aSElliott Hughes except ValueError: 1800*e1fe3e4aSElliott Hughes pass 1801*e1fe3e4aSElliott Hughes super().startElementHandler(name, attrs) 1802*e1fe3e4aSElliott Hughes 1803*e1fe3e4aSElliott Hughes 1804*e1fe3e4aSElliott Hughes# image 1805*e1fe3e4aSElliott Hughes 1806*e1fe3e4aSElliott Hughes 1807*e1fe3e4aSElliott Hughesdef _fetchImageFileName(glif): 1808*e1fe3e4aSElliott Hughes """ 1809*e1fe3e4aSElliott Hughes The image file name (if any) from glif. 1810*e1fe3e4aSElliott Hughes """ 1811*e1fe3e4aSElliott Hughes parser = _FetchImageFileNameParser() 1812*e1fe3e4aSElliott Hughes try: 1813*e1fe3e4aSElliott Hughes parser.parse(glif) 1814*e1fe3e4aSElliott Hughes except _DoneParsing: 1815*e1fe3e4aSElliott Hughes pass 1816*e1fe3e4aSElliott Hughes return parser.fileName 1817*e1fe3e4aSElliott Hughes 1818*e1fe3e4aSElliott Hughes 1819*e1fe3e4aSElliott Hughesclass _FetchImageFileNameParser(_BaseParser): 1820*e1fe3e4aSElliott Hughes def __init__(self): 1821*e1fe3e4aSElliott Hughes self.fileName = None 1822*e1fe3e4aSElliott Hughes super().__init__() 1823*e1fe3e4aSElliott Hughes 1824*e1fe3e4aSElliott Hughes def startElementHandler(self, name, attrs): 1825*e1fe3e4aSElliott Hughes if name == "image" and self._elementStack and self._elementStack[-1] == "glyph": 1826*e1fe3e4aSElliott Hughes self.fileName = attrs.get("fileName") 1827*e1fe3e4aSElliott Hughes raise _DoneParsing 1828*e1fe3e4aSElliott Hughes super().startElementHandler(name, attrs) 1829*e1fe3e4aSElliott Hughes 1830*e1fe3e4aSElliott Hughes 1831*e1fe3e4aSElliott Hughes# component references 1832*e1fe3e4aSElliott Hughes 1833*e1fe3e4aSElliott Hughes 1834*e1fe3e4aSElliott Hughesdef _fetchComponentBases(glif): 1835*e1fe3e4aSElliott Hughes """ 1836*e1fe3e4aSElliott Hughes Get a list of component base glyphs listed in glif. 1837*e1fe3e4aSElliott Hughes """ 1838*e1fe3e4aSElliott Hughes parser = _FetchComponentBasesParser() 1839*e1fe3e4aSElliott Hughes try: 1840*e1fe3e4aSElliott Hughes parser.parse(glif) 1841*e1fe3e4aSElliott Hughes except _DoneParsing: 1842*e1fe3e4aSElliott Hughes pass 1843*e1fe3e4aSElliott Hughes return list(parser.bases) 1844*e1fe3e4aSElliott Hughes 1845*e1fe3e4aSElliott Hughes 1846*e1fe3e4aSElliott Hughesclass _FetchComponentBasesParser(_BaseParser): 1847*e1fe3e4aSElliott Hughes def __init__(self): 1848*e1fe3e4aSElliott Hughes self.bases = [] 1849*e1fe3e4aSElliott Hughes super().__init__() 1850*e1fe3e4aSElliott Hughes 1851*e1fe3e4aSElliott Hughes def startElementHandler(self, name, attrs): 1852*e1fe3e4aSElliott Hughes if ( 1853*e1fe3e4aSElliott Hughes name == "component" 1854*e1fe3e4aSElliott Hughes and self._elementStack 1855*e1fe3e4aSElliott Hughes and self._elementStack[-1] == "outline" 1856*e1fe3e4aSElliott Hughes ): 1857*e1fe3e4aSElliott Hughes base = attrs.get("base") 1858*e1fe3e4aSElliott Hughes if base is not None: 1859*e1fe3e4aSElliott Hughes self.bases.append(base) 1860*e1fe3e4aSElliott Hughes super().startElementHandler(name, attrs) 1861*e1fe3e4aSElliott Hughes 1862*e1fe3e4aSElliott Hughes def endElementHandler(self, name): 1863*e1fe3e4aSElliott Hughes if name == "outline": 1864*e1fe3e4aSElliott Hughes raise _DoneParsing 1865*e1fe3e4aSElliott Hughes super().endElementHandler(name) 1866*e1fe3e4aSElliott Hughes 1867*e1fe3e4aSElliott Hughes 1868*e1fe3e4aSElliott Hughes# -------------- 1869*e1fe3e4aSElliott Hughes# GLIF Point Pen 1870*e1fe3e4aSElliott Hughes# -------------- 1871*e1fe3e4aSElliott Hughes 1872*e1fe3e4aSElliott Hughes_transformationInfo = [ 1873*e1fe3e4aSElliott Hughes # field name, default value 1874*e1fe3e4aSElliott Hughes ("xScale", 1), 1875*e1fe3e4aSElliott Hughes ("xyScale", 0), 1876*e1fe3e4aSElliott Hughes ("yxScale", 0), 1877*e1fe3e4aSElliott Hughes ("yScale", 1), 1878*e1fe3e4aSElliott Hughes ("xOffset", 0), 1879*e1fe3e4aSElliott Hughes ("yOffset", 0), 1880*e1fe3e4aSElliott Hughes] 1881*e1fe3e4aSElliott Hughes 1882*e1fe3e4aSElliott Hughes 1883*e1fe3e4aSElliott Hughesclass GLIFPointPen(AbstractPointPen): 1884*e1fe3e4aSElliott Hughes """ 1885*e1fe3e4aSElliott Hughes Helper class using the PointPen protocol to write the <outline> 1886*e1fe3e4aSElliott Hughes part of .glif files. 1887*e1fe3e4aSElliott Hughes """ 1888*e1fe3e4aSElliott Hughes 1889*e1fe3e4aSElliott Hughes def __init__(self, element, formatVersion=None, identifiers=None, validate=True): 1890*e1fe3e4aSElliott Hughes if identifiers is None: 1891*e1fe3e4aSElliott Hughes identifiers = set() 1892*e1fe3e4aSElliott Hughes self.formatVersion = GLIFFormatVersion(formatVersion) 1893*e1fe3e4aSElliott Hughes self.identifiers = identifiers 1894*e1fe3e4aSElliott Hughes self.outline = element 1895*e1fe3e4aSElliott Hughes self.contour = None 1896*e1fe3e4aSElliott Hughes self.prevOffCurveCount = 0 1897*e1fe3e4aSElliott Hughes self.prevPointTypes = [] 1898*e1fe3e4aSElliott Hughes self.validate = validate 1899*e1fe3e4aSElliott Hughes 1900*e1fe3e4aSElliott Hughes def beginPath(self, identifier=None, **kwargs): 1901*e1fe3e4aSElliott Hughes attrs = OrderedDict() 1902*e1fe3e4aSElliott Hughes if identifier is not None and self.formatVersion.major >= 2: 1903*e1fe3e4aSElliott Hughes if self.validate: 1904*e1fe3e4aSElliott Hughes if identifier in self.identifiers: 1905*e1fe3e4aSElliott Hughes raise GlifLibError( 1906*e1fe3e4aSElliott Hughes "identifier used more than once: %s" % identifier 1907*e1fe3e4aSElliott Hughes ) 1908*e1fe3e4aSElliott Hughes if not identifierValidator(identifier): 1909*e1fe3e4aSElliott Hughes raise GlifLibError( 1910*e1fe3e4aSElliott Hughes "identifier not formatted properly: %s" % identifier 1911*e1fe3e4aSElliott Hughes ) 1912*e1fe3e4aSElliott Hughes attrs["identifier"] = identifier 1913*e1fe3e4aSElliott Hughes self.identifiers.add(identifier) 1914*e1fe3e4aSElliott Hughes self.contour = etree.SubElement(self.outline, "contour", attrs) 1915*e1fe3e4aSElliott Hughes self.prevOffCurveCount = 0 1916*e1fe3e4aSElliott Hughes 1917*e1fe3e4aSElliott Hughes def endPath(self): 1918*e1fe3e4aSElliott Hughes if self.prevPointTypes and self.prevPointTypes[0] == "move": 1919*e1fe3e4aSElliott Hughes if self.validate and self.prevPointTypes[-1] == "offcurve": 1920*e1fe3e4aSElliott Hughes raise GlifLibError("open contour has loose offcurve point") 1921*e1fe3e4aSElliott Hughes # prevent lxml from writing self-closing tags 1922*e1fe3e4aSElliott Hughes if not len(self.contour): 1923*e1fe3e4aSElliott Hughes self.contour.text = "\n " 1924*e1fe3e4aSElliott Hughes self.contour = None 1925*e1fe3e4aSElliott Hughes self.prevPointType = None 1926*e1fe3e4aSElliott Hughes self.prevOffCurveCount = 0 1927*e1fe3e4aSElliott Hughes self.prevPointTypes = [] 1928*e1fe3e4aSElliott Hughes 1929*e1fe3e4aSElliott Hughes def addPoint( 1930*e1fe3e4aSElliott Hughes self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs 1931*e1fe3e4aSElliott Hughes ): 1932*e1fe3e4aSElliott Hughes attrs = OrderedDict() 1933*e1fe3e4aSElliott Hughes # coordinates 1934*e1fe3e4aSElliott Hughes if pt is not None: 1935*e1fe3e4aSElliott Hughes if self.validate: 1936*e1fe3e4aSElliott Hughes for coord in pt: 1937*e1fe3e4aSElliott Hughes if not isinstance(coord, numberTypes): 1938*e1fe3e4aSElliott Hughes raise GlifLibError("coordinates must be int or float") 1939*e1fe3e4aSElliott Hughes attrs["x"] = repr(pt[0]) 1940*e1fe3e4aSElliott Hughes attrs["y"] = repr(pt[1]) 1941*e1fe3e4aSElliott Hughes # segment type 1942*e1fe3e4aSElliott Hughes if segmentType == "offcurve": 1943*e1fe3e4aSElliott Hughes segmentType = None 1944*e1fe3e4aSElliott Hughes if self.validate: 1945*e1fe3e4aSElliott Hughes if segmentType == "move" and self.prevPointTypes: 1946*e1fe3e4aSElliott Hughes raise GlifLibError( 1947*e1fe3e4aSElliott Hughes "move occurs after a point has already been added to the contour." 1948*e1fe3e4aSElliott Hughes ) 1949*e1fe3e4aSElliott Hughes if ( 1950*e1fe3e4aSElliott Hughes segmentType in ("move", "line") 1951*e1fe3e4aSElliott Hughes and self.prevPointTypes 1952*e1fe3e4aSElliott Hughes and self.prevPointTypes[-1] == "offcurve" 1953*e1fe3e4aSElliott Hughes ): 1954*e1fe3e4aSElliott Hughes raise GlifLibError("offcurve occurs before %s point." % segmentType) 1955*e1fe3e4aSElliott Hughes if segmentType == "curve" and self.prevOffCurveCount > 2: 1956*e1fe3e4aSElliott Hughes raise GlifLibError("too many offcurve points before curve point.") 1957*e1fe3e4aSElliott Hughes if segmentType is not None: 1958*e1fe3e4aSElliott Hughes attrs["type"] = segmentType 1959*e1fe3e4aSElliott Hughes else: 1960*e1fe3e4aSElliott Hughes segmentType = "offcurve" 1961*e1fe3e4aSElliott Hughes if segmentType == "offcurve": 1962*e1fe3e4aSElliott Hughes self.prevOffCurveCount += 1 1963*e1fe3e4aSElliott Hughes else: 1964*e1fe3e4aSElliott Hughes self.prevOffCurveCount = 0 1965*e1fe3e4aSElliott Hughes self.prevPointTypes.append(segmentType) 1966*e1fe3e4aSElliott Hughes # smooth 1967*e1fe3e4aSElliott Hughes if smooth: 1968*e1fe3e4aSElliott Hughes if self.validate and segmentType == "offcurve": 1969*e1fe3e4aSElliott Hughes raise GlifLibError("can't set smooth in an offcurve point.") 1970*e1fe3e4aSElliott Hughes attrs["smooth"] = "yes" 1971*e1fe3e4aSElliott Hughes # name 1972*e1fe3e4aSElliott Hughes if name is not None: 1973*e1fe3e4aSElliott Hughes attrs["name"] = name 1974*e1fe3e4aSElliott Hughes # identifier 1975*e1fe3e4aSElliott Hughes if identifier is not None and self.formatVersion.major >= 2: 1976*e1fe3e4aSElliott Hughes if self.validate: 1977*e1fe3e4aSElliott Hughes if identifier in self.identifiers: 1978*e1fe3e4aSElliott Hughes raise GlifLibError( 1979*e1fe3e4aSElliott Hughes "identifier used more than once: %s" % identifier 1980*e1fe3e4aSElliott Hughes ) 1981*e1fe3e4aSElliott Hughes if not identifierValidator(identifier): 1982*e1fe3e4aSElliott Hughes raise GlifLibError( 1983*e1fe3e4aSElliott Hughes "identifier not formatted properly: %s" % identifier 1984*e1fe3e4aSElliott Hughes ) 1985*e1fe3e4aSElliott Hughes attrs["identifier"] = identifier 1986*e1fe3e4aSElliott Hughes self.identifiers.add(identifier) 1987*e1fe3e4aSElliott Hughes etree.SubElement(self.contour, "point", attrs) 1988*e1fe3e4aSElliott Hughes 1989*e1fe3e4aSElliott Hughes def addComponent(self, glyphName, transformation, identifier=None, **kwargs): 1990*e1fe3e4aSElliott Hughes attrs = OrderedDict([("base", glyphName)]) 1991*e1fe3e4aSElliott Hughes for (attr, default), value in zip(_transformationInfo, transformation): 1992*e1fe3e4aSElliott Hughes if self.validate and not isinstance(value, numberTypes): 1993*e1fe3e4aSElliott Hughes raise GlifLibError("transformation values must be int or float") 1994*e1fe3e4aSElliott Hughes if value != default: 1995*e1fe3e4aSElliott Hughes attrs[attr] = repr(value) 1996*e1fe3e4aSElliott Hughes if identifier is not None and self.formatVersion.major >= 2: 1997*e1fe3e4aSElliott Hughes if self.validate: 1998*e1fe3e4aSElliott Hughes if identifier in self.identifiers: 1999*e1fe3e4aSElliott Hughes raise GlifLibError( 2000*e1fe3e4aSElliott Hughes "identifier used more than once: %s" % identifier 2001*e1fe3e4aSElliott Hughes ) 2002*e1fe3e4aSElliott Hughes if self.validate and not identifierValidator(identifier): 2003*e1fe3e4aSElliott Hughes raise GlifLibError( 2004*e1fe3e4aSElliott Hughes "identifier not formatted properly: %s" % identifier 2005*e1fe3e4aSElliott Hughes ) 2006*e1fe3e4aSElliott Hughes attrs["identifier"] = identifier 2007*e1fe3e4aSElliott Hughes self.identifiers.add(identifier) 2008*e1fe3e4aSElliott Hughes etree.SubElement(self.outline, "component", attrs) 2009*e1fe3e4aSElliott Hughes 2010*e1fe3e4aSElliott Hughes 2011*e1fe3e4aSElliott Hughesif __name__ == "__main__": 2012*e1fe3e4aSElliott Hughes import doctest 2013*e1fe3e4aSElliott Hughes 2014*e1fe3e4aSElliott Hughes doctest.testmod() 2015