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