1from __future__ import absolute_import, unicode_literals 2import os 3import shutil 4from io import StringIO, BytesIO, open 5from copy import deepcopy 6from fontTools.misc.py23 import basestring, unicode, tounicode 7from ufoLib.glifLib import GlyphSet 8from ufoLib.validators import * 9from ufoLib.filenames import userNameToFileName 10from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning 11from ufoLib import plistlib 12""" 13A library for importing .ufo files and their descendants. 14Refer to http://unifiedfontobject.com for the UFO specification. 15 16The UFOReader and UFOWriter classes support versions 1, 2 and 3 17of the specification. 18 19Sets that list the font info attribute names for the fontinfo.plist 20formats are available for external use. These are: 21 fontInfoAttributesVersion1 22 fontInfoAttributesVersion2 23 fontInfoAttributesVersion3 24 25A set listing the fontinfo.plist attributes that were deprecated 26in version 2 is available for external use: 27 deprecatedFontInfoAttributesVersion2 28 29Functions that do basic validation on values for fontinfo.plist 30are available for external use. These are 31 validateFontInfoVersion2ValueForAttribute 32 validateFontInfoVersion3ValueForAttribute 33 34Value conversion functions are available for converting 35fontinfo.plist values between the possible format versions. 36 convertFontInfoValueForAttributeFromVersion1ToVersion2 37 convertFontInfoValueForAttributeFromVersion2ToVersion1 38 convertFontInfoValueForAttributeFromVersion2ToVersion3 39 convertFontInfoValueForAttributeFromVersion3ToVersion2 40""" 41 42__all__ = [ 43 "makeUFOPath", 44 "UFOLibError", 45 "UFOReader", 46 "UFOWriter", 47 "fontInfoAttributesVersion1", 48 "fontInfoAttributesVersion2", 49 "fontInfoAttributesVersion3", 50 "deprecatedFontInfoAttributesVersion2", 51 "validateFontInfoVersion2ValueForAttribute", 52 "validateFontInfoVersion3ValueForAttribute", 53 "convertFontInfoValueForAttributeFromVersion1ToVersion2", 54 "convertFontInfoValueForAttributeFromVersion2ToVersion1", 55 # deprecated 56 "convertUFOFormatVersion1ToFormatVersion2", 57] 58 59__version__ = "2.3.2" 60 61 62class UFOLibError(Exception): pass 63 64 65# ---------- 66# File Names 67# ---------- 68 69DEFAULT_GLYPHS_DIRNAME = "glyphs" 70DATA_DIRNAME = "data" 71IMAGES_DIRNAME = "images" 72METAINFO_FILENAME = "metainfo.plist" 73FONTINFO_FILENAME = "fontinfo.plist" 74LIB_FILENAME = "lib.plist" 75GROUPS_FILENAME = "groups.plist" 76KERNING_FILENAME = "kerning.plist" 77FEATURES_FILENAME = "features.fea" 78LAYERCONTENTS_FILENAME = "layercontents.plist" 79LAYERINFO_FILENAME = "layerinfo.plist" 80 81DEFAULT_LAYER_NAME = "public.default" 82 83supportedUFOFormatVersions = [1, 2, 3] 84 85 86# -------------- 87# Shared Methods 88# -------------- 89 90def _getPlist(self, fileName, default=None): 91 """ 92 Read a property list relative to the 93 path argument of UFOReader. If the file 94 is missing and default is None a 95 UFOLibError will be raised otherwise 96 default is returned. The errors that 97 could be raised during the reading of 98 a plist are unpredictable and/or too 99 large to list, so, a blind try: except: 100 is done. If an exception occurs, a 101 UFOLibError will be raised. 102 """ 103 path = os.path.join(self._path, fileName) 104 if not os.path.exists(path): 105 if default is not None: 106 return default 107 else: 108 raise UFOLibError("%s is missing in %s. This file is required" % (fileName, self._path)) 109 try: 110 with open(path, "rb") as f: 111 return plistlib.load(f) 112 except: 113 raise UFOLibError("The file %s could not be read." % fileName) 114 115# ---------- 116# UFO Reader 117# ---------- 118 119class UFOReader(object): 120 121 """ 122 Read the various components of the .ufo. 123 124 By default read data is validated. Set ``validate`` to 125 ``False`` to not validate the data. 126 """ 127 128 def __init__(self, path, validate=True): 129 if not os.path.exists(path): 130 raise UFOLibError("The specified UFO doesn't exist.") 131 self._path = path 132 self._validate = validate 133 self.readMetaInfo(validate=validate) 134 self._upConvertedKerningData = None 135 136 # properties 137 138 def _get_formatVersion(self): 139 return self._formatVersion 140 141 formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.") 142 143 # up conversion 144 145 def _upConvertKerning(self, validate): 146 """ 147 Up convert kerning and groups in UFO 1 and 2. 148 The data will be held internally until each bit of data 149 has been retrieved. The conversion of both must be done 150 at once, so the raw data is cached and an error is raised 151 if one bit of data becomes obsolete before it is called. 152 153 ``validate`` will validate the data. 154 """ 155 if self._upConvertedKerningData: 156 testKerning = self._readKerning() 157 if testKerning != self._upConvertedKerningData["originalKerning"]: 158 raise UFOLibError("The data in kerning.plist has been modified since it was converted to UFO 3 format.") 159 testGroups = self._readGroups() 160 if testGroups != self._upConvertedKerningData["originalGroups"]: 161 raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.") 162 else: 163 groups = self._readGroups() 164 if validate: 165 invalidFormatMessage = "groups.plist is not properly formatted." 166 if not isinstance(groups, dict): 167 raise UFOLibError(invalidFormatMessage) 168 for groupName, glyphList in list(groups.items()): 169 if not isinstance(groupName, basestring): 170 raise UFOLibError(invalidFormatMessage) 171 elif not isinstance(glyphList, list): 172 raise UFOLibError(invalidFormatMessage) 173 for glyphName in glyphList: 174 if not isinstance(glyphName, basestring): 175 raise UFOLibError(invalidFormatMessage) 176 self._upConvertedKerningData = dict( 177 kerning={}, 178 originalKerning=self._readKerning(), 179 groups={}, 180 originalGroups=groups 181 ) 182 # convert kerning and groups 183 kerning, groups, conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning( 184 self._upConvertedKerningData["originalKerning"], 185 deepcopy(self._upConvertedKerningData["originalGroups"]) 186 ) 187 # store 188 self._upConvertedKerningData["kerning"] = kerning 189 self._upConvertedKerningData["groups"] = groups 190 self._upConvertedKerningData["groupRenameMaps"] = conversionMaps 191 192 # support methods 193 194 _checkForFile = staticmethod(os.path.exists) 195 196 _getPlist = _getPlist 197 198 def readBytesFromPath(self, path, encoding=None): 199 """ 200 Returns the bytes in the file at the given path. 201 The path must be relative to the UFO path. 202 Returns None if the file does not exist. 203 An encoding may be passed if needed. 204 """ 205 fullPath = os.path.join(self._path, path) 206 if not self._checkForFile(fullPath): 207 return None 208 if os.path.isdir(fullPath): 209 raise UFOLibError("%s is a directory." % path) 210 if encoding: 211 f = open(fullPath, encoding=encoding) 212 else: 213 f = open(fullPath, "rb", encoding=encoding) 214 data = f.read() 215 f.close() 216 return data 217 218 def getReadFileForPath(self, path, encoding=None): 219 """ 220 Returns a file (or file-like) object for the 221 file at the given path. The path must be relative 222 to the UFO path. Returns None if the file does not exist. 223 An encoding may be passed if needed. 224 225 Note: The caller is responsible for closing the open file. 226 """ 227 fullPath = os.path.join(self._path, path) 228 if not self._checkForFile(fullPath): 229 return None 230 if os.path.isdir(fullPath): 231 raise UFOLibError("%s is a directory." % path) 232 if encoding: 233 f = open(fullPath, "rb", encoding=encoding) 234 else: 235 f = open(fullPath, "r") 236 return f 237 238 def getFileModificationTime(self, path): 239 """ 240 Returns the modification time (as reported by os.path.getmtime) 241 for the file at the given path. The path must be relative to 242 the UFO path. Returns None if the file does not exist. 243 """ 244 fullPath = os.path.join(self._path, path) 245 if not self._checkForFile(fullPath): 246 return None 247 return os.path.getmtime(fullPath) 248 249 # metainfo.plist 250 251 def readMetaInfo(self, validate=None): 252 """ 253 Read metainfo.plist. Only used for internal operations. 254 255 ``validate`` will validate the read data, by default it is set 256 to the class's validate value, can be overridden. 257 """ 258 if validate is None: 259 validate = self._validate 260 # should there be a blind try/except with a UFOLibError 261 # raised in except here (and elsewhere)? It would be nice to 262 # provide external callers with a single exception to catch. 263 data = self._getPlist(METAINFO_FILENAME) 264 if validate and not isinstance(data, dict): 265 raise UFOLibError("metainfo.plist is not properly formatted.") 266 formatVersion = data["formatVersion"] 267 if validate: 268 if not isinstance(formatVersion, int): 269 metaplist_path = os.path.join(self._path, METAINFO_FILENAME) 270 raise UFOLibError("formatVersion must be specified as an integer in " + metaplist_path) 271 if formatVersion not in supportedUFOFormatVersions: 272 raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path)) 273 self._formatVersion = formatVersion 274 275 # groups.plist 276 277 def _readGroups(self): 278 return self._getPlist(GROUPS_FILENAME, {}) 279 280 def readGroups(self, validate=None): 281 """ 282 Read groups.plist. Returns a dict. 283 ``validate`` will validate the read data, by default it is set to the 284 class's validate value, can be overridden. 285 """ 286 if validate is None: 287 validate = self._validate 288 # handle up conversion 289 if self._formatVersion < 3: 290 self._upConvertKerning(validate) 291 groups = self._upConvertedKerningData["groups"] 292 # normal 293 else: 294 groups = self._readGroups() 295 if validate: 296 valid, message = groupsValidator(groups) 297 if not valid: 298 raise UFOLibError(message) 299 return groups 300 301 def getKerningGroupConversionRenameMaps(self, validate=None): 302 """ 303 Get maps defining the renaming that was done during any 304 needed kerning group conversion. This method returns a 305 dictionary of this form: 306 307 { 308 "side1" : {"old group name" : "new group name"}, 309 "side2" : {"old group name" : "new group name"} 310 } 311 312 When no conversion has been performed, the side1 and side2 313 dictionaries will be empty. 314 315 ``validate`` will validate the groups, by default it is set to the 316 class's validate value, can be overridden. 317 """ 318 if validate is None: 319 validate = self._validate 320 if self._formatVersion >= 3: 321 return dict(side1={}, side2={}) 322 # use the public group reader to force the load and 323 # conversion of the data if it hasn't happened yet. 324 self.readGroups(validate=validate) 325 return self._upConvertedKerningData["groupRenameMaps"] 326 327 # fontinfo.plist 328 329 def _readInfo(self, validate): 330 data = self._getPlist(FONTINFO_FILENAME, {}) 331 if validate and not isinstance(data, dict): 332 raise UFOLibError("fontinfo.plist is not properly formatted.") 333 return data 334 335 def readInfo(self, info, validate=None): 336 """ 337 Read fontinfo.plist. It requires an object that allows 338 setting attributes with names that follow the fontinfo.plist 339 version 3 specification. This will write the attributes 340 defined in the file into the object. 341 342 ``validate`` will validate the read data, by default it is set to the 343 class's validate value, can be overridden. 344 """ 345 if validate is None: 346 validate = self._validate 347 infoDict = self._readInfo(validate) 348 infoDataToSet = {} 349 # version 1 350 if self._formatVersion == 1: 351 for attr in fontInfoAttributesVersion1: 352 value = infoDict.get(attr) 353 if value is not None: 354 infoDataToSet[attr] = value 355 infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet) 356 infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet) 357 # version 2 358 elif self._formatVersion == 2: 359 for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()): 360 value = infoDict.get(attr) 361 if value is None: 362 continue 363 infoDataToSet[attr] = value 364 infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet) 365 # version 3 366 elif self._formatVersion == 3: 367 for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()): 368 value = infoDict.get(attr) 369 if value is None: 370 continue 371 infoDataToSet[attr] = value 372 # unsupported version 373 else: 374 raise NotImplementedError 375 # validate data 376 if validate: 377 infoDataToSet = validateInfoVersion3Data(infoDataToSet) 378 # populate the object 379 for attr, value in list(infoDataToSet.items()): 380 try: 381 setattr(info, attr, value) 382 except AttributeError: 383 raise UFOLibError("The supplied info object does not support setting a necessary attribute (%s)." % attr) 384 385 # kerning.plist 386 387 def _readKerning(self): 388 data = self._getPlist(KERNING_FILENAME, {}) 389 return data 390 391 def readKerning(self, validate=None): 392 """ 393 Read kerning.plist. Returns a dict. 394 395 ``validate`` will validate the kerning data, by default it is set to the 396 class's validate value, can be overridden. 397 """ 398 if validate is None: 399 validate = self._validate 400 # handle up conversion 401 if self._formatVersion < 3: 402 self._upConvertKerning(validate) 403 kerningNested = self._upConvertedKerningData["kerning"] 404 # normal 405 else: 406 kerningNested = self._readKerning() 407 if validate: 408 valid, message = kerningValidator(kerningNested) 409 if not valid: 410 raise UFOLibError(message) 411 # flatten 412 kerning = {} 413 for left in kerningNested: 414 for right in kerningNested[left]: 415 value = kerningNested[left][right] 416 kerning[left, right] = value 417 return kerning 418 419 # lib.plist 420 421 def readLib(self, validate=None): 422 """ 423 Read lib.plist. Returns a dict. 424 425 ``validate`` will validate the data, by default it is set to the 426 class's validate value, can be overridden. 427 """ 428 if validate is None: 429 validate = self._validate 430 data = self._getPlist(LIB_FILENAME, {}) 431 if validate: 432 valid, message = fontLibValidator(data) 433 if not valid: 434 raise UFOLibError(message) 435 return data 436 437 # features.fea 438 439 def readFeatures(self): 440 """ 441 Read features.fea. Returns a string. 442 """ 443 path = os.path.join(self._path, FEATURES_FILENAME) 444 if not self._checkForFile(path): 445 return "" 446 with open(path, "r", encoding="utf-8") as f: 447 text = f.read() 448 return text 449 450 # glyph sets & layers 451 452 def _readLayerContents(self, validate): 453 """ 454 Rebuild the layer contents list by checking what glyphsets 455 are available on disk. 456 457 ``validate`` will validate the layer contents. 458 """ 459 if self._formatVersion < 3: 460 return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] 461 # read the file on disk 462 contents = self._getPlist(LAYERCONTENTS_FILENAME) 463 if validate: 464 valid, error = layerContentsValidator(contents, self._path) 465 if not valid: 466 raise UFOLibError(error) 467 return contents 468 469 def getLayerNames(self, validate=None): 470 """ 471 Get the ordered layer names from layercontents.plist. 472 473 ``validate`` will validate the data, by default it is set to the 474 class's validate value, can be overridden. 475 """ 476 if validate is None: 477 validate = self._validate 478 layerContents = self._readLayerContents(validate) 479 layerNames = [layerName for layerName, directoryName in layerContents] 480 return layerNames 481 482 def getDefaultLayerName(self, validate=None): 483 """ 484 Get the default layer name from layercontents.plist. 485 486 ``validate`` will validate the data, by default it is set to the 487 class's validate value, can be overridden. 488 """ 489 if validate is None: 490 validate = self._validate 491 layerContents = self._readLayerContents(validate) 492 for layerName, layerDirectory in layerContents: 493 if layerDirectory == DEFAULT_GLYPHS_DIRNAME: 494 return layerName 495 # this will already have been raised during __init__ 496 raise UFOLibError("The default layer is not defined in layercontents.plist.") 497 498 def getGlyphSet(self, layerName=None, validateRead=None, validateWrite=None): 499 """ 500 Return the GlyphSet associated with the 501 glyphs directory mapped to layerName 502 in the UFO. If layerName is not provided, 503 the name retrieved with getDefaultLayerName 504 will be used. 505 506 ``validateRead`` will validate the read data, by default it is set to the 507 class's validate value, can be overridden. 508 ``validateWrte`` will validate the written data, by default it is set to the 509 class's validate value, can be overridden. 510 """ 511 if validateRead is None: 512 validateRead = self._validate 513 if validateWrite is None: 514 validateWrite = self._validate 515 if layerName is None: 516 layerName = self.getDefaultLayerName(validate=validateRead) 517 directory = None 518 layerContents = self._readLayerContents(validateRead) 519 for storedLayerName, storedLayerDirectory in layerContents: 520 if layerName == storedLayerName: 521 directory = storedLayerDirectory 522 break 523 if directory is None: 524 raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName) 525 glyphsPath = os.path.join(self._path, directory) 526 return GlyphSet(glyphsPath, ufoFormatVersion=self._formatVersion, validateRead=validateRead, validateWrite=validateWrite) 527 528 def getCharacterMapping(self, layerName=None, validate=None): 529 """ 530 Return a dictionary that maps unicode values (ints) to 531 lists of glyph names. 532 """ 533 if validate is None: 534 validate = self._validate 535 glyphSet = self.getGlyphSet(layerName, validateRead=validate, validateWrite=True) 536 allUnicodes = glyphSet.getUnicodes() 537 cmap = {} 538 for glyphName, unicodes in allUnicodes.items(): 539 for code in unicodes: 540 if code in cmap: 541 cmap[code].append(glyphName) 542 else: 543 cmap[code] = [glyphName] 544 return cmap 545 546 # /data 547 548 def getDataDirectoryListing(self, maxDepth=100): 549 """ 550 Returns a list of all files in the data directory. 551 The returned paths will be relative to the UFO. 552 This will not list directory names, only file names. 553 Thus, empty directories will be skipped. 554 555 The maxDepth argument sets the maximum number 556 of sub-directories that are allowed. 557 """ 558 path = os.path.join(self._path, DATA_DIRNAME) 559 if not self._checkForFile(path): 560 return [] 561 listing = self._getDirectoryListing(path, maxDepth=maxDepth) 562 listing = [os.path.relpath(path, "data") for path in listing] 563 return listing 564 565 def _getDirectoryListing(self, path, depth=0, maxDepth=100): 566 if depth > maxDepth: 567 raise UFOLibError("Maximum recusion depth reached.") 568 result = [] 569 for fileName in os.listdir(path): 570 p = os.path.join(path, fileName) 571 if os.path.isdir(p): 572 result += self._getDirectoryListing(p, depth=depth+1, maxDepth=maxDepth) 573 else: 574 p = os.path.relpath(p, self._path) 575 result.append(p) 576 return result 577 578 def getImageDirectoryListing(self, validate=None): 579 """ 580 Returns a list of all image file names in 581 the images directory. Each of the images will 582 have been verified to have the PNG signature. 583 584 ``validate`` will validate the data, by default it is set to the 585 class's validate value, can be overridden. 586 """ 587 if validate is None: 588 validate = self._validate 589 if self._formatVersion < 3: 590 return [] 591 path = os.path.join(self._path, IMAGES_DIRNAME) 592 if not os.path.exists(path): 593 return [] 594 if not os.path.isdir(path): 595 raise UFOLibError("The UFO contains an \"images\" file instead of a directory.") 596 result = [] 597 for fileName in os.listdir(path): 598 p = os.path.join(path, fileName) 599 if os.path.isdir(p): 600 # silently skip this as version control 601 # systems often have hidden directories 602 continue 603 if validate: 604 valid, error = pngValidator(path=p) 605 if valid: 606 result.append(fileName) 607 else: 608 result.append(fileName) 609 return result 610 611 def readImage(self, fileName, validate=None): 612 """ 613 Return image data for the file named fileName. 614 615 ``validate`` will validate the data, by default it is set to the 616 class's validate value, can be overridden. 617 """ 618 if validate is None: 619 validate = self._validate 620 if self._formatVersion < 3: 621 raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion) 622 data = self.readBytesFromPath(os.path.join(IMAGES_DIRNAME, fileName)) 623 if data is None: 624 raise UFOLibError("No image file named %s." % fileName) 625 if validate: 626 valid, error = pngValidator(data=data) 627 if not valid: 628 raise UFOLibError(error) 629 return data 630 631# ---------- 632# UFO Writer 633# ---------- 634 635 636class UFOWriter(object): 637 638 """ 639 Write the various components of the .ufo. 640 641 By default, the written data will be validated before writing. Set ``validate`` to 642 ``False`` if you do not want to validate the data. Validation can also be overriden 643 on a per method level if desired. 644 """ 645 646 def __init__(self, path, formatVersion=3, fileCreator="org.robofab.ufoLib", validate=True): 647 if formatVersion not in supportedUFOFormatVersions: 648 raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) 649 # establish some basic stuff 650 self._path = path 651 self._formatVersion = formatVersion 652 self._fileCreator = fileCreator 653 self._downConversionKerningData = None 654 self._validate = validate 655 656 # if the file already exists, get the format version. 657 # this will be needed for up and down conversion. 658 previousFormatVersion = None 659 if os.path.exists(path): 660 metaInfo = self._getPlist(METAINFO_FILENAME) 661 previousFormatVersion = metaInfo.get("formatVersion") 662 try: 663 previousFormatVersion = int(previousFormatVersion) 664 except: 665 raise UFOLibError("The existing metainfo.plist is not properly formatted.") 666 if previousFormatVersion not in supportedUFOFormatVersions: 667 raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) 668 # catch down conversion 669 if previousFormatVersion is not None and previousFormatVersion > formatVersion: 670 raise UFOLibError("The UFO located at this path is a higher version (%d) than the version (%d) that is trying to be written. This is not supported." % (previousFormatVersion, formatVersion)) 671 # handle the layer contents 672 self.layerContents = {} 673 if previousFormatVersion is not None and previousFormatVersion >= 3: 674 # already exists 675 self._readLayerContents(validate=validate) 676 else: 677 # previous < 3 678 # imply the layer contents 679 p = os.path.join(path, DEFAULT_GLYPHS_DIRNAME) 680 if os.path.exists(p): 681 self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME} 682 # write the new metainfo 683 self._writeMetaInfo() 684 685 # properties 686 687 def _get_path(self): 688 return self._path 689 690 path = property(_get_path, doc="The path the UFO is being written to.") 691 692 def _get_formatVersion(self): 693 return self._formatVersion 694 695 formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.") 696 697 def _get_fileCreator(self): 698 return self._fileCreator 699 700 fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.") 701 702 # support methods 703 704 _getPlist = _getPlist 705 706 def _writePlist(self, fileName, data): 707 """ 708 Write a property list. The errors that 709 could be raised during the writing of 710 a plist are unpredictable and/or too 711 large to list, so, a blind try: except: 712 is done. If an exception occurs, a 713 UFOLibError will be raised. 714 """ 715 self._makeDirectory() 716 path = os.path.join(self._path, fileName) 717 try: 718 data = writePlistAtomically(data, path) 719 except: 720 raise UFOLibError("The data for the file %s could not be written because it is not properly formatted." % fileName) 721 722 def _deleteFile(self, fileName): 723 path = os.path.join(self._path, fileName) 724 if os.path.exists(path): 725 os.remove(path) 726 727 def _makeDirectory(self, subDirectory=None): 728 path = self._path 729 if subDirectory: 730 path = os.path.join(self._path, subDirectory) 731 if not os.path.exists(path): 732 os.makedirs(path) 733 return path 734 735 def _buildDirectoryTree(self, path): 736 directory, fileName = os.path.split(path) 737 directoryTree = [] 738 while directory: 739 directory, d = os.path.split(directory) 740 directoryTree.append(d) 741 directoryTree.reverse() 742 built = "" 743 for d in directoryTree: 744 d = os.path.join(built, d) 745 p = os.path.join(self._path, d) 746 if not os.path.exists(p): 747 os.mkdir(p) 748 built = d 749 750 def _removeFileForPath(self, path, raiseErrorIfMissing=False): 751 originalPath = path 752 path = os.path.join(self._path, path) 753 if not os.path.exists(path): 754 if raiseErrorIfMissing: 755 raise UFOLibError("The file %s does not exist." % path) 756 else: 757 if os.path.isdir(path): 758 shutil.rmtree(path) 759 else: 760 os.remove(path) 761 # remove any directories that are now empty 762 self._removeEmptyDirectoriesForPath(os.path.dirname(originalPath)) 763 764 def _removeEmptyDirectoriesForPath(self, directory): 765 absoluteDirectory = os.path.join(self._path, directory) 766 if not os.path.exists(absoluteDirectory): 767 return 768 if not len(os.listdir(absoluteDirectory)): 769 shutil.rmtree(absoluteDirectory) 770 else: 771 return 772 directory = os.path.dirname(directory) 773 if directory: 774 self._removeEmptyDirectoriesForPath(directory) 775 776 # file system interaction 777 778 def writeBytesToPath(self, path, data, encoding=None): 779 """ 780 Write bytes to path. If needed, the directory tree 781 for the given path will be built. The path must be 782 relative to the UFO. An encoding may be passed if needed. 783 """ 784 fullPath = os.path.join(self._path, path) 785 if os.path.exists(fullPath) and os.path.isdir(fullPath): 786 raise UFOLibError("A directory exists at %s." % path) 787 self._buildDirectoryTree(path) 788 if encoding: 789 data = StringIO(data).encode(encoding) 790 writeDataFileAtomically(data, fullPath) 791 792 def getFileObjectForPath(self, path, encoding=None): 793 """ 794 Creates a write mode file object at path. If needed, 795 the directory tree for the given path will be built. 796 The path must be relative to the UFO. An encoding may 797 be passed if needed. 798 799 Note: The caller is responsible for closing the open file. 800 """ 801 fullPath = os.path.join(self._path, path) 802 if os.path.exists(fullPath) and os.path.isdir(fullPath): 803 raise UFOLibError("A directory exists at %s." % path) 804 self._buildDirectoryTree(path) 805 return open(fullPath, "w", encoding=encoding) 806 807 def removeFileForPath(self, path): 808 """ 809 Remove the file (or directory) at path. The path 810 must be relative to the UFO. This is only allowed 811 for files in the data and image directories. 812 """ 813 # make sure that only data or images is being changed 814 d = path 815 parts = [] 816 while d: 817 d, p = os.path.split(d) 818 if p: 819 parts.append(p) 820 if parts[-1] not in ("images", "data"): 821 raise UFOLibError("Removing \"%s\" is not legal." % path) 822 # remove the file 823 self._removeFileForPath(path, raiseErrorIfMissing=True) 824 825 def copyFromReader(self, reader, sourcePath, destPath): 826 """ 827 Copy the sourcePath in the provided UFOReader to destPath 828 in this writer. The paths must be relative. They may represent 829 directories or paths. This uses the most memory efficient 830 method possible for copying the data possible. 831 """ 832 if not isinstance(reader, UFOReader): 833 raise UFOLibError("The reader must be an instance of UFOReader.") 834 fullSourcePath = os.path.join(reader._path, sourcePath) 835 if not reader._checkForFile(fullSourcePath): 836 raise UFOLibError("No file named \"%s\" to copy from." % sourcePath) 837 fullDestPath = os.path.join(self._path, destPath) 838 if os.path.exists(fullDestPath): 839 raise UFOLibError("A file named \"%s\" already exists." % sourcePath) 840 self._buildDirectoryTree(destPath) 841 if os.path.isdir(fullSourcePath): 842 shutil.copytree(fullSourcePath, fullDestPath) 843 else: 844 shutil.copy(fullSourcePath, fullDestPath) 845 846 # UFO mod time 847 848 def setModificationTime(self): 849 """ 850 Set the UFO modification time to the current time. 851 This is never called automatically. It is up to the 852 caller to call this when finished working on the UFO. 853 """ 854 os.utime(self._path, None) 855 856 # metainfo.plist 857 858 def _writeMetaInfo(self): 859 metaInfo = dict( 860 creator=self._fileCreator, 861 formatVersion=self._formatVersion 862 ) 863 self._writePlist(METAINFO_FILENAME, metaInfo) 864 865 # groups.plist 866 867 def setKerningGroupConversionRenameMaps(self, maps): 868 """ 869 Set maps defining the renaming that should be done 870 when writing groups and kerning in UFO 1 and UFO 2. 871 This will effectively undo the conversion done when 872 UFOReader reads this data. The dictionary should have 873 this form: 874 875 { 876 "side1" : {"group name to use when writing" : "group name in data"}, 877 "side2" : {"group name to use when writing" : "group name in data"} 878 } 879 880 This is the same form returned by UFOReader's 881 getKerningGroupConversionRenameMaps method. 882 """ 883 if self._formatVersion >= 3: 884 return # XXX raise an error here 885 # flip the dictionaries 886 remap = {} 887 for side in ("side1", "side2"): 888 for writeName, dataName in list(maps[side].items()): 889 remap[dataName] = writeName 890 self._downConversionKerningData = dict(groupRenameMap=remap) 891 892 def writeGroups(self, groups, validate=None): 893 """ 894 Write groups.plist. This method requires a 895 dict of glyph groups as an argument. 896 897 ``validate`` will validate the data, by default it is set to the 898 class's validate value, can be overridden. 899 """ 900 if validate is None: 901 validate = self._validate 902 # validate the data structure 903 if validate: 904 valid, message = groupsValidator(groups) 905 if not valid: 906 raise UFOLibError(message) 907 # down convert 908 if self._formatVersion < 3 and self._downConversionKerningData is not None: 909 remap = self._downConversionKerningData["groupRenameMap"] 910 remappedGroups = {} 911 # there are some edge cases here that are ignored: 912 # 1. if a group is being renamed to a name that 913 # already exists, the existing group is always 914 # overwritten. (this is why there are two loops 915 # below.) there doesn't seem to be a logical 916 # solution to groups mismatching and overwriting 917 # with the specifiecd group seems like a better 918 # solution than throwing an error. 919 # 2. if side 1 and side 2 groups are being renamed 920 # to the same group name there is no check to 921 # ensure that the contents are identical. that 922 # is left up to the caller. 923 for name, contents in list(groups.items()): 924 if name in remap: 925 continue 926 remappedGroups[name] = contents 927 for name, contents in list(groups.items()): 928 if name not in remap: 929 continue 930 name = remap[name] 931 remappedGroups[name] = contents 932 groups = remappedGroups 933 # pack and write 934 groupsNew = {} 935 for key, value in list(groups.items()): 936 groupsNew[key] = list(value) 937 if groupsNew: 938 self._writePlist(GROUPS_FILENAME, groupsNew) 939 else: 940 self._deleteFile(GROUPS_FILENAME) 941 942 # fontinfo.plist 943 944 def writeInfo(self, info, validate=None): 945 """ 946 Write info.plist. This method requires an object 947 that supports getting attributes that follow the 948 fontinfo.plist version 2 specification. Attributes 949 will be taken from the given object and written 950 into the file. 951 952 ``validate`` will validate the data, by default it is set to the 953 class's validate value, can be overridden. 954 """ 955 if validate is None: 956 validate = self._validate 957 # gather version 3 data 958 infoData = {} 959 for attr in list(fontInfoAttributesVersion3ValueData.keys()): 960 if hasattr(info, attr): 961 try: 962 value = getattr(info, attr) 963 except AttributeError: 964 raise UFOLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) 965 if value is None: 966 continue 967 infoData[attr] = value 968 # down convert data if necessary and validate 969 if self._formatVersion == 3: 970 if validate: 971 infoData = validateInfoVersion3Data(infoData) 972 elif self._formatVersion == 2: 973 infoData = _convertFontInfoDataVersion3ToVersion2(infoData) 974 if validate: 975 infoData = validateInfoVersion2Data(infoData) 976 elif self._formatVersion == 1: 977 infoData = _convertFontInfoDataVersion3ToVersion2(infoData) 978 if validate: 979 infoData = validateInfoVersion2Data(infoData) 980 infoData = _convertFontInfoDataVersion2ToVersion1(infoData) 981 # write file 982 self._writePlist(FONTINFO_FILENAME, infoData) 983 984 # kerning.plist 985 986 def writeKerning(self, kerning, validate=None): 987 """ 988 Write kerning.plist. This method requires a 989 dict of kerning pairs as an argument. 990 991 This performs basic structural validation of the kerning, 992 but it does not check for compliance with the spec in 993 regards to conflicting pairs. The assumption is that the 994 kerning data being passed is standards compliant. 995 996 ``validate`` will validate the data, by default it is set to the 997 class's validate value, can be overridden. 998 """ 999 if validate is None: 1000 validate = self._validate 1001 # validate the data structure 1002 if validate: 1003 invalidFormatMessage = "The kerning is not properly formatted." 1004 if not isDictEnough(kerning): 1005 raise UFOLibError(invalidFormatMessage) 1006 for pair, value in list(kerning.items()): 1007 if not isinstance(pair, (list, tuple)): 1008 raise UFOLibError(invalidFormatMessage) 1009 if not len(pair) == 2: 1010 raise UFOLibError(invalidFormatMessage) 1011 if not isinstance(pair[0], basestring): 1012 raise UFOLibError(invalidFormatMessage) 1013 if not isinstance(pair[1], basestring): 1014 raise UFOLibError(invalidFormatMessage) 1015 if not isinstance(value, (int, float)): 1016 raise UFOLibError(invalidFormatMessage) 1017 # down convert 1018 if self._formatVersion < 3 and self._downConversionKerningData is not None: 1019 remap = self._downConversionKerningData["groupRenameMap"] 1020 remappedKerning = {} 1021 for (side1, side2), value in list(kerning.items()): 1022 side1 = remap.get(side1, side1) 1023 side2 = remap.get(side2, side2) 1024 remappedKerning[side1, side2] = value 1025 kerning = remappedKerning 1026 # pack and write 1027 kerningDict = {} 1028 for left, right in list(kerning.keys()): 1029 value = kerning[left, right] 1030 if left not in kerningDict: 1031 kerningDict[left] = {} 1032 kerningDict[left][right] = value 1033 if kerningDict: 1034 self._writePlist(KERNING_FILENAME, kerningDict) 1035 else: 1036 self._deleteFile(KERNING_FILENAME) 1037 1038 # lib.plist 1039 1040 def writeLib(self, libDict, validate=None): 1041 """ 1042 Write lib.plist. This method requires a 1043 lib dict as an argument. 1044 1045 ``validate`` will validate the data, by default it is set to the 1046 class's validate value, can be overridden. 1047 """ 1048 if validate is None: 1049 validate = self._validate 1050 if validate: 1051 valid, message = fontLibValidator(libDict) 1052 if not valid: 1053 raise UFOLibError(message) 1054 if libDict: 1055 self._writePlist(LIB_FILENAME, libDict) 1056 else: 1057 self._deleteFile(LIB_FILENAME) 1058 1059 # features.fea 1060 1061 def writeFeatures(self, features, validate=None): 1062 """ 1063 Write features.fea. This method requires a 1064 features string as an argument. 1065 """ 1066 if validate is None: 1067 validate = self._validate 1068 if self._formatVersion == 1: 1069 raise UFOLibError("features.fea is not allowed in UFO Format Version 1.") 1070 if validate: 1071 if not isinstance(features, basestring): 1072 raise UFOLibError("The features are not text.") 1073 self._makeDirectory() 1074 path = os.path.join(self._path, FEATURES_FILENAME) 1075 writeFileAtomically(features, path) 1076 1077 # glyph sets & layers 1078 1079 def _readLayerContents(self, validate): 1080 """ 1081 Rebuild the layer contents list by checking what glyph sets 1082 are available on disk. 1083 1084 ``validate`` will validate the data. 1085 """ 1086 # read the file on disk 1087 raw = self._getPlist(LAYERCONTENTS_FILENAME) 1088 contents = {} 1089 if validate: 1090 valid, error = layerContentsValidator(raw, self._path) 1091 if not valid: 1092 raise UFOLibError(error) 1093 for entry in raw: 1094 layerName, directoryName = entry 1095 contents[layerName] = directoryName 1096 self.layerContents = contents 1097 1098 def writeLayerContents(self, layerOrder=None, validate=None): 1099 """ 1100 Write the layercontents.plist file. This method *must* be called 1101 after all glyph sets have been written. 1102 """ 1103 if validate is None: 1104 validate = self._validate 1105 if self.formatVersion < 3: 1106 return 1107 if layerOrder is not None: 1108 newOrder = [] 1109 for layerName in layerOrder: 1110 if layerName is None: 1111 layerName = DEFAULT_LAYER_NAME 1112 else: 1113 layerName = tounicode(layerName) 1114 newOrder.append(layerName) 1115 layerOrder = newOrder 1116 else: 1117 layerOrder = list(self.layerContents.keys()) 1118 if validate and set(layerOrder) != set(self.layerContents.keys()): 1119 raise UFOLibError("The layer order content does not match the glyph sets that have been created.") 1120 layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder] 1121 self._writePlist(LAYERCONTENTS_FILENAME, layerContents) 1122 1123 def _findDirectoryForLayerName(self, layerName): 1124 foundDirectory = None 1125 for existingLayerName, directoryName in list(self.layerContents.items()): 1126 if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME: 1127 foundDirectory = directoryName 1128 break 1129 elif existingLayerName == layerName: 1130 foundDirectory = directoryName 1131 break 1132 if not foundDirectory: 1133 raise UFOLibError("Could not locate a glyph set directory for the layer named %s." % layerName) 1134 return foundDirectory 1135 1136 def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None, validateRead=None, validateWrite=None): 1137 """ 1138 Return the GlyphSet object associated with the 1139 appropriate glyph directory in the .ufo. 1140 If layerName is None, the default glyph set 1141 will be used. The defaultLayer flag indictes 1142 that the layer should be saved into the default 1143 glyphs directory. 1144 1145 ``validateRead`` will validate the read data, by default it is set to the 1146 class's validate value, can be overridden. 1147 ``validateWrte`` will validate the written data, by default it is set to the 1148 class's validate value, can be overridden. 1149 """ 1150 if validateRead is None: 1151 validateRead = self._validate 1152 if validateWrite is None: 1153 validateWrite = self._validate 1154 # only default can be written in < 3 1155 if self._formatVersion < 3 and (not defaultLayer or layerName is not None): 1156 raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion) 1157 # locate a layer name when None has been given 1158 if layerName is None and defaultLayer: 1159 for existingLayerName, directory in list(self.layerContents.items()): 1160 if directory == DEFAULT_GLYPHS_DIRNAME: 1161 layerName = existingLayerName 1162 if layerName is None: 1163 layerName = DEFAULT_LAYER_NAME 1164 elif layerName is None and not defaultLayer: 1165 raise UFOLibError("A layer name must be provided for non-default layers.") 1166 # move along to format specific writing 1167 if self.formatVersion == 1: 1168 return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc) 1169 elif self.formatVersion == 2: 1170 return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc) 1171 elif self.formatVersion == 3: 1172 return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc) 1173 1174 def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): 1175 glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME) 1176 return GlyphSet(glyphDir, glyphNameToFileNameFunc, ufoFormatVersion=1, validateRead=validateRead, validateWrite=validateWrite) 1177 1178 def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): 1179 glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME) 1180 return GlyphSet(glyphDir, glyphNameToFileNameFunc, ufoFormatVersion=2, validateRead=validateRead, validateWrite=validateWrite) 1181 1182 def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None): 1183 # if the default flag is on, make sure that the default in the file 1184 # matches the default being written. also make sure that this layer 1185 # name is not already linked to a non-default layer. 1186 if defaultLayer: 1187 for existingLayerName, directory in list(self.layerContents.items()): 1188 if directory == DEFAULT_GLYPHS_DIRNAME: 1189 if existingLayerName != layerName: 1190 raise UFOLibError("Another layer is already mapped to the default directory.") 1191 elif existingLayerName == layerName: 1192 raise UFOLibError("The layer name is already mapped to a non-default layer.") 1193 # get an existing directory name 1194 if layerName in self.layerContents: 1195 directory = self.layerContents[layerName] 1196 # get a new directory name 1197 else: 1198 if defaultLayer: 1199 directory = DEFAULT_GLYPHS_DIRNAME 1200 else: 1201 # not caching this could be slightly expensive, 1202 # but caching it will be cumbersome 1203 existing = [d.lower() for d in list(self.layerContents.values())] 1204 if not isinstance(layerName, unicode): 1205 try: 1206 layerName = unicode(layerName) 1207 except UnicodeDecodeError: 1208 raise UFOLibError("The specified layer name is not a Unicode string.") 1209 directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.") 1210 # make the directory 1211 path = os.path.join(self._path, directory) 1212 if not os.path.exists(path): 1213 self._makeDirectory(subDirectory=directory) 1214 # store the mapping 1215 self.layerContents[layerName] = directory 1216 # load the glyph set 1217 return GlyphSet(path, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3, validateRead=validateRead, validateWrite=validateWrite) 1218 1219 def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): 1220 """ 1221 Rename a glyph set. 1222 1223 Note: if a GlyphSet object has already been retrieved for 1224 layerName, it is up to the caller to inform that object that 1225 the directory it represents has changed. 1226 """ 1227 if self._formatVersion < 3: 1228 # ignore renaming glyph sets for UFO1 UFO2 1229 # just write the data from the default layer 1230 return 1231 # the new and old names can be the same 1232 # as long as the default is being switched 1233 if layerName == newLayerName: 1234 # if the default is off and the layer is already not the default, skip 1235 if self.layerContents[layerName] != DEFAULT_GLYPHS_DIRNAME and not defaultLayer: 1236 return 1237 # if the default is on and the layer is already the default, skip 1238 if self.layerContents[layerName] == DEFAULT_GLYPHS_DIRNAME and defaultLayer: 1239 return 1240 else: 1241 # make sure the new layer name doesn't already exist 1242 if newLayerName is None: 1243 newLayerName = DEFAULT_LAYER_NAME 1244 if newLayerName in self.layerContents: 1245 raise UFOLibError("A layer named %s already exists." % newLayerName) 1246 # make sure the default layer doesn't already exist 1247 if defaultLayer and DEFAULT_GLYPHS_DIRNAME in list(self.layerContents.values()): 1248 raise UFOLibError("A default layer already exists.") 1249 # get the paths 1250 oldDirectory = self._findDirectoryForLayerName(layerName) 1251 if defaultLayer: 1252 newDirectory = DEFAULT_GLYPHS_DIRNAME 1253 else: 1254 existing = [name.lower() for name in list(self.layerContents.values())] 1255 newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.") 1256 # update the internal mapping 1257 del self.layerContents[layerName] 1258 self.layerContents[newLayerName] = newDirectory 1259 # do the file system copy 1260 oldDirectory = os.path.join(self._path, oldDirectory) 1261 newDirectory = os.path.join(self._path, newDirectory) 1262 shutil.move(oldDirectory, newDirectory) 1263 1264 def deleteGlyphSet(self, layerName): 1265 """ 1266 Remove the glyph set matching layerName. 1267 """ 1268 if self._formatVersion < 3: 1269 # ignore deleting glyph sets for UFO1 UFO2 as there are no layers 1270 # just write the data from the default layer 1271 return 1272 foundDirectory = self._findDirectoryForLayerName(layerName) 1273 self._removeFileForPath(foundDirectory) 1274 del self.layerContents[layerName] 1275 1276 # /images 1277 1278 def writeImage(self, fileName, data, validate=None): 1279 """ 1280 Write data to fileName in the images directory. 1281 The data must be a valid PNG. 1282 """ 1283 if validate is None: 1284 validate = self._validate 1285 if self._formatVersion < 3: 1286 raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) 1287 if validate: 1288 valid, error = pngValidator(data=data) 1289 if not valid: 1290 raise UFOLibError(error) 1291 path = os.path.join(IMAGES_DIRNAME, fileName) 1292 self.writeBytesToPath(path, data) 1293 1294 def removeImage(self, fileName, validate=None): 1295 """ 1296 Remove the file named fileName from the 1297 images directory. 1298 """ 1299 if validate is None: 1300 validate = self._validate 1301 if self._formatVersion < 3: 1302 raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) 1303 path = os.path.join(IMAGES_DIRNAME, fileName) 1304 self.removeFileForPath(path) 1305 1306 def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None): 1307 """ 1308 Copy the sourceFileName in the provided UFOReader to destFileName 1309 in this writer. This uses the most memory efficient method possible 1310 for copying the data possible. 1311 """ 1312 if validate is None: 1313 validate = self._validate 1314 if self._formatVersion < 3: 1315 raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) 1316 sourcePath = os.path.join("images", sourceFileName) 1317 destPath = os.path.join("images", destFileName) 1318 self.copyFromReader(reader, sourcePath, destPath) 1319 1320 1321# ---------------- 1322# Helper Functions 1323# ---------------- 1324 1325def makeUFOPath(path): 1326 """ 1327 Return a .ufo pathname. 1328 1329 >>> makeUFOPath("directory/something.ext") == ( 1330 ... os.path.join('directory', 'something.ufo')) 1331 True 1332 >>> makeUFOPath("directory/something.another.thing.ext") == ( 1333 ... os.path.join('directory', 'something.another.thing.ufo')) 1334 True 1335 """ 1336 dir, name = os.path.split(path) 1337 name = ".".join([".".join(name.split(".")[:-1]), "ufo"]) 1338 return os.path.join(dir, name) 1339 1340def writePlistAtomically(obj, path): 1341 """ 1342 Write a plist for "obj" to "path". Do this sort of atomically, 1343 making it harder to cause corrupt files, for example when writePlist 1344 encounters an error halfway during write. This also checks to see 1345 if text matches the text that is already in the file at path. 1346 If so, the file is not rewritten so that the modification date 1347 is preserved. 1348 """ 1349 data = plistlib.dumps(obj) 1350 writeDataFileAtomically(data, path) 1351 1352def writeFileAtomically(text, path, encoding="utf-8"): 1353 """ 1354 Write text into a file at path. Do this sort of atomically 1355 making it harder to cause corrupt files. This also checks to see 1356 if text matches the text that is already in the file at path. 1357 If so, the file is not rewritten so that the modification date 1358 is preserved. An encoding may be passed if needed. 1359 """ 1360 if os.path.exists(path): 1361 with open(path, "r", encoding=encoding) as f: 1362 oldText = f.read() 1363 if text == oldText: 1364 return 1365 # if the text is empty, remove the existing file 1366 if not text: 1367 os.remove(path) 1368 if text: 1369 with open(path, "w", encoding=encoding) as f: 1370 f.write(text) 1371 1372def writeDataFileAtomically(data, path): 1373 """ 1374 Write data into a file at path. Do this sort of atomically 1375 making it harder to cause corrupt files. This also checks to see 1376 if data matches the data that is already in the file at path. 1377 If so, the file is not rewritten so that the modification date 1378 is preserved. 1379 """ 1380 assert isinstance(data, bytes) 1381 if os.path.exists(path): 1382 f = open(path, "rb") 1383 oldData = f.read() 1384 f.close() 1385 if data == oldData: 1386 return 1387 # if the data is empty, remove the existing file 1388 if not data: 1389 os.remove(path) 1390 if data: 1391 f = open(path, "wb") 1392 f.write(data) 1393 f.close() 1394 1395# --------------------------- 1396# Format Conversion Functions 1397# --------------------------- 1398 1399def convertUFOFormatVersion1ToFormatVersion2(inPath, outPath=None, validateRead=False, validateWrite=True): 1400 """ 1401 Function for converting a version format 1 UFO 1402 to version format 2. inPath should be a path 1403 to a UFO. outPath is the path where the new UFO 1404 should be written. If outPath is not given, the 1405 inPath will be used and, therefore, the UFO will 1406 be converted in place. Otherwise, if outPath is 1407 specified, nothing must exist at that path. 1408 1409 ``validateRead`` will validate the read data. 1410 ``validateWrite`` will validate the written data. 1411 """ 1412 from warnings import warn 1413 warn("convertUFOFormatVersion1ToFormatVersion2 is deprecated.", DeprecationWarning) 1414 if outPath is None: 1415 outPath = inPath 1416 if inPath != outPath and os.path.exists(outPath): 1417 raise UFOLibError("A file already exists at %s." % outPath) 1418 # use a reader for loading most of the data 1419 reader = UFOReader(inPath, validate=validateRead) 1420 if reader.formatVersion == 2: 1421 raise UFOLibError("The UFO at %s is already format version 2." % inPath) 1422 groups = reader.readGroups() 1423 kerning = reader.readKerning() 1424 libData = reader.readLib() 1425 # read the info data manually and convert 1426 infoPath = os.path.join(inPath, FONTINFO_FILENAME) 1427 if not os.path.exists(infoPath): 1428 infoData = {} 1429 else: 1430 with open(infoPath, "rb") as f: 1431 infoData = plistlib.load(f) 1432 infoData = _convertFontInfoDataVersion1ToVersion2(infoData) 1433 # if the paths are the same, only need to change the 1434 # fontinfo and meta info files. 1435 infoPath = os.path.join(outPath, FONTINFO_FILENAME) 1436 if inPath == outPath: 1437 metaInfoPath = os.path.join(inPath, METAINFO_FILENAME) 1438 metaInfo = dict( 1439 creator="org.robofab.ufoLib", 1440 formatVersion=2 1441 ) 1442 writePlistAtomically(metaInfo, metaInfoPath) 1443 writePlistAtomically(infoData, infoPath) 1444 # otherwise write everything. 1445 else: 1446 writer = UFOWriter(outPath, formatVersion=2, validate=validateWrite) 1447 writer.writeGroups(groups) 1448 writer.writeKerning(kerning) 1449 writer.writeLib(libData) 1450 # write the info manually 1451 writePlistAtomically(infoData, infoPath) 1452 # copy the glyph tree 1453 inGlyphs = os.path.join(inPath, DEFAULT_GLYPHS_DIRNAME) 1454 outGlyphs = os.path.join(outPath, DEFAULT_GLYPHS_DIRNAME) 1455 if os.path.exists(inGlyphs): 1456 shutil.copytree(inGlyphs, outGlyphs) 1457 1458# ---------------------- 1459# fontinfo.plist Support 1460# ---------------------- 1461 1462# Version Validators 1463 1464# There is no version 1 validator and there shouldn't be. 1465# The version 1 spec was very loose and there were numerous 1466# cases of invalid values. 1467 1468def validateFontInfoVersion2ValueForAttribute(attr, value): 1469 """ 1470 This performs very basic validation of the value for attribute 1471 following the UFO 2 fontinfo.plist specification. The results 1472 of this should not be interpretted as *correct* for the font 1473 that they are part of. This merely indicates that the value 1474 is of the proper type and, where the specification defines 1475 a set range of possible values for an attribute, that the 1476 value is in the accepted range. 1477 """ 1478 dataValidationDict = fontInfoAttributesVersion2ValueData[attr] 1479 valueType = dataValidationDict.get("type") 1480 validator = dataValidationDict.get("valueValidator") 1481 valueOptions = dataValidationDict.get("valueOptions") 1482 # have specific options for the validator 1483 if valueOptions is not None: 1484 isValidValue = validator(value, valueOptions) 1485 # no specific options 1486 else: 1487 if validator == genericTypeValidator: 1488 isValidValue = validator(value, valueType) 1489 else: 1490 isValidValue = validator(value) 1491 return isValidValue 1492 1493def validateInfoVersion2Data(infoData): 1494 """ 1495 This performs very basic validation of the value for infoData 1496 following the UFO 2 fontinfo.plist specification. The results 1497 of this should not be interpretted as *correct* for the font 1498 that they are part of. This merely indicates that the values 1499 are of the proper type and, where the specification defines 1500 a set range of possible values for an attribute, that the 1501 value is in the accepted range. 1502 """ 1503 validInfoData = {} 1504 for attr, value in list(infoData.items()): 1505 isValidValue = validateFontInfoVersion2ValueForAttribute(attr, value) 1506 if not isValidValue: 1507 raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value))) 1508 else: 1509 validInfoData[attr] = value 1510 return validInfoData 1511 1512def validateFontInfoVersion3ValueForAttribute(attr, value): 1513 """ 1514 This performs very basic validation of the value for attribute 1515 following the UFO 3 fontinfo.plist specification. The results 1516 of this should not be interpretted as *correct* for the font 1517 that they are part of. This merely indicates that the value 1518 is of the proper type and, where the specification defines 1519 a set range of possible values for an attribute, that the 1520 value is in the accepted range. 1521 """ 1522 dataValidationDict = fontInfoAttributesVersion3ValueData[attr] 1523 valueType = dataValidationDict.get("type") 1524 validator = dataValidationDict.get("valueValidator") 1525 valueOptions = dataValidationDict.get("valueOptions") 1526 # have specific options for the validator 1527 if valueOptions is not None: 1528 isValidValue = validator(value, valueOptions) 1529 # no specific options 1530 else: 1531 if validator == genericTypeValidator: 1532 isValidValue = validator(value, valueType) 1533 else: 1534 isValidValue = validator(value) 1535 return isValidValue 1536 1537def validateInfoVersion3Data(infoData): 1538 """ 1539 This performs very basic validation of the value for infoData 1540 following the UFO 3 fontinfo.plist specification. The results 1541 of this should not be interpretted as *correct* for the font 1542 that they are part of. This merely indicates that the values 1543 are of the proper type and, where the specification defines 1544 a set range of possible values for an attribute, that the 1545 value is in the accepted range. 1546 """ 1547 validInfoData = {} 1548 for attr, value in list(infoData.items()): 1549 isValidValue = validateFontInfoVersion3ValueForAttribute(attr, value) 1550 if not isValidValue: 1551 raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value))) 1552 else: 1553 validInfoData[attr] = value 1554 return validInfoData 1555 1556# Value Options 1557 1558fontInfoOpenTypeHeadFlagsOptions = list(range(0, 15)) 1559fontInfoOpenTypeOS2SelectionOptions = [1, 2, 3, 4, 7, 8, 9] 1560fontInfoOpenTypeOS2UnicodeRangesOptions = list(range(0, 128)) 1561fontInfoOpenTypeOS2CodePageRangesOptions = list(range(0, 64)) 1562fontInfoOpenTypeOS2TypeOptions = [0, 1, 2, 3, 8, 9] 1563 1564# Version Attribute Definitions 1565# This defines the attributes, types and, in some 1566# cases the possible values, that can exist is 1567# fontinfo.plist. 1568 1569fontInfoAttributesVersion1 = set([ 1570 "familyName", 1571 "styleName", 1572 "fullName", 1573 "fontName", 1574 "menuName", 1575 "fontStyle", 1576 "note", 1577 "versionMajor", 1578 "versionMinor", 1579 "year", 1580 "copyright", 1581 "notice", 1582 "trademark", 1583 "license", 1584 "licenseURL", 1585 "createdBy", 1586 "designer", 1587 "designerURL", 1588 "vendorURL", 1589 "unitsPerEm", 1590 "ascender", 1591 "descender", 1592 "capHeight", 1593 "xHeight", 1594 "defaultWidth", 1595 "slantAngle", 1596 "italicAngle", 1597 "widthName", 1598 "weightName", 1599 "weightValue", 1600 "fondName", 1601 "otFamilyName", 1602 "otStyleName", 1603 "otMacName", 1604 "msCharSet", 1605 "fondID", 1606 "uniqueID", 1607 "ttVendor", 1608 "ttUniqueID", 1609 "ttVersion", 1610]) 1611 1612fontInfoAttributesVersion2ValueData = { 1613 "familyName" : dict(type=basestring), 1614 "styleName" : dict(type=basestring), 1615 "styleMapFamilyName" : dict(type=basestring), 1616 "styleMapStyleName" : dict(type=basestring, valueValidator=fontInfoStyleMapStyleNameValidator), 1617 "versionMajor" : dict(type=int), 1618 "versionMinor" : dict(type=int), 1619 "year" : dict(type=int), 1620 "copyright" : dict(type=basestring), 1621 "trademark" : dict(type=basestring), 1622 "unitsPerEm" : dict(type=(int, float)), 1623 "descender" : dict(type=(int, float)), 1624 "xHeight" : dict(type=(int, float)), 1625 "capHeight" : dict(type=(int, float)), 1626 "ascender" : dict(type=(int, float)), 1627 "italicAngle" : dict(type=(float, int)), 1628 "note" : dict(type=basestring), 1629 "openTypeHeadCreated" : dict(type=basestring, valueValidator=fontInfoOpenTypeHeadCreatedValidator), 1630 "openTypeHeadLowestRecPPEM" : dict(type=(int, float)), 1631 "openTypeHeadFlags" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeHeadFlagsOptions), 1632 "openTypeHheaAscender" : dict(type=(int, float)), 1633 "openTypeHheaDescender" : dict(type=(int, float)), 1634 "openTypeHheaLineGap" : dict(type=(int, float)), 1635 "openTypeHheaCaretSlopeRise" : dict(type=int), 1636 "openTypeHheaCaretSlopeRun" : dict(type=int), 1637 "openTypeHheaCaretOffset" : dict(type=(int, float)), 1638 "openTypeNameDesigner" : dict(type=basestring), 1639 "openTypeNameDesignerURL" : dict(type=basestring), 1640 "openTypeNameManufacturer" : dict(type=basestring), 1641 "openTypeNameManufacturerURL" : dict(type=basestring), 1642 "openTypeNameLicense" : dict(type=basestring), 1643 "openTypeNameLicenseURL" : dict(type=basestring), 1644 "openTypeNameVersion" : dict(type=basestring), 1645 "openTypeNameUniqueID" : dict(type=basestring), 1646 "openTypeNameDescription" : dict(type=basestring), 1647 "openTypeNamePreferredFamilyName" : dict(type=basestring), 1648 "openTypeNamePreferredSubfamilyName" : dict(type=basestring), 1649 "openTypeNameCompatibleFullName" : dict(type=basestring), 1650 "openTypeNameSampleText" : dict(type=basestring), 1651 "openTypeNameWWSFamilyName" : dict(type=basestring), 1652 "openTypeNameWWSSubfamilyName" : dict(type=basestring), 1653 "openTypeOS2WidthClass" : dict(type=int, valueValidator=fontInfoOpenTypeOS2WidthClassValidator), 1654 "openTypeOS2WeightClass" : dict(type=int, valueValidator=fontInfoOpenTypeOS2WeightClassValidator), 1655 "openTypeOS2Selection" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2SelectionOptions), 1656 "openTypeOS2VendorID" : dict(type=basestring), 1657 "openTypeOS2Panose" : dict(type="integerList", valueValidator=fontInfoVersion2OpenTypeOS2PanoseValidator), 1658 "openTypeOS2FamilyClass" : dict(type="integerList", valueValidator=fontInfoOpenTypeOS2FamilyClassValidator), 1659 "openTypeOS2UnicodeRanges" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2UnicodeRangesOptions), 1660 "openTypeOS2CodePageRanges" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2CodePageRangesOptions), 1661 "openTypeOS2TypoAscender" : dict(type=(int, float)), 1662 "openTypeOS2TypoDescender" : dict(type=(int, float)), 1663 "openTypeOS2TypoLineGap" : dict(type=(int, float)), 1664 "openTypeOS2WinAscent" : dict(type=(int, float)), 1665 "openTypeOS2WinDescent" : dict(type=(int, float)), 1666 "openTypeOS2Type" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2TypeOptions), 1667 "openTypeOS2SubscriptXSize" : dict(type=(int, float)), 1668 "openTypeOS2SubscriptYSize" : dict(type=(int, float)), 1669 "openTypeOS2SubscriptXOffset" : dict(type=(int, float)), 1670 "openTypeOS2SubscriptYOffset" : dict(type=(int, float)), 1671 "openTypeOS2SuperscriptXSize" : dict(type=(int, float)), 1672 "openTypeOS2SuperscriptYSize" : dict(type=(int, float)), 1673 "openTypeOS2SuperscriptXOffset" : dict(type=(int, float)), 1674 "openTypeOS2SuperscriptYOffset" : dict(type=(int, float)), 1675 "openTypeOS2StrikeoutSize" : dict(type=(int, float)), 1676 "openTypeOS2StrikeoutPosition" : dict(type=(int, float)), 1677 "openTypeVheaVertTypoAscender" : dict(type=(int, float)), 1678 "openTypeVheaVertTypoDescender" : dict(type=(int, float)), 1679 "openTypeVheaVertTypoLineGap" : dict(type=(int, float)), 1680 "openTypeVheaCaretSlopeRise" : dict(type=int), 1681 "openTypeVheaCaretSlopeRun" : dict(type=int), 1682 "openTypeVheaCaretOffset" : dict(type=(int, float)), 1683 "postscriptFontName" : dict(type=basestring), 1684 "postscriptFullName" : dict(type=basestring), 1685 "postscriptSlantAngle" : dict(type=(float, int)), 1686 "postscriptUniqueID" : dict(type=int), 1687 "postscriptUnderlineThickness" : dict(type=(int, float)), 1688 "postscriptUnderlinePosition" : dict(type=(int, float)), 1689 "postscriptIsFixedPitch" : dict(type=bool), 1690 "postscriptBlueValues" : dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator), 1691 "postscriptOtherBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator), 1692 "postscriptFamilyBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator), 1693 "postscriptFamilyOtherBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator), 1694 "postscriptStemSnapH" : dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator), 1695 "postscriptStemSnapV" : dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator), 1696 "postscriptBlueFuzz" : dict(type=(int, float)), 1697 "postscriptBlueShift" : dict(type=(int, float)), 1698 "postscriptBlueScale" : dict(type=(float, int)), 1699 "postscriptForceBold" : dict(type=bool), 1700 "postscriptDefaultWidthX" : dict(type=(int, float)), 1701 "postscriptNominalWidthX" : dict(type=(int, float)), 1702 "postscriptWeightName" : dict(type=basestring), 1703 "postscriptDefaultCharacter" : dict(type=basestring), 1704 "postscriptWindowsCharacterSet" : dict(type=int, valueValidator=fontInfoPostscriptWindowsCharacterSetValidator), 1705 "macintoshFONDFamilyID" : dict(type=int), 1706 "macintoshFONDName" : dict(type=basestring), 1707} 1708fontInfoAttributesVersion2 = set(fontInfoAttributesVersion2ValueData.keys()) 1709 1710fontInfoAttributesVersion3ValueData = deepcopy(fontInfoAttributesVersion2ValueData) 1711fontInfoAttributesVersion3ValueData.update({ 1712 "versionMinor" : dict(type=int, valueValidator=genericNonNegativeIntValidator), 1713 "unitsPerEm" : dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator), 1714 "openTypeHeadLowestRecPPEM" : dict(type=int, valueValidator=genericNonNegativeNumberValidator), 1715 "openTypeHheaAscender" : dict(type=int), 1716 "openTypeHheaDescender" : dict(type=int), 1717 "openTypeHheaLineGap" : dict(type=int), 1718 "openTypeHheaCaretOffset" : dict(type=int), 1719 "openTypeOS2Panose" : dict(type="integerList", valueValidator=fontInfoVersion3OpenTypeOS2PanoseValidator), 1720 "openTypeOS2TypoAscender" : dict(type=int), 1721 "openTypeOS2TypoDescender" : dict(type=int), 1722 "openTypeOS2TypoLineGap" : dict(type=int), 1723 "openTypeOS2WinAscent" : dict(type=int, valueValidator=genericNonNegativeNumberValidator), 1724 "openTypeOS2WinDescent" : dict(type=int, valueValidator=genericNonNegativeNumberValidator), 1725 "openTypeOS2SubscriptXSize" : dict(type=int), 1726 "openTypeOS2SubscriptYSize" : dict(type=int), 1727 "openTypeOS2SubscriptXOffset" : dict(type=int), 1728 "openTypeOS2SubscriptYOffset" : dict(type=int), 1729 "openTypeOS2SuperscriptXSize" : dict(type=int), 1730 "openTypeOS2SuperscriptYSize" : dict(type=int), 1731 "openTypeOS2SuperscriptXOffset" : dict(type=int), 1732 "openTypeOS2SuperscriptYOffset" : dict(type=int), 1733 "openTypeOS2StrikeoutSize" : dict(type=int), 1734 "openTypeOS2StrikeoutPosition" : dict(type=int), 1735 "openTypeGaspRangeRecords" : dict(type="dictList", valueValidator=fontInfoOpenTypeGaspRangeRecordsValidator), 1736 "openTypeNameRecords" : dict(type="dictList", valueValidator=fontInfoOpenTypeNameRecordsValidator), 1737 "openTypeVheaVertTypoAscender" : dict(type=int), 1738 "openTypeVheaVertTypoDescender" : dict(type=int), 1739 "openTypeVheaVertTypoLineGap" : dict(type=int), 1740 "openTypeVheaCaretOffset" : dict(type=int), 1741 "woffMajorVersion" : dict(type=int, valueValidator=genericNonNegativeIntValidator), 1742 "woffMinorVersion" : dict(type=int, valueValidator=genericNonNegativeIntValidator), 1743 "woffMetadataUniqueID" : dict(type=dict, valueValidator=fontInfoWOFFMetadataUniqueIDValidator), 1744 "woffMetadataVendor" : dict(type=dict, valueValidator=fontInfoWOFFMetadataVendorValidator), 1745 "woffMetadataCredits" : dict(type=dict, valueValidator=fontInfoWOFFMetadataCreditsValidator), 1746 "woffMetadataDescription" : dict(type=dict, valueValidator=fontInfoWOFFMetadataDescriptionValidator), 1747 "woffMetadataLicense" : dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseValidator), 1748 "woffMetadataCopyright" : dict(type=dict, valueValidator=fontInfoWOFFMetadataCopyrightValidator), 1749 "woffMetadataTrademark" : dict(type=dict, valueValidator=fontInfoWOFFMetadataTrademarkValidator), 1750 "woffMetadataLicensee" : dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseeValidator), 1751 "woffMetadataExtensions" : dict(type=list, valueValidator=fontInfoWOFFMetadataExtensionsValidator), 1752 "guidelines" : dict(type=list, valueValidator=guidelinesValidator) 1753}) 1754fontInfoAttributesVersion3 = set(fontInfoAttributesVersion3ValueData.keys()) 1755 1756# insert the type validator for all attrs that 1757# have no defined validator. 1758for attr, dataDict in list(fontInfoAttributesVersion2ValueData.items()): 1759 if "valueValidator" not in dataDict: 1760 dataDict["valueValidator"] = genericTypeValidator 1761 1762for attr, dataDict in list(fontInfoAttributesVersion3ValueData.items()): 1763 if "valueValidator" not in dataDict: 1764 dataDict["valueValidator"] = genericTypeValidator 1765 1766# Version Conversion Support 1767# These are used from converting from version 1 1768# to version 2 or vice-versa. 1769 1770def _flipDict(d): 1771 flipped = {} 1772 for key, value in list(d.items()): 1773 flipped[value] = key 1774 return flipped 1775 1776fontInfoAttributesVersion1To2 = { 1777 "menuName" : "styleMapFamilyName", 1778 "designer" : "openTypeNameDesigner", 1779 "designerURL" : "openTypeNameDesignerURL", 1780 "createdBy" : "openTypeNameManufacturer", 1781 "vendorURL" : "openTypeNameManufacturerURL", 1782 "license" : "openTypeNameLicense", 1783 "licenseURL" : "openTypeNameLicenseURL", 1784 "ttVersion" : "openTypeNameVersion", 1785 "ttUniqueID" : "openTypeNameUniqueID", 1786 "notice" : "openTypeNameDescription", 1787 "otFamilyName" : "openTypeNamePreferredFamilyName", 1788 "otStyleName" : "openTypeNamePreferredSubfamilyName", 1789 "otMacName" : "openTypeNameCompatibleFullName", 1790 "weightName" : "postscriptWeightName", 1791 "weightValue" : "openTypeOS2WeightClass", 1792 "ttVendor" : "openTypeOS2VendorID", 1793 "uniqueID" : "postscriptUniqueID", 1794 "fontName" : "postscriptFontName", 1795 "fondID" : "macintoshFONDFamilyID", 1796 "fondName" : "macintoshFONDName", 1797 "defaultWidth" : "postscriptDefaultWidthX", 1798 "slantAngle" : "postscriptSlantAngle", 1799 "fullName" : "postscriptFullName", 1800 # require special value conversion 1801 "fontStyle" : "styleMapStyleName", 1802 "widthName" : "openTypeOS2WidthClass", 1803 "msCharSet" : "postscriptWindowsCharacterSet" 1804} 1805fontInfoAttributesVersion2To1 = _flipDict(fontInfoAttributesVersion1To2) 1806deprecatedFontInfoAttributesVersion2 = set(fontInfoAttributesVersion1To2.keys()) 1807 1808_fontStyle1To2 = { 1809 64 : "regular", 1810 1 : "italic", 1811 32 : "bold", 1812 33 : "bold italic" 1813} 1814_fontStyle2To1 = _flipDict(_fontStyle1To2) 1815# Some UFO 1 files have 0 1816_fontStyle1To2[0] = "regular" 1817 1818_widthName1To2 = { 1819 "Ultra-condensed" : 1, 1820 "Extra-condensed" : 2, 1821 "Condensed" : 3, 1822 "Semi-condensed" : 4, 1823 "Medium (normal)" : 5, 1824 "Semi-expanded" : 6, 1825 "Expanded" : 7, 1826 "Extra-expanded" : 8, 1827 "Ultra-expanded" : 9 1828} 1829_widthName2To1 = _flipDict(_widthName1To2) 1830# FontLab's default width value is "Normal". 1831# Many format version 1 UFOs will have this. 1832_widthName1To2["Normal"] = 5 1833# FontLab has an "All" width value. In UFO 1 1834# move this up to "Normal". 1835_widthName1To2["All"] = 5 1836# "medium" appears in a lot of UFO 1 files. 1837_widthName1To2["medium"] = 5 1838# "Medium" appears in a lot of UFO 1 files. 1839_widthName1To2["Medium"] = 5 1840 1841_msCharSet1To2 = { 1842 0 : 1, 1843 1 : 2, 1844 2 : 3, 1845 77 : 4, 1846 128 : 5, 1847 129 : 6, 1848 130 : 7, 1849 134 : 8, 1850 136 : 9, 1851 161 : 10, 1852 162 : 11, 1853 163 : 12, 1854 177 : 13, 1855 178 : 14, 1856 186 : 15, 1857 200 : 16, 1858 204 : 17, 1859 222 : 18, 1860 238 : 19, 1861 255 : 20 1862} 1863_msCharSet2To1 = _flipDict(_msCharSet1To2) 1864 1865# 1 <-> 2 1866 1867def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value): 1868 """ 1869 Convert value from version 1 to version 2 format. 1870 Returns the new attribute name and the converted value. 1871 If the value is None, None will be returned for the new value. 1872 """ 1873 # convert floats to ints if possible 1874 if isinstance(value, float): 1875 if int(value) == value: 1876 value = int(value) 1877 if value is not None: 1878 if attr == "fontStyle": 1879 v = _fontStyle1To2.get(value) 1880 if v is None: 1881 raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr)) 1882 value = v 1883 elif attr == "widthName": 1884 v = _widthName1To2.get(value) 1885 if v is None: 1886 raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr)) 1887 value = v 1888 elif attr == "msCharSet": 1889 v = _msCharSet1To2.get(value) 1890 if v is None: 1891 raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr)) 1892 value = v 1893 attr = fontInfoAttributesVersion1To2.get(attr, attr) 1894 return attr, value 1895 1896def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value): 1897 """ 1898 Convert value from version 2 to version 1 format. 1899 Returns the new attribute name and the converted value. 1900 If the value is None, None will be returned for the new value. 1901 """ 1902 if value is not None: 1903 if attr == "styleMapStyleName": 1904 value = _fontStyle2To1.get(value) 1905 elif attr == "openTypeOS2WidthClass": 1906 value = _widthName2To1.get(value) 1907 elif attr == "postscriptWindowsCharacterSet": 1908 value = _msCharSet2To1.get(value) 1909 attr = fontInfoAttributesVersion2To1.get(attr, attr) 1910 return attr, value 1911 1912def _convertFontInfoDataVersion1ToVersion2(data): 1913 converted = {} 1914 for attr, value in list(data.items()): 1915 # FontLab gives -1 for the weightValue 1916 # for fonts wil no defined value. Many 1917 # format version 1 UFOs will have this. 1918 if attr == "weightValue" and value == -1: 1919 continue 1920 newAttr, newValue = convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value) 1921 # skip if the attribute is not part of version 2 1922 if newAttr not in fontInfoAttributesVersion2: 1923 continue 1924 # catch values that can't be converted 1925 if value is None: 1926 raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr)) 1927 # store 1928 converted[newAttr] = newValue 1929 return converted 1930 1931def _convertFontInfoDataVersion2ToVersion1(data): 1932 converted = {} 1933 for attr, value in list(data.items()): 1934 newAttr, newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value) 1935 # only take attributes that are registered for version 1 1936 if newAttr not in fontInfoAttributesVersion1: 1937 continue 1938 # catch values that can't be converted 1939 if value is None: 1940 raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr)) 1941 # store 1942 converted[newAttr] = newValue 1943 return converted 1944 1945# 2 <-> 3 1946 1947_ufo2To3NonNegativeInt = set(( 1948 "versionMinor", 1949 "openTypeHeadLowestRecPPEM", 1950 "openTypeOS2WinAscent", 1951 "openTypeOS2WinDescent" 1952)) 1953_ufo2To3NonNegativeIntOrFloat = set(( 1954 "unitsPerEm" 1955)) 1956_ufo2To3FloatToInt = set((( 1957 "openTypeHeadLowestRecPPEM", 1958 "openTypeHheaAscender", 1959 "openTypeHheaDescender", 1960 "openTypeHheaLineGap", 1961 "openTypeHheaCaretOffset", 1962 "openTypeOS2TypoAscender", 1963 "openTypeOS2TypoDescender", 1964 "openTypeOS2TypoLineGap", 1965 "openTypeOS2WinAscent", 1966 "openTypeOS2WinDescent", 1967 "openTypeOS2SubscriptXSize", 1968 "openTypeOS2SubscriptYSize", 1969 "openTypeOS2SubscriptXOffset", 1970 "openTypeOS2SubscriptYOffset", 1971 "openTypeOS2SuperscriptXSize", 1972 "openTypeOS2SuperscriptYSize", 1973 "openTypeOS2SuperscriptXOffset", 1974 "openTypeOS2SuperscriptYOffset", 1975 "openTypeOS2StrikeoutSize", 1976 "openTypeOS2StrikeoutPosition", 1977 "openTypeVheaVertTypoAscender", 1978 "openTypeVheaVertTypoDescender", 1979 "openTypeVheaVertTypoLineGap", 1980 "openTypeVheaCaretOffset" 1981))) 1982 1983def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value): 1984 """ 1985 Convert value from version 2 to version 3 format. 1986 Returns the new attribute name and the converted value. 1987 If the value is None, None will be returned for the new value. 1988 """ 1989 if attr in _ufo2To3FloatToInt: 1990 try: 1991 v = int(round(value)) 1992 except (ValueError, TypeError): 1993 raise UFOLibError("Could not convert value for %s." % attr) 1994 if v != value: 1995 value = v 1996 if attr in _ufo2To3NonNegativeInt: 1997 try: 1998 v = int(abs(value)) 1999 except (ValueError, TypeError): 2000 raise UFOLibError("Could not convert value for %s." % attr) 2001 if v != value: 2002 value = v 2003 elif attr in _ufo2To3NonNegativeIntOrFloat: 2004 try: 2005 v = float(abs(value)) 2006 except (ValueError, TypeError): 2007 raise UFOLibError("Could not convert value for %s." % attr) 2008 if v == int(v): 2009 v = int(v) 2010 if v != value: 2011 value = v 2012 return attr, value 2013 2014def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value): 2015 """ 2016 Convert value from version 3 to version 2 format. 2017 Returns the new attribute name and the converted value. 2018 If the value is None, None will be returned for the new value. 2019 """ 2020 return attr, value 2021 2022def _convertFontInfoDataVersion3ToVersion2(data): 2023 converted = {} 2024 for attr, value in list(data.items()): 2025 newAttr, newValue = convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value) 2026 if newAttr not in fontInfoAttributesVersion2: 2027 continue 2028 converted[newAttr] = newValue 2029 return converted 2030 2031def _convertFontInfoDataVersion2ToVersion3(data): 2032 converted = {} 2033 for attr, value in list(data.items()): 2034 attr, value = convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value) 2035 converted[attr] = value 2036 return converted 2037 2038if __name__ == "__main__": 2039 import doctest 2040 doctest.testmod() 2041