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