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