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