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