├── .gitignore ├── README.md ├── fileStructureTests ├── core │ ├── __init__.py │ ├── environment.py │ ├── fileNames.py │ ├── fileSystem.py │ ├── fonts.py │ ├── fonts.txt │ ├── glyphTree.py │ ├── objects.py │ ├── plistTree.py │ ├── ufoReaderWriter.py │ └── xmlUtilities.py ├── singleXML.py ├── sqlite.py ├── testAll.py ├── ufo3.py └── ufo3zip.py └── fontSizeReporter ├── UFOStats.py └── results.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | fileStructureTests/dropbox.txt 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UFO 4 Research 2 | 3 | UFO 4 *research* is being conducted here. None of this is official or an example implementation. 4 | 5 | ## File Structure Tests 6 | 7 | Possible file structures are currently being researched. At the moment, the following are being considered: 8 | 9 | 1. Zipped version of the current UFO 3 file structure 10 | 2. Single XML file. 11 | 3. Package file structure similar to the UFO 3 file structure but with the glyph sub-directories flattened into single XML files. 12 | 4. Zipped version of 3. 13 | 5. Package file structure similar to the UFO 3 file structure but with the glyphs in the glyph sub-directories grouped into chunks rather than one file per glyph. 14 | 6. Zipped version of 5. 15 | 7. A very simple SQLite database. The initial idea is to use the existing UFO 3 structure, but instead of writing to paths, the files would be stored in something like a key value with the relative paths indicating the file location in the UFO 3 package structe as the keys and the current XML formats as the values. Additionally, modification times for each of these could be stored. 16 | 17 | ### Some research links: 18 | - [SQLite as file format.](https://www.sqlite.org/appfileformat.html) 19 | - [SQLite Case Study - Approaches to structuring SQLite-as-file-format DBs](https://www.sqlite.org/affcase1.html) 20 | - [Some notes on SQLite corruption](https://www.sqlite.org/howtocorrupt.html) 21 | - [SQlite databases under git](http://ongardie.net/blog/sqlite-in-git/) 22 | - Fontlab have introduced an `.ufoz` format (that basically is 1. in the list above) as mentioned in this [blogpost](http://blog.fontlab.com/font-utility/vfb2ufo/) 23 | 24 | ### Test Implementation 25 | 26 | The tests are likely going to be structured as a common UFO reader/writer that works with an abstract "file system" API. Basically, this API will enable the reader/writer to interact with the various file structure proposals without knowing the internal details of the file structures. Each test will be implemented as one of these file structures following the abstract API. Given that the "file systems" will be the only part of the code that varies from test to test, this should give us a very clear picture of the read/write effeciency and ease of implementation of the various proposals. 27 | 28 | The current effort is going into building a file system wrapper that works with the existing UFO 3 file structure. 29 | 30 | The UFO reader/writer and GLIF interpretation were initially forked from RoboFab's UFO 3 version of ufoLib. These have been greatly simplified for these tests. Validation of the data has been eliminated to reduce the number of moving parts. Therefore, all incoming data must be perfectly compliant with the UFO 3 specification. The XML parsing, both GLIF and plist, is now handled exclusively by the Python Standard Library's version of cElementTree. 31 | -------------------------------------------------------------------------------- /fileStructureTests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unified-font-object/ufo4Research/25d52b1c102cc8a0a1b825904415311e4f4b5b2d/fileStructureTests/core/__init__.py -------------------------------------------------------------------------------- /fileStructureTests/core/environment.py: -------------------------------------------------------------------------------- 1 | from xml.etree import cElementTree as ET -------------------------------------------------------------------------------- /fileStructureTests/core/fileNames.py: -------------------------------------------------------------------------------- 1 | illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ") 2 | illegalCharacters += [chr(i) for i in range(1, 32)] 3 | illegalCharacters += [chr(0x7F)] 4 | reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") 5 | reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") 6 | maxFileNameLength = 255 7 | 8 | def userNameToFileName(userName, existing=[], prefix="", suffix=""): 9 | """ 10 | existing should be a case-insensitive list 11 | of all existing file names. 12 | 13 | >>> userNameToFileName(u"a") 14 | u'a' 15 | >>> userNameToFileName(u"A") 16 | u'A_' 17 | >>> userNameToFileName(u"AE") 18 | u'A_E_' 19 | >>> userNameToFileName(u"Ae") 20 | u'A_e' 21 | >>> userNameToFileName(u"ae") 22 | u'ae' 23 | >>> userNameToFileName(u"aE") 24 | u'aE_' 25 | >>> userNameToFileName(u"a.alt") 26 | u'a.alt' 27 | >>> userNameToFileName(u"A.alt") 28 | u'A_.alt' 29 | >>> userNameToFileName(u"A.Alt") 30 | u'A_.A_lt' 31 | >>> userNameToFileName(u"A.aLt") 32 | u'A_.aL_t' 33 | >>> userNameToFileName(u"A.alT") 34 | u'A_.alT_' 35 | >>> userNameToFileName(u"T_H") 36 | u'T__H_' 37 | >>> userNameToFileName(u"T_h") 38 | u'T__h' 39 | >>> userNameToFileName(u"t_h") 40 | u't_h' 41 | >>> userNameToFileName(u"F_F_I") 42 | u'F__F__I_' 43 | >>> userNameToFileName(u"f_f_i") 44 | u'f_f_i' 45 | >>> userNameToFileName(u"Aacute_V.swash") 46 | u'A_acute_V_.swash' 47 | >>> userNameToFileName(u".notdef") 48 | u'_notdef' 49 | >>> userNameToFileName(u"con") 50 | u'_con' 51 | >>> userNameToFileName(u"CON") 52 | u'C_O_N_' 53 | >>> userNameToFileName(u"con.alt") 54 | u'_con.alt' 55 | >>> userNameToFileName(u"alt.con") 56 | u'alt._con' 57 | """ 58 | # the incoming name must be a unicode string 59 | assert isinstance(userName, unicode), "The value for userName must be a unicode string." 60 | # establish the prefix and suffix lengths 61 | prefixLength = len(prefix) 62 | suffixLength = len(suffix) 63 | # replace an initial period with an _ 64 | # if no prefix is to be added 65 | if not prefix and userName[0] == ".": 66 | userName = "_" + userName[1:] 67 | # filter the user name 68 | filteredUserName = [] 69 | for character in userName: 70 | # replace illegal characters with _ 71 | if character in illegalCharacters: 72 | character = "_" 73 | # add _ to all non-lower characters 74 | elif character != character.lower(): 75 | character += "_" 76 | filteredUserName.append(character) 77 | userName = "".join(filteredUserName) 78 | # clip to 255 79 | sliceLength = maxFileNameLength - prefixLength - suffixLength 80 | userName = userName[:sliceLength] 81 | # test for illegal files names 82 | parts = [] 83 | for part in userName.split("."): 84 | if part.lower() in reservedFileNames: 85 | part = "_" + part 86 | parts.append(part) 87 | userName = ".".join(parts) 88 | # test for clash 89 | fullName = prefix + userName + suffix 90 | if fullName.lower() in existing: 91 | fullName = handleClash1(userName, existing, prefix, suffix) 92 | # finished 93 | return fullName 94 | 95 | def handleClash1(userName, existing=[], prefix="", suffix=""): 96 | """ 97 | existing should be a case-insensitive list 98 | of all existing file names. 99 | 100 | >>> prefix = ("0" * 5) + "." 101 | >>> suffix = "." + ("0" * 10) 102 | >>> existing = ["a" * 5] 103 | 104 | >>> e = list(existing) 105 | >>> handleClash1(userName="A" * 5, existing=e, 106 | ... prefix=prefix, suffix=suffix) 107 | '00000.AAAAA000000000000001.0000000000' 108 | 109 | >>> e = list(existing) 110 | >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) 111 | >>> handleClash1(userName="A" * 5, existing=e, 112 | ... prefix=prefix, suffix=suffix) 113 | '00000.AAAAA000000000000002.0000000000' 114 | 115 | >>> e = list(existing) 116 | >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) 117 | >>> handleClash1(userName="A" * 5, existing=e, 118 | ... prefix=prefix, suffix=suffix) 119 | '00000.AAAAA000000000000001.0000000000' 120 | """ 121 | # if the prefix length + user name length + suffix length + 15 is at 122 | # or past the maximum length, silce 15 characters off of the user name 123 | prefixLength = len(prefix) 124 | suffixLength = len(suffix) 125 | if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: 126 | l = (prefixLength + len(userName) + suffixLength + 15) 127 | sliceLength = maxFileNameLength - l 128 | userName = userName[:sliceLength] 129 | finalName = None 130 | # try to add numbers to create a unique name 131 | counter = 1 132 | while finalName is None: 133 | name = userName + str(counter).zfill(15) 134 | fullName = prefix + name + suffix 135 | if fullName.lower() not in existing: 136 | finalName = fullName 137 | break 138 | else: 139 | counter += 1 140 | if counter >= 999999999999999: 141 | break 142 | # if there is a clash, go to the next fallback 143 | if finalName is None: 144 | finalName = handleClash2(existing, prefix, suffix) 145 | # finished 146 | return finalName 147 | 148 | def handleClash2(existing=[], prefix="", suffix=""): 149 | """ 150 | existing should be a case-insensitive list 151 | of all existing file names. 152 | 153 | >>> prefix = ("0" * 5) + "." 154 | >>> suffix = "." + ("0" * 10) 155 | >>> existing = [prefix + str(i) + suffix for i in range(100)] 156 | 157 | >>> e = list(existing) 158 | >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) 159 | '00000.100.0000000000' 160 | 161 | >>> e = list(existing) 162 | >>> e.remove(prefix + "1" + suffix) 163 | >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) 164 | '00000.1.0000000000' 165 | 166 | >>> e = list(existing) 167 | >>> e.remove(prefix + "2" + suffix) 168 | >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) 169 | '00000.2.0000000000' 170 | """ 171 | # calculate the longest possible string 172 | maxLength = maxFileNameLength - len(prefix) - len(suffix) 173 | maxValue = int("9" * maxLength) 174 | # try to find a number 175 | finalName = None 176 | counter = 1 177 | while finalName is None: 178 | fullName = prefix + str(counter) + suffix 179 | if fullName.lower() not in existing: 180 | finalName = fullName 181 | break 182 | else: 183 | counter += 1 184 | if counter >= maxValue: 185 | break 186 | # raise an error if nothing has been found 187 | if finalName is None: 188 | raise NameTranslationError("No unique name could be found.") 189 | # finished 190 | return finalName -------------------------------------------------------------------------------- /fileStructureTests/core/fileSystem.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from environment import ET 3 | from xmlUtilities import treeToString 4 | from plistTree import convertTreeToPlist, convertPlistToTree, plistHeader 5 | from fileNames import userNameToFileName 6 | 7 | 8 | class FileSystemError(Exception): pass 9 | 10 | 11 | class BaseFileSystem(object): 12 | 13 | """ 14 | This implements the base file system functionality. 15 | 16 | 17 | # Locations and Names 18 | Locations are the place that given data should be stored. 19 | Names are the names of particular bits of data. For example, 20 | the glyph "A" in the default layer will have the name: 21 | 22 | A_.glif 23 | 24 | And the location: 25 | 26 | glyphs/A_.glif 27 | 28 | Locations are always relative to the top level of the UFO. 29 | 30 | 31 | # Trees 32 | All incoming data must be and all outgoing data will 33 | be instances of ElementTree Elements. 34 | 35 | # Subclasses 36 | Subclasses must override the base methods where indicated. 37 | Some methods may be overriden where indicated. All other 38 | methods must not be overriden. 39 | """ 40 | 41 | fileExtension = "base" 42 | 43 | def __init__(self): 44 | self._haveReadLayerStorageMapping = False 45 | self._layerStorageMapping = OrderedDict() 46 | self._glyphStorageMapping = {} 47 | self._defaultLayerName = None 48 | 49 | def close(self): 50 | """ 51 | Close the file system. This must be called 52 | when any write operations are complete. 53 | 54 | Subclasses MAY override this method. 55 | """ 56 | pass 57 | 58 | # ------------ 59 | # File Support 60 | # ------------ 61 | 62 | # locations 63 | 64 | def joinLocations(self, location1, *location2): 65 | """ 66 | Return location1 and location2 joined by the 67 | appropriate separator. This behaves exactly 68 | like os.path.join. 69 | 70 | Subclasses MAY override this method. 71 | """ 72 | # taken from the Python Standard Library's posixpath.py 73 | a = location1 74 | p = location2 75 | path = a 76 | for b in p: 77 | if b.startswith('/'): 78 | path = b 79 | elif path == '' or path.endswith('/'): 80 | path += b 81 | else: 82 | path += '/' + b 83 | return path 84 | 85 | def splitLocation(self, location): 86 | """ 87 | Split location into a directory and basename. 88 | This behaves exactly like os.path.split. 89 | 90 | Subclasses MAY override this method. 91 | """ 92 | # taken from the Python Standard Library's posixpath.py 93 | p = location 94 | i = p.rfind('/') + 1 95 | head, tail = p[:i], p[i:] 96 | if head and head != '/'*len(head): 97 | head = head.rstrip('/') 98 | return head, tail 99 | 100 | # bytes <-> location 101 | 102 | def readBytesFromLocation(self, location): 103 | """ 104 | Read the data from the given location. 105 | If the location does not exist, this will return None. 106 | 107 | Subclasses MUST override this method. 108 | """ 109 | raise NotImplementedError 110 | 111 | def writeBytesToLocation(self, data, location): 112 | """ 113 | Write the given data into the given location. 114 | 115 | Subclasses MUST override this method. 116 | """ 117 | raise NotImplementedError 118 | 119 | # plist <-> location 120 | 121 | def readPlistFromLocation(self, location): 122 | """ 123 | Read a property list from the given location. 124 | 125 | Subclasses MUST NOT override this method. 126 | """ 127 | tree = self.readTreeFromLocation(location) 128 | if tree is None: 129 | return None 130 | try: 131 | return convertTreeToPlist(tree) 132 | except: 133 | raise FileSystemError("The file %s could not be read." % location) 134 | 135 | def writePlistToLocation(self, data, location): 136 | """ 137 | Write the given data into the given location 138 | in property list format. 139 | 140 | Subclasses MUST NOT override this method. 141 | """ 142 | tree = convertPlistToTree(data) 143 | self.writeTreeToLocation(tree, location, header=plistHeader) 144 | 145 | # tree <-> location 146 | 147 | def readTreeFromLocation(self, location): 148 | """ 149 | Read an XML tree from the given location. 150 | 151 | Subclasses MAY override this method. 152 | """ 153 | data = self.readBytesFromLocation(location) 154 | if data is None: 155 | return None 156 | tree = self.convertBytesToTree(data) 157 | return tree 158 | 159 | def writeTreeToLocation(self, tree, location, header=None): 160 | """ 161 | Write the given tree into the given location. 162 | If header is given, it will be inserted at the 163 | beginning of the XML string. 164 | 165 | Subclasses MAY override this method. 166 | """ 167 | data = self.convertTreeToBytes(tree, header) 168 | self.writeBytesToLocation(data, location) 169 | 170 | # bytes <-> tree 171 | 172 | def convertBytesToTree(self, data): 173 | """ 174 | Read an XML tree from the given string. 175 | 176 | Subclasses MUST NOT override this method. 177 | """ 178 | if data is None: 179 | return None 180 | tree = ET.fromstring(data) 181 | return tree 182 | 183 | def convertTreeToBytes(self, tree, header=None): 184 | """ 185 | Write the given tree to a string. If header 186 | is given, it will be inserted at the beginning 187 | of the XML string. 188 | 189 | Subclasses MUST NOT override this method. 190 | """ 191 | return treeToString(tree, header) 192 | 193 | # --------------- 194 | # Top Level Files 195 | # --------------- 196 | 197 | def readMetaInfo(self): 198 | """ 199 | Read the meta info to a tree. 200 | 201 | Subclasses MAY override this method. 202 | """ 203 | return self.readPlistFromLocation("metainfo.plist") 204 | 205 | def writeMetaInfo(self, tree): 206 | """ 207 | Write the meta info from a tree. 208 | 209 | Subclasses MAY override this method. 210 | """ 211 | self.writePlistToLocation(tree, "metainfo.plist") 212 | 213 | def readFontInfo(self): 214 | """ 215 | Read the font info to a tree. 216 | 217 | Subclasses MAY override this method. 218 | """ 219 | return self.readPlistFromLocation("fontinfo.plist") 220 | 221 | def writeFontInfo(self, tree): 222 | """ 223 | Write the font info from a tree. 224 | 225 | Subclasses MAY override this method. 226 | """ 227 | self.writePlistToLocation(tree, "fontinfo.plist") 228 | 229 | def readGroups(self): 230 | """ 231 | Read the groups to a tree. 232 | 233 | Subclasses MAY override this method. 234 | """ 235 | return self.readPlistFromLocation("groups.plist") 236 | 237 | def writeGroups(self, tree): 238 | """ 239 | Write the groups from a tree. 240 | 241 | Subclasses MAY override this method. 242 | """ 243 | self.writePlistToLocation(tree, "groups.plist") 244 | 245 | def readKerning(self): 246 | """ 247 | Read the kerning to a tree. 248 | 249 | Subclasses MAY override this method. 250 | """ 251 | return self.readPlistFromLocation("kerning.plist") 252 | 253 | def writeKerning(self, tree): 254 | """ 255 | Write the kerning from a tree. 256 | 257 | Subclasses MAY override this method. 258 | """ 259 | self.writePlistToLocation(tree, "kerning.plist") 260 | 261 | def readFeatures(self): 262 | """ 263 | Read the features to a string. 264 | 265 | Subclasses MAY override this method. 266 | """ 267 | return self.readBytesFromLocation("features.fea") 268 | 269 | def writeFeatures(self, tree): 270 | """ 271 | Write the features from a string. 272 | 273 | Subclasses MAY override this method. 274 | """ 275 | self.writeBytesToLocation(tree, "features.fea") 276 | 277 | def readLib(self): 278 | """ 279 | Read the lib to a tree. 280 | 281 | Subclasses MAY override this method. 282 | """ 283 | return self.readPlistFromLocation("lib.plist") 284 | 285 | def writeLib(self, tree): 286 | """ 287 | Write the lib from a tree. 288 | 289 | Subclasses MAY override this method. 290 | """ 291 | self.writePlistToLocation(tree, "lib.plist") 292 | 293 | # ----------------- 294 | # Layers and Glyphs 295 | # ----------------- 296 | 297 | # layers 298 | 299 | def readLayerContents(self): 300 | """ 301 | Read the layer contents mapping and return an OrderedDict of form: 302 | 303 | { 304 | layer name : storage name 305 | } 306 | 307 | Subclasses MAY override this method. 308 | """ 309 | raw = self.readPlistFromLocation("layercontents.plist") 310 | data = OrderedDict() 311 | if raw is not None: 312 | for layerName, storageName in raw: 313 | data[layerName] = storageName 314 | return data 315 | 316 | def writeLayerContents(self): 317 | """ 318 | Write the layer contents mapping. 319 | 320 | Subclasses MAY override this method. 321 | """ 322 | data = self.getLayerStorageMapping() 323 | data = [(k, v) for k, v in data.items()] 324 | self.writePlistToLocation(data, "layercontents.plist") 325 | 326 | def getLayerStorageMapping(self): 327 | """ 328 | Get the layer contents mapping as a dict of form: 329 | 330 | { 331 | layer name : storage name 332 | } 333 | 334 | Subclasses MUST NOT override this method. 335 | """ 336 | if not self._haveReadLayerStorageMapping: 337 | self._layerStorageMapping = self.readLayerContents() 338 | self._haveReadLayerStorageMapping = True 339 | return self._layerStorageMapping 340 | 341 | def getLayerStorageName(self, layerName): 342 | """ 343 | Get the storage name for the given layer name. 344 | If none is defined, one will be created using the 345 | UFO 3 user name to file name algorithm. 346 | 347 | Subclasses MUST NOT override this method. 348 | """ 349 | layerStorageMapping = self.getLayerStorageMapping() 350 | if layerName not in layerStorageMapping: 351 | if layerName == self.getDefaultLayerName(): 352 | storageName = "glyphs" 353 | else: 354 | layerName = unicode(layerName) 355 | storageName = userNameToFileName(layerName, existing=layerStorageMapping.values(), prefix="glyphs.") 356 | layerStorageMapping[layerName] = storageName 357 | return layerStorageMapping[layerName] 358 | 359 | def getLayerNames(self): 360 | """ 361 | Get a list of all layer names, in order. 362 | 363 | Subclasses MUST NOT override this method. 364 | """ 365 | layerStorageMapping = self.getLayerStorageMapping() 366 | return layerStorageMapping.keys() 367 | 368 | def getDefaultLayerName(self): 369 | """ 370 | Get the default layer name. 371 | 372 | Subclasses MUST NOT override this method. 373 | """ 374 | if self._defaultLayerName is None: 375 | default = "public.default" 376 | for layerName, layerDirectory in self.getLayerStorageMapping(): 377 | if layerDirectory == "glyphs": 378 | default = layerName 379 | break 380 | self._defaultLayerName = default 381 | return self._defaultLayerName 382 | 383 | def setDefaultLayerName(self, layerName): 384 | """ 385 | Set the default layer name. 386 | 387 | Subclasses MUST NOT override this method. 388 | """ 389 | self._defaultLayerName = layerName 390 | 391 | # glyphs 392 | 393 | def readGlyphSetContents(self, layerName): 394 | """ 395 | Read the glyph set contents mapping for the given layer name. 396 | 397 | Subclasses MAY override this method. 398 | """ 399 | layerDirectory = self.getLayerStorageName(layerName) 400 | path = self.joinLocations(layerDirectory, "contents.plist") 401 | data = self.readPlistFromLocation(path) 402 | if data is None: 403 | data = {} 404 | return data 405 | 406 | def writeGlyphSetContents(self, layerName): 407 | """ 408 | Write the glyph set contents mapping for the given layer name. 409 | 410 | Subclasses MAY override this method. 411 | """ 412 | layerDirectory = self.getLayerStorageName(layerName) 413 | path = self.joinLocations(layerDirectory, "contents.plist") 414 | data = self.getGlyphStorageMapping(layerName) 415 | self.writePlistToLocation(data, path) 416 | 417 | def getGlyphStorageMapping(self, layerName): 418 | """ 419 | Get the glyph set contents mapping for the given layer name. 420 | 421 | Subclasses MUST NOT override this method. 422 | """ 423 | if self._glyphStorageMapping.get(layerName) is None: 424 | data = self.readGlyphSetContents(layerName) 425 | self._glyphStorageMapping[layerName] = data 426 | return self._glyphStorageMapping[layerName] 427 | 428 | def getGlyphStorageName(self, layerName, glyphName): 429 | """ 430 | Get the glyph storage name for the given layer name and glyph name. 431 | If none is defined, one will be created using the 432 | UFO 3 user name to file name algorithm. 433 | 434 | Subclasses MUST NOT override this method. 435 | """ 436 | glyphStorageMapping = self.getGlyphStorageMapping(layerName) 437 | if glyphName not in glyphStorageMapping: 438 | storageName = userNameToFileName(unicode(glyphName), existing=glyphStorageMapping.values(), suffix=".glif") 439 | glyphStorageMapping[glyphName] = storageName 440 | return glyphStorageMapping[glyphName] 441 | 442 | def getGlyphNames(self, layerName): 443 | """ 444 | Get a list of glyph names for layer name. 445 | 446 | Subclasses MUST NOT override this method. 447 | """ 448 | return self.getGlyphStorageMapping(layerName).keys() 449 | 450 | def readGlyph(self, layerName, glyphName): 451 | """ 452 | Read a glyph with the given name from the layer 453 | with the given layer name. 454 | 455 | Subclasses MAY override this method. 456 | """ 457 | layerStorageName = self.getLayerStorageName(layerName) 458 | glyphStorageName = self.getGlyphStorageName(layerName, glyphName) 459 | path = self.joinLocations(layerStorageName, glyphStorageName) 460 | tree = self.readTreeFromLocation(path) 461 | return tree 462 | 463 | def writeGlyph(self, layerName, glyphName, tree): 464 | """ 465 | Write a glyph with the given name to the layer 466 | with the given layer name from the given tree. 467 | 468 | Subclasses MAY override this method. 469 | """ 470 | layerStorageName = self.getLayerStorageName(layerName) 471 | glyphStorageName = self.getGlyphStorageName(layerName, glyphName) 472 | path = self.joinLocations(layerStorageName, glyphStorageName) 473 | self.writeTreeToLocation(tree, path) 474 | 475 | 476 | # --------- 477 | # Debugging 478 | # --------- 479 | 480 | def _makeTestPath(fileSystemClass): 481 | import os 482 | fileName = "ufo4-debug-%s.%s" % (fileSystemClass.__name__, fileSystemClass.fileExtension) 483 | path = os.path.join("~", "desktop", fileName) 484 | path = os.path.expanduser(path) 485 | return path 486 | 487 | 488 | def debugWriteFont(fileSystemClass): 489 | """ 490 | This function will write a basic font file 491 | with the given file system class. It will 492 | be written to the following path: 493 | 494 | ~/desktop/ufo4-debug-[file system class name].[file extension] 495 | 496 | This should only be used for debugging when 497 | creating a subclass of BaseFileSystem. 498 | """ 499 | import os 500 | import shutil 501 | from ufoReaderWriter import UFOReaderWriter 502 | from fonts import compileFont 503 | 504 | font = compileFont("file structure building test") 505 | 506 | path = _makeTestPath(fileSystemClass) 507 | if os.path.exists(path): 508 | if os.path.isdir(path): 509 | shutil.rmtree(path) 510 | else: 511 | os.remove(path) 512 | 513 | fileSystem = fileSystemClass(path) 514 | 515 | writer = UFOReaderWriter(fileSystem) 516 | writer.writeMetaInfo() 517 | writer.writeInfo(font.info) 518 | writer.writeGroups(font.groups) 519 | writer.writeKerning(font.kerning) 520 | writer.writeLib(font.lib) 521 | writer.writeFeatures(font.features) 522 | for layerName, layer in sorted(font.layers.items()): 523 | for glyphName in sorted(layer.keys()): 524 | glyph = layer[glyphName] 525 | writer.writeGlyph(layerName, glyphName, glyph) 526 | writer.writeGlyphSetContents(layerName) 527 | writer.writeLayerContents() 528 | writer.close() 529 | return font 530 | 531 | def debugReadFont(fileSystemClass): 532 | """ 533 | This function will read a font file with 534 | the given file system class. It expects 535 | a file to be located at the following path: 536 | 537 | ~/desktop/ufo4-debug-[file system class name].[file extension] 538 | 539 | This should only be used for debugging when 540 | creating a subclass of BaseFileSystem. 541 | """ 542 | from ufoReaderWriter import UFOReaderWriter 543 | from objects import Font 544 | 545 | path = _makeTestPath(fileSystemClass) 546 | 547 | fileSystem = fileSystemClass(path) 548 | 549 | font = Font() 550 | reader = UFOReaderWriter(fileSystem) 551 | reader.readMetaInfo() 552 | reader.readInfo(font.info) 553 | font.groups = reader.readGroups() 554 | font.kerning = reader.readKerning() 555 | font.lib = reader.readLib() 556 | font.features = reader.readFeatures() 557 | font.loadLayers(reader) 558 | for layer in font.layers.values(): 559 | for glyph in layer: 560 | pass 561 | return font 562 | 563 | def debugRoundTripFont(fileSystemClass): 564 | """ 565 | Compare the read/write results with the given file 566 | system class. Returns a string listing differences 567 | or None if no differences were found. This will write 568 | a file to the following path: 569 | 570 | ~/desktop/ufo4-debug-[file system class name].[file extension] 571 | 572 | This should only be used for debugging when 573 | creating a subclass of BaseFileSystem. 574 | """ 575 | font1 = debugWriteFont(fileSystemClass) 576 | font2 = debugReadFont(fileSystemClass) 577 | 578 | differences = [] 579 | # font info 580 | for attr in sorted(dir(font1.info)): 581 | if attr.startswith("_"): 582 | continue 583 | value1 = getattr(font1.info, attr) 584 | value2 = None 585 | if hasattr(font2.info, attr): 586 | value2 = getattr(font2.info, attr) 587 | if value1 != value2: 588 | differences.append("info: %s" % attr) 589 | # groups 590 | if font1.groups != font2.groups: 591 | differences.append("groups") 592 | # kerning 593 | if font1.kerning != font2.kerning: 594 | differences.append("kerning") 595 | # lib 596 | if font1.lib != font2.lib: 597 | print font1.lib, font2.lib 598 | differences.append("lib") 599 | # features 600 | if font1.features != font2.features: 601 | differences.append("features") 602 | # layer order 603 | layers1 = [layerName for layerName in font1.layers.keys()] 604 | layers2 = [layerName for layerName in font2.layers.keys()] 605 | if set(layers1) != set(layers2): 606 | differences.append("layers") 607 | if layers1 != layers2: 608 | differences.append("layer order") 609 | # glyphs 610 | layerNames = set(font1.layers.keys()) | set(font2.layers.keys()) 611 | glyphNames = set() 612 | for font in (font1, font2): 613 | for layerName, layer in font.layers.items(): 614 | for glyphName in layer.keys(): 615 | glyphNames.add((layerName, glyphName)) 616 | for layerName, glyphName in sorted(glyphNames): 617 | layer1 = font1.layers.get(layerName) 618 | layer2 = font2.layers.get(layerName) 619 | if layer1 is not None and layer2 is not None: 620 | glyph1 = layer1.get(glyphName) 621 | glyph2 = layer2.get(glyphName) 622 | if glyph1 is None and glyph2 is None: 623 | pass 624 | elif glyph1 is None or glyph2 is None: 625 | differences.append("missing glyph: %s" % glyphName) 626 | else: 627 | glyph1 = _comparableGlyph(glyph1) 628 | glyph2 = _comparableGlyph(glyph2) 629 | if glyph1 != glyph2: 630 | differences.append("glyph: %s" % glyphName) 631 | if differences: 632 | return "\n".join(differences) 633 | 634 | def _comparableGlyph(glyph): 635 | from copy import deepcopy 636 | data = dict( 637 | name=glyph.name, 638 | unicodes=deepcopy(glyph.unicodes), 639 | width=glyph.width, 640 | height=glyph.height, 641 | contours=deepcopy(glyph.contours), 642 | components=deepcopy(glyph.components), 643 | lib=deepcopy(glyph.lib), 644 | note=glyph.note 645 | ) 646 | return data 647 | -------------------------------------------------------------------------------- /fileStructureTests/core/fonts.py: -------------------------------------------------------------------------------- 1 | import os 2 | from copy import deepcopy 3 | from objects import Font 4 | from fontTools.agl import AGL2UV 5 | 6 | aglNames = list(sorted(AGL2UV.keys())) 7 | 8 | directory = os.path.dirname(__file__) 9 | path = os.path.join(directory, "fonts.txt") 10 | f = open(path, "rb") 11 | profiles = f.read() 12 | f.close() 13 | 14 | shellFontInfo = { 15 | "ascender" : 750, 16 | "capHeight" : 670, 17 | "copyright" : "copyright", 18 | "descender" : -170, 19 | "familyName" : "Test Family", 20 | "note" : "This is a note.", 21 | "openTypeHeadLowestRecPPEM" : 9, 22 | "openTypeHheaAscender" : 830, 23 | "openTypeHheaCaretOffset" : 0, 24 | "openTypeHheaCaretSlopeRise" : 1, 25 | "openTypeHheaCaretSlopeRun" : 0, 26 | "openTypeHheaDescender" : -170, 27 | "openTypeHheaLineGap" : 0, 28 | "openTypeNameDesigner" : "designer", 29 | "openTypeNameDesignerURL" : "http://designer.url", 30 | "openTypeNameLicense" : "license", 31 | "openTypeNameLicenseURL" : "license url", 32 | "openTypeNameManufacturer" : "manufacturer", 33 | "openTypeNameManufacturerURL" : "http://manufacturer.url", 34 | "openTypeNameUniqueID" : "manufacturer:Test Family-Regular:2015", 35 | "openTypeOS2CodePageRanges" : [0, 1, 4, 7, 29], 36 | "openTypeOS2Panose" : [2, 13, 10, 4, 4, 5, 2, 5, 2, 3], 37 | "openTypeOS2Selection" : [7, 8], 38 | "openTypeOS2StrikeoutPosition" : 373, 39 | "openTypeOS2StrikeoutSize" : 151, 40 | "openTypeOS2SubscriptXOffset" : 41, 41 | "openTypeOS2SubscriptXSize" : 354, 42 | "openTypeOS2SubscriptYOffset" : -70, 43 | "openTypeOS2SubscriptYSize" : 360, 44 | "openTypeOS2SuperscriptXOffset" : 41, 45 | "openTypeOS2SuperscriptXSize" : 354, 46 | "openTypeOS2SuperscriptYOffset" : 380, 47 | "openTypeOS2SuperscriptYSize" : 360, 48 | "openTypeOS2Type" : [2], 49 | "openTypeOS2TypoAscender" : 830, 50 | "openTypeOS2TypoDescender" : -170, 51 | "openTypeOS2TypoLineGap" : 0, 52 | "openTypeOS2UnicodeRanges" : [0, 1, 2], 53 | "openTypeOS2VendorID" : "UFO4", 54 | "openTypeOS2WeightClass" : 800, 55 | "openTypeOS2WidthClass" : 5, 56 | "openTypeOS2WinAscent" : 956, 57 | "openTypeOS2WinDescent" : 262, 58 | "postscriptBlueFuzz" : 0, 59 | "postscriptBlueScale" : 0.039625, 60 | "postscriptBlueShift" : 7, 61 | "postscriptBlueValues" : [-10, 0, 544, 554, 670, 680, 750, 760], 62 | "postscriptFamilyBlues" : [-10, 0, 544, 554, 670, 680, 750, 760], 63 | "postscriptFamilyOtherBlues" : [-180, -170, 320, 330], 64 | "postscriptForceBold" : False, 65 | "postscriptOtherBlues" : [-180, -170, 320, 330], 66 | "postscriptStemSnapH" : [128, 145, 143], 67 | "postscriptStemSnapV" : [197, 202, 208, 220], 68 | "postscriptUnderlinePosition" : -110, 69 | "postscriptUnderlineThickness" : 100, 70 | "postscriptWeightName" : "Regular", 71 | "styleMapFamilyName" : "Test Family", 72 | "styleMapStyleName" : "regular", 73 | "styleName" : "Regular", 74 | "trademark" : "trademark", 75 | "unitsPerEm" : 1000, 76 | "versionMajor" : 1, 77 | "versionMinor" : 0, 78 | "xHeight" : 544 79 | } 80 | 81 | # ------- 82 | # Support 83 | # ------- 84 | 85 | # shuffle 86 | 87 | def shuffle(stuff, recursion=0): 88 | """ 89 | Shuffle stuff in a repeatable way. 90 | """ 91 | length = len(stuff) 92 | if length == 1: 93 | return stuff 94 | elif length == 2: 95 | return list(reversed(stuff)) 96 | elif length < 6: 97 | shuffler = [ 98 | [], 99 | [] 100 | ] 101 | elif length < 15: 102 | shuffler = [ 103 | [], 104 | [], 105 | [], 106 | [] 107 | ] 108 | else: 109 | shuffler = [ 110 | [], 111 | [], 112 | [], 113 | [], 114 | [], 115 | [], 116 | [], 117 | [], 118 | [], 119 | [] 120 | ] 121 | i = 0 122 | for s in stuff: 123 | shuffler[i].append(s) 124 | i += 1 125 | if i == len(shuffler): 126 | i = 0 127 | del stuff[:] 128 | for i in shuffler: 129 | stuff.extend(i) 130 | if recursion < 3: 131 | recursion += 1 132 | shuffle(stuff, recursion) 133 | 134 | # distribute 135 | 136 | def distribute(stuff, targets): 137 | """ 138 | Distribute stuff to targets. 139 | """ 140 | stuff = stuff 141 | target = 0 142 | for i in stuff: 143 | targets[target].append(i) 144 | target += 1 145 | if target == len(targets): 146 | target = 0 147 | 148 | # stuff genertors 149 | 150 | def generateList(stuff, length): 151 | """ 152 | Generate a list from the stuff with the given length. 153 | """ 154 | result = [] 155 | candidates = [] 156 | for i in range(length): 157 | if not candidates: 158 | candidates = list(stuff) 159 | result.append(candidates.pop(0)) 160 | return result 161 | 162 | characters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_") 163 | shuffle(characters) 164 | 165 | def generateString(length): 166 | """ 167 | Generate a string with length. 168 | """ 169 | compiled = generateList(characters, length) 170 | shuffle(compiled) 171 | return "".join(compiled) 172 | 173 | def generateSegments(count, x, y, typ): 174 | segments = [] 175 | for i in range(count): 176 | segments.append(dict(type=typ, points=[])) 177 | if typ == "curve": 178 | for i in range(2): 179 | segments[-1]["points"].append((x, y)) 180 | x += 1 181 | y += 1 182 | elif typ == "qCurve": 183 | segments[-1]["points"].append((x, y)) 184 | x += 1 185 | y += 1 186 | segments[-1]["points"].append((x, y)) 187 | x += 1 188 | y += 1 189 | return segments, x, y 190 | 191 | # ------ 192 | # Parser 193 | # ------ 194 | 195 | fontDescriptions = [] 196 | currentFont = None 197 | for line in profiles.splitlines(): 198 | if line.startswith("#") or not line: 199 | continue 200 | # start 201 | elif line.startswith(">"): 202 | currentFont = dict( 203 | name="font %d" % (len(fontDescriptions) + 1), 204 | fontInfo=0, 205 | kerning=0, 206 | groups=0, 207 | groupContents=0, 208 | features=0, 209 | layers=0, 210 | glyphs={}, 211 | contours={}, 212 | components={} 213 | ) 214 | fontDescriptions.append(currentFont) 215 | # name 216 | elif line.startswith("name"): 217 | currentFont["name"] = line.split(":", 1)[-1].strip() 218 | # fingerprint 219 | elif line.startswith("fingerprint"): 220 | currentFont["fingerprint"] = line.split(":", 1)[-1].strip() 221 | # font info 222 | elif line.startswith("font info characters:"): 223 | currentFont["fontInfo"] = int(line.split(":")[-1].strip()) 224 | # kerning 225 | elif line.startswith("kerning pairs:"): 226 | currentFont["kerning"] = int(line.split(":")[-1].strip()) 227 | # groups 228 | elif line.startswith("groups:"): 229 | currentFont["groups"] = int(line.split(":")[-1].strip()) 230 | elif line.startswith("group members:"): 231 | currentFont["groupContents"] = int(line.split(":")[-1].strip()) 232 | # features 233 | elif line.startswith("features:"): 234 | currentFont["features"] = int(line.split(":")[-1].strip()) 235 | # layers 236 | elif line.startswith("layers:"): 237 | currentFont["layers"] = int(line.split(":")[-1].strip()) 238 | # glyphs 239 | elif line.startswith("glyphs:"): 240 | currentFont["glyphCount"] = int(line.split(":")[-1].strip()) 241 | elif line.startswith("glyphs with "): 242 | line = line.replace("glyphs with ", "") 243 | line = line.replace(" contours:", "") 244 | contourCount, glyphCount = line.split(" ") 245 | glyphCount = float(glyphCount) 246 | contourCount = int(contourCount) 247 | currentFont["glyphs"][contourCount] = glyphCount 248 | elif line.startswith("glyph name characters:"): 249 | currentFont["glyphNames"] = int(line.split(":")[-1].strip()) 250 | elif line.startswith("glyph note characters:"): 251 | currentFont["glyphNotes"] = int(line.split(":")[-1].strip()) 252 | # contours 253 | elif line.startswith("contours:"): 254 | currentFont["contourCount"] = int(line.split(":")[-1].strip()) 255 | elif line.startswith("contours with "): 256 | line = line.replace("contours with ", "") 257 | line = line.replace(" segments:", "") 258 | segmentCount, contourCount = line.split(" ") 259 | contourCount = float(contourCount) 260 | segmentCount = int(segmentCount) 261 | currentFont["contours"][segmentCount] = contourCount 262 | # segments 263 | elif line.startswith("segments:"): 264 | currentFont["segments"] = int(line.split(":")[-1].strip()) 265 | elif line.startswith("moveTo segments:"): 266 | currentFont["moveToSegments"] = float(line.split(":")[-1].strip()) 267 | elif line.startswith("lineTo segments:"): 268 | currentFont["lineToSegments"] = float(line.split(":")[-1].strip()) 269 | elif line.startswith("curveTo segments:"): 270 | currentFont["curveToSegments"] = float(line.split(":")[-1].strip()) 271 | elif line.startswith("qCurveTo segments:"): 272 | currentFont["qCurveToSegments"] = float(line.split(":")[-1].strip()) 273 | # components 274 | elif line.startswith("components:"): 275 | currentFont["componentCount"] = int(line.split(":")[-1].strip()) 276 | elif line.startswith("components with ("): 277 | line = line.replace("components with (", "") 278 | line = line.replace(") transformation:", "") 279 | parts = line.split(" ") 280 | transformation = parts[:-1] 281 | glyphCount = parts[-1] 282 | glyphCount = float(glyphCount) 283 | transformation = tuple([float(i) for i in transformation]) 284 | currentFont["components"][transformation] = glyphCount 285 | 286 | d = {} 287 | for description in fontDescriptions: 288 | d[description["name"]] = description 289 | fontDescriptions = d 290 | 291 | # ------------ 292 | # Pre-Compiler 293 | # ------------ 294 | 295 | fontInfoStringAttributes = """ 296 | familyName 297 | styleName 298 | styleMapFamilyName 299 | copyright 300 | trademark 301 | note 302 | openTypeNameDesigner 303 | openTypeNameDesignerURL 304 | openTypeNameManufacturer 305 | openTypeNameManufacturerURL 306 | openTypeNameLicense 307 | openTypeNameLicenseURL 308 | openTypeNameVersion 309 | openTypeNameUniqueID 310 | openTypeNameDescription 311 | openTypeNamePreferredFamilyName 312 | openTypeNamePreferredSubfamilyName 313 | openTypeNameCompatibleFullName 314 | openTypeNameSampleText 315 | openTypeNameWWSFamilyName 316 | openTypeNameWWSSubfamilyName 317 | postscriptFontName 318 | postscriptFullName 319 | postscriptWeightName 320 | macintoshFONDName 321 | """.strip().split() 322 | 323 | for font in fontDescriptions.values(): 324 | # glyphs 325 | ## segments 326 | totalSegments = font.pop("segments") 327 | moveToSegmentCount = int(round(totalSegments * font.pop("moveToSegments"))) 328 | lineToSegmentCount = int(round(totalSegments * font.pop("lineToSegments"))) 329 | curveToSegmentCount = int(round(totalSegments * font.pop("curveToSegments"))) 330 | qCurveToSegmentCount = int(round(totalSegments * font.pop("qCurveToSegments"))) 331 | x = y = 0 332 | lineToSegments, x, y = generateSegments(moveToSegmentCount + lineToSegmentCount, x, y, "line") 333 | curveToSegments, x, y = generateSegments(curveToSegmentCount, x, y, "curve") 334 | qCurveToSegments, x, y = generateSegments(qCurveToSegmentCount, x, y, "qCurve") 335 | segments = lineToSegments + curveToSegments + qCurveToSegments 336 | shuffle(segments) 337 | generatedTotal = moveToSegmentCount + lineToSegmentCount + curveToSegmentCount + qCurveToSegmentCount 338 | assert generatedTotal == totalSegments 339 | ## contours 340 | totalContours = font.pop("contourCount") 341 | contours = [] 342 | for segmentCount, contourCount in sorted(font.pop("contours").items()): 343 | contourCount = int(round(totalContours * contourCount)) 344 | for i in range(contourCount): 345 | contour = segments[:segmentCount] 346 | segments = segments[segmentCount:] 347 | contours.append(contour) 348 | shuffle(contours) 349 | assert len(contours) == totalContours 350 | ## glyph 351 | totalGlyphs = font.pop("glyphCount") 352 | width = 1 353 | glyphs = [] 354 | for contourCount, glyphCount in sorted(font.pop("glyphs").items()): 355 | glyphCount = int(round(totalGlyphs * glyphCount)) 356 | for i in range(glyphCount): 357 | glyph = dict( 358 | name="", 359 | unicode=None, 360 | width=width, 361 | contours=contours[:contourCount], 362 | components=[], 363 | note="" 364 | ) 365 | glyphs.append(glyph) 366 | contours = contours[contourCount:] 367 | width += 1 368 | ## name & unicode 369 | candidates = [] 370 | loop = -1 371 | for i in range(len(glyphs)): 372 | if not candidates: 373 | candidates = list(aglNames) 374 | loop += 1 375 | name = candidates.pop(0) 376 | if loop: 377 | name += ".alt%d" % loop 378 | uni = AGL2UV.get(name) 379 | glyphs[i]["name"] = name 380 | glyphs[i]["unicode"] = uni 381 | glyphNames = sorted([i["name"] for i in glyphs]) 382 | ## components 383 | totalComponents = font.pop("componentCount") 384 | baseGlyphs = generateList(glyphNames, totalComponents) 385 | components = [] 386 | for transformation, componentCount in sorted(font.pop("components").items()): 387 | componentCount = int(round(totalComponents * componentCount)) 388 | for i in range(componentCount): 389 | component = (baseGlyphs.pop(0), transformation) 390 | components.append(component) 391 | assert len(components) == totalComponents 392 | glyphComponents = [d["components"] for d in glyphs] 393 | distribute(components, glyphComponents) 394 | ## note 395 | glyphNotes = [[] for i in range(len(glyphs))] 396 | glyphNoteCharacters = list(generateString(font.pop("glyphNotes"))) 397 | distribute(glyphNoteCharacters, glyphNotes) 398 | for i, note in enumerate(glyphNotes): 399 | glyphs[i]["note"] = "".join(note) 400 | # layers 401 | layers = {} 402 | for i in range(font["layers"]): 403 | if i == 0: 404 | name = "public.default" 405 | else: 406 | name = "layer%d" % i 407 | layers[name] = [] 408 | font["layers"] = layers 409 | layers = [v for k, v in sorted(layers.items())] 410 | distribute(glyphs, layers) 411 | # fontInfo 412 | fontInfo = deepcopy(shellFontInfo) 413 | fontInfoCharacters = list(generateString(font["fontInfo"])) 414 | fontInfoStrings = [[] for i in range(len(fontInfoStringAttributes))] 415 | distribute(fontInfoCharacters, fontInfoStrings) 416 | for i, key in enumerate(sorted(fontInfoStringAttributes)): 417 | fontInfo[key] = "".join(fontInfoStrings[i]) 418 | font["fontInfo"] = fontInfo 419 | # kerning 420 | kerning = {} 421 | value = 1 422 | for i1, side1 in enumerate(glyphNames): 423 | for i2, side2 in enumerate(glyphNames): 424 | if len(kerning) == font["kerning"]: 425 | break 426 | if i2 % 2: 427 | v = -value 428 | else: 429 | v = value 430 | kerning[side1, side2] = v 431 | value += 1 432 | font["kerning"] = kerning 433 | # groups 434 | font["groups"] = {"group%d" % (i + 1) : [] for i in range(font["groups"])} 435 | groups = [v for k, v in sorted(font["groups"].items())] 436 | groupContents = generateList(glyphNames, font.pop("groupContents")) 437 | shuffle(groupContents) 438 | distribute(groupContents, groups) 439 | # features 440 | font["features"] = generateString(font["features"]) 441 | 442 | # -------- 443 | # Compiler 444 | # -------- 445 | 446 | def compileFont(name): 447 | description = fontDescriptions[name] 448 | font = Font() 449 | for attr, value in description["fontInfo"].items(): 450 | setattr(font.info, attr, value) 451 | font.kerning.update(description["kerning"]) 452 | font.groups.update(description["groups"]) 453 | for layerName, glyphs in description["layers"].items(): 454 | layer = font.newLayer(layerName) 455 | for glyph in glyphs: 456 | target = layer.newGlyph(glyph["name"]) 457 | target.width = glyph["width"] 458 | target.unicodes = [glyph["unicode"]] 459 | target.note = glyph["note"] 460 | for contour in glyph["contours"]: 461 | target.beginPath() 462 | for segment in contour: 463 | for i, point in enumerate(segment["points"]): 464 | pointType = None 465 | if i == len(segment["points"]) - 1: 466 | pointType = segment["type"] 467 | target.addPoint(point, segmentType=pointType) 468 | target.endPath() 469 | return font 470 | -------------------------------------------------------------------------------- /fileStructureTests/core/fonts.txt: -------------------------------------------------------------------------------- 1 | > 2 | name: file structure building test 3 | font info characters: 18032 4 | kerning pairs: 3527 5 | groups: 96 6 | group members: 527 7 | feature characters: 100 8 | layers: 2 9 | glyphs: 547 10 | glyph note characters: 50 11 | contours: 1003 12 | components: 10 13 | segments: 8586 14 | moveTo segments: 0.116818075938 15 | lineTo segments: 0.555322618216 16 | curveTo segments: 0.327859305847 17 | qCurveTo segments: 0.0 18 | glyphs with 7 contours: 0.0073126143 19 | glyphs with 5 contours: 0.0073126143 20 | glyphs with 4 contours: 0.0292504570 21 | glyphs with 3 contours: 0.1407678245 22 | glyphs with 2 contours: 0.4003656307 23 | glyphs with 1 contours: 0.4058500914 24 | glyphs with 0 contours: 0.0091407678 25 | contours with 29 segments: 0.0039880359 26 | contours with 28 segments: 0.0039880359 27 | contours with 25 segments: 0.0059820538 28 | contours with 24 segments: 0.0009970090 29 | contours with 23 segments: 0.0069790628 30 | contours with 22 segments: 0.0119641077 31 | contours with 21 segments: 0.0149551346 32 | contours with 20 segments: 0.0089730808 33 | contours with 19 segments: 0.0129611167 34 | contours with 18 segments: 0.0129611167 35 | contours with 17 segments: 0.0139581256 36 | contours with 16 segments: 0.0139581256 37 | contours with 15 segments: 0.0059820538 38 | contours with 14 segments: 0.0029910269 39 | contours with 13 segments: 0.0438683948 40 | contours with 12 segments: 0.0757726820 41 | contours with 11 segments: 0.0578265204 42 | contours with 10 segments: 0.0329012961 43 | contours with 9 segments: 0.0358923230 44 | contours with 8 segments: 0.0279162512 45 | contours with 7 segments: 0.2023928215 46 | contours with 6 segments: 0.0508474576 47 | contours with 5 segments: 0.0767696909 48 | contours with 4 segments: 0.2482552343 49 | contours with 3 segments: 0.0269192423 50 | components with (0 0 1 0 0 1) transformation: 0.5 51 | components with (0 0 1 10 20 1) transformation: 0.5 52 | < -------------------------------------------------------------------------------- /fileStructureTests/core/glyphTree.py: -------------------------------------------------------------------------------- 1 | from environment import ET 2 | from plistTree import convertTreeToPlist, convertPlistToTree 3 | 4 | class GlyphTreeError(Exception): pass 5 | 6 | # ------------- 7 | # Glyph Reading 8 | # ------------- 9 | 10 | def readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(2)): 11 | readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen) 12 | 13 | def readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None): 14 | # get the name 15 | _readName(glyphObject, tree) 16 | # populate the sub elements 17 | unicodes = [] 18 | guidelines = [] 19 | anchors = [] 20 | haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = False 21 | identifiers = set() 22 | for element in tree: 23 | tag = element.tag 24 | if tag == "outline": 25 | attrib = element.attrib 26 | haveSeenOutline = True 27 | if pointPen is not None: 28 | buildOutlineFormat2(glyphObject, pointPen, element, identifiers) 29 | elif glyphObject is None: 30 | continue 31 | elif tag == "advance": 32 | haveSeenAdvance = True 33 | _readAdvance(glyphObject, element) 34 | elif tag == "unicode": 35 | try: 36 | attrib = element.attrib 37 | v = attrib.get("hex", "undefined") 38 | v = int(v, 16) 39 | if v not in unicodes: 40 | unicodes.append(v) 41 | except ValueError: 42 | raise GlyphTreeError("Illegal value for hex attribute of unicode element.") 43 | elif tag == "guideline": 44 | attrib = element.attrib 45 | for attr in ("x", "y", "angle"): 46 | if attr in attrib: 47 | attrib[attr] = _number(attrib[attr]) 48 | guidelines.append(attrib) 49 | elif tag == "anchor": 50 | attrib = element.attrib 51 | for attr in ("x", "y"): 52 | if attr in attrib: 53 | attrib[attr] = _number(attrib[attr]) 54 | anchors.append(attrib) 55 | elif tag == "image": 56 | haveSeenImage = True 57 | _readImage(glyphObject, element) 58 | elif tag == "note": 59 | haveSeenNote = True 60 | _readNote(glyphObject, element) 61 | elif tag == "lib": 62 | haveSeenLib = True 63 | _readLib(glyphObject, element) 64 | else: 65 | raise GlyphTreeError("Unknown element in GLIF: %s" % tag) 66 | # set the collected unicodes 67 | if unicodes: 68 | _relaxedSetattr(glyphObject, "unicodes", unicodes) 69 | # set the collected guidelines 70 | if guidelines: 71 | _relaxedSetattr(glyphObject, "guidelines", guidelines) 72 | # set the collected anchors 73 | if anchors: 74 | _relaxedSetattr(glyphObject, "anchors", anchors) 75 | 76 | def _readName(glyphObject, element): 77 | glyphName = element.attrib.get("name") 78 | if glyphName and glyphObject is not None: 79 | _relaxedSetattr(glyphObject, "name", glyphName) 80 | 81 | def _readAdvance(glyphObject, element): 82 | attrib = element.attrib 83 | width = _number(attrib.get("width", 0)) 84 | _relaxedSetattr(glyphObject, "width", width) 85 | height = _number(attrib.get("height", 0)) 86 | _relaxedSetattr(glyphObject, "height", height) 87 | 88 | def _readNote(glyphObject, element): 89 | rawNote = element.text 90 | lines = rawNote.split("\n") 91 | lines = [line.strip() for line in lines] 92 | note = "\n".join(lines) 93 | _relaxedSetattr(glyphObject, "note", note) 94 | 95 | def _readLib(glyphObject, element): 96 | from plist import convertTreeToPlist 97 | assert len(element) == 1 98 | lib = convertTreeToPlist(element[0]) 99 | if lib is None: 100 | lib = {} 101 | _relaxedSetattr(glyphObject, "lib", lib) 102 | 103 | def _readImage(glyphObject, element): 104 | imageData = element.attrib 105 | for attr, default in _transformationInfo: 106 | value = default 107 | if attr in imageData: 108 | value = imageData[attr] 109 | imageData[attr] = _number(value) 110 | _relaxedSetattr(glyphObject, "image", imageData) 111 | 112 | # ---------------- 113 | # GLIF to PointPen 114 | # ---------------- 115 | 116 | def buildOutlineFormat2(glyphObject, pen, tree, identifiers): 117 | anchors = [] 118 | for element in tree: 119 | tag = element.tag 120 | if tag == "contour": 121 | _buildOutlineContourFormat2(pen, element, identifiers) 122 | elif tag == "component": 123 | _buildOutlineComponentFormat2(pen, element, identifiers) 124 | else: 125 | raise GlyphTreeError("Unknown element in outline element: %s" % tag) 126 | 127 | def _buildOutlineContourFormat2(pen, tree, identifiers): 128 | attrib = tree.attrib 129 | identifier = attrib.get("identifier") 130 | if identifier is not None: 131 | identifiers.add(identifier) 132 | pen.beginPath(identifier=identifier) 133 | for element in tree: 134 | _buildOutlinePointsFormat2(pen, element, identifiers) 135 | pen.endPath() 136 | 137 | def _buildOutlinePointsFormat2(pen, element, identifiers): 138 | attrib = element.attrib 139 | x = _number(attrib["x"]) 140 | y = _number(attrib["y"]) 141 | segmentType = attrib.get("type") 142 | smooth = attrib.get("smooth") 143 | if smooth is not None: 144 | smooth = smooth == "yes" 145 | else: 146 | smooth = False 147 | name = attrib.get("name") 148 | identifier = attrib.get("identifier") 149 | pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name, identifier=identifier) 150 | 151 | def _buildOutlineComponentFormat2(pen, element, identifiers): 152 | if len(element): 153 | raise GlyphTreeError("Unknown child elements of component element.") 154 | attrib = element.attrib 155 | baseGlyphName = attrib.get("base") 156 | transformation = [] 157 | for attr, default in _transformationInfo: 158 | value = attrib.get(attr) 159 | if value is None: 160 | value = default 161 | else: 162 | value = _number(value) 163 | transformation.append(value) 164 | identifier = attrib.get("identifier") 165 | pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier) 166 | 167 | # ------------- 168 | # Glyph Writing 169 | # ------------- 170 | 171 | def writeGlyphToTree(glyph): 172 | tree = ET.Element("glyph") 173 | tree.attrib["name"] = glyph.name 174 | tree.attrib["format"] = "2" 175 | _writeAdvance(glyph, tree) 176 | _writeUnicodes(glyph, tree) 177 | _writeNote(glyph, tree) 178 | _writeImage(glyph, tree) 179 | _writeGuidelines(glyph, tree) 180 | _writeAnchors(glyph, tree) 181 | _writeOutline(glyph, tree) 182 | _writeLib(glyph, tree) 183 | return tree 184 | 185 | def _writeAdvance(glyph, tree): 186 | width = glyph.width 187 | height = glyph.height 188 | if width or height: 189 | element = ET.Element("advance") 190 | if width: 191 | element.attrib["width"] = str(width) 192 | if height: 193 | element.attrib["height"] = str(height) 194 | tree.append(element) 195 | 196 | def _writeUnicodes(glyph, tree): 197 | for code in glyph.unicodes: 198 | hexCode = hex(code)[2:].upper() 199 | if len(hexCode) < 4: 200 | hexCode = "0" * (4 - len(hexCode)) + hexCode 201 | element = ET.Element("unicode") 202 | element.attrib["hex"] = hexCode 203 | tree.append(element) 204 | 205 | def _writeNote(glyph, tree): 206 | note = glyph.note 207 | if note: 208 | element = ET.Element("note") 209 | element.text = note 210 | tree.append(element) 211 | 212 | def _writeImage(glyph, tree): 213 | if not glyph.image: 214 | return 215 | element = ET.Element("image") 216 | element.attrib.update(glyph.image) 217 | tree.append(image) 218 | 219 | def _writeGuidelines(glyph, tree): 220 | for guideline in glyph.guidelines: 221 | data = {} 222 | for key, value in guideline.items(): 223 | data[key] = str(value) 224 | element = ET.Element("guideline") 225 | tree.append(guideline) 226 | 227 | def _writeAnchors(glyph, tree): 228 | for anchor in glyph.anchors: 229 | element = ET.Element("anchor") 230 | element.attrib.update(anchor) 231 | tree.append(element) 232 | 233 | def _writeLib(glyph, tree): 234 | if glyph.lib: 235 | element = convertPlistToTree(glyph.lib) 236 | tree.append(element) 237 | 238 | def _writeOutline(glyph, tree): 239 | element = ET.Element("outline") 240 | _writeContours(glyph, element) 241 | _writeComponents(glyph, element) 242 | tree.append(element) 243 | 244 | def _writeContours(glyph, tree): 245 | for contour in glyph.contours: 246 | contourElement = ET.Element("contour") 247 | if contour.identifier: 248 | contourElement.attrib["identifier"] = contour.identifier 249 | for point in contour: 250 | (x, y), segmentType, smooth, name, identifier = point 251 | pointElement = ET.Element("point") 252 | pointElement.attrib["x"] = str(x) 253 | pointElement.attrib["y"] = str(y) 254 | if segmentType: 255 | pointElement.attrib["type"] = segmentType 256 | if smooth: 257 | pointElement.attrib["smooth"] = "yes" 258 | if identifier: 259 | pointElement.attrib["identifier"] = identifier 260 | contourElement.append(pointElement) 261 | tree.append(contourElement) 262 | 263 | _transformationInfo = [ 264 | # field name, default value 265 | ("xScale", 1), 266 | ("xyScale", 0), 267 | ("yxScale", 0), 268 | ("yScale", 1), 269 | ("xOffset", 0), 270 | ("yOffset", 0), 271 | ] 272 | 273 | def _writeComponents(glyph, tree): 274 | for component in glyph.components: 275 | base, transformation, identifier = component 276 | element = ET.Element("component") 277 | element.attrib["base"] = base 278 | if transformation: 279 | for i, (attr, default) in enumerate(_transformationInfo): 280 | value = transformation[i] 281 | if value != default: 282 | element.attrib[attr] = value 283 | if identifier: 284 | element.attrib["identifier"] 285 | tree.append(element) 286 | 287 | # --------------------- 288 | # Misc Helper Functions 289 | # --------------------- 290 | 291 | def _relaxedSetattr(object, attr, value): 292 | try: 293 | setattr(object, attr, value) 294 | except AttributeError: 295 | pass 296 | 297 | def _number(s): 298 | """ 299 | Given a numeric string, return an integer or a float, whichever 300 | the string indicates. _number("1") will return the integer 1, 301 | _number("1.0") will return the float 1.0. 302 | 303 | >>> _number("1") 304 | 1 305 | >>> _number("1.0") 306 | 1.0 307 | >>> _number("a") 308 | Traceback (most recent call last): 309 | ... 310 | GlyphTreeError: Could not convert a to an int or float. 311 | """ 312 | try: 313 | n = int(s) 314 | return n 315 | except ValueError: 316 | pass 317 | try: 318 | n = float(s) 319 | return n 320 | except ValueError: 321 | raise GlyphTreeError("Could not convert %s to an int or float." % s) 322 | -------------------------------------------------------------------------------- /fileStructureTests/core/objects.py: -------------------------------------------------------------------------------- 1 | class Font(object): 2 | 3 | def __init__(self): 4 | self.info = FontInfo() 5 | self.groups = {} 6 | self.kerning = {} 7 | self.lib = {} 8 | self.features = None 9 | self.layers = {} 10 | 11 | def newLayer(self, layerName): 12 | layer = Layer(None, layerName) 13 | self.layers[layerName] = layer 14 | return layer 15 | 16 | def loadLayers(self, reader): 17 | for layerName in reader.getLayerNames(): 18 | self.layers[layerName] = Layer(reader, layerName) 19 | 20 | 21 | class FontInfo(object): pass 22 | 23 | 24 | class Layer(object): 25 | 26 | def __init__(self, reader, name): 27 | self.reader = reader 28 | self.name = name 29 | if reader is None: 30 | self._glyphs = {} 31 | else: 32 | self._glyphs = dict.fromkeys(reader.getGlyphNames(name)) 33 | 34 | def newGlyph(self, glyphName): 35 | glyph = Glyph() 36 | glyph.name = glyphName 37 | self._glyphs[glyphName] = glyph 38 | return glyph 39 | 40 | def loadGlyph(self, glyphName): 41 | glyph = Glyph() 42 | self.reader.readGlyph(self.name, glyphName, glyph) 43 | return glyph 44 | 45 | def keys(self): 46 | return self._glyphs.keys() 47 | 48 | def __contains__(self, name): 49 | return name in self._glyphs 50 | 51 | def __iter__(self): 52 | names = self.keys() 53 | while names: 54 | name = names[0] 55 | yield self[name] 56 | names = names[1:] 57 | 58 | def __getitem__(self, name): 59 | if self._glyphs[name] is None: 60 | self._glyphs[name] = self.loadGlyph(name) 61 | return self._glyphs[name] 62 | 63 | def get(self, name): 64 | return self._glyphs.get(name) 65 | 66 | 67 | class Glyph(object): 68 | 69 | def __init__(self): 70 | self.width = 0 71 | self.height = 0 72 | self.unicodes = [] 73 | self.contours = [] 74 | self.components = [] 75 | self.guidelines = [] 76 | self.anchors = [] 77 | self.lib = {} 78 | self.image = None 79 | self.note = "" 80 | 81 | def drawPoints(self, pointPen): 82 | raise NotImplementedError 83 | 84 | # ------------- 85 | # Point Pen API 86 | # ------------- 87 | 88 | def beginPath(self, identifier=None, **kwargs): 89 | contour = Contour() 90 | contour.identifier = identifier 91 | self.contours.append(contour) 92 | 93 | def endPath(self): 94 | pass 95 | 96 | def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): 97 | self.contours[-1].append((pt, segmentType, smooth, name, identifier)) 98 | 99 | def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs): 100 | component = (baseGlyphName, transformation, identifier) 101 | self.components.append(component) 102 | 103 | 104 | class Contour(list): 105 | 106 | identifier = None 107 | -------------------------------------------------------------------------------- /fileStructureTests/core/plistTree.py: -------------------------------------------------------------------------------- 1 | from environment import ET 2 | from xmlUtilities import treeToString 3 | 4 | def convertTreeToPlist(tree): 5 | """ 6 | Convert an ElementTree tree/element representing 7 | a Property List to a nested set of Python objects. 8 | """ 9 | root = tree[0] 10 | return _convertElementToObj(root) 11 | 12 | def convertPlistToTree(obj): 13 | """ 14 | Convert a nested set of objects representing a 15 | Property List to an ElementTree tree/element. 16 | """ 17 | tree = ET.Element("plist") 18 | tree.attrib["version"] = "1.0" 19 | element = _convertObjToElement(obj) 20 | tree.append(element) 21 | return tree 22 | 23 | plistHeader = """ 24 | 25 | 26 | """.strip() 27 | 28 | # ------- 29 | # Support 30 | # ------- 31 | 32 | def _convertElementToObj(element): 33 | tag = element.tag 34 | if tag == "dict": 35 | return _convertElementToDict(element) 36 | elif tag == "array": 37 | return _convertElementToList(element) 38 | elif tag == "string": 39 | return _convertElementToString(element) 40 | elif tag == "integer": 41 | return _convertElementToInt(element) 42 | elif tag == "real": 43 | return _convertElementToFloat(element) 44 | elif tag == "false": 45 | return False 46 | elif tag == "true": 47 | return True 48 | # XXX data, date 49 | else: 50 | print tag 51 | 52 | def _convertObjToElement(obj): 53 | if isinstance(obj, dict): 54 | return _convertDictToElement(obj) 55 | elif isinstance(obj, (list, tuple)): 56 | return _convertListToElement(obj) 57 | elif isinstance(obj, basestring): 58 | return _convertStringToElement(obj) 59 | elif isinstance(obj, bool): 60 | if obj == False: 61 | return ET.Element("false") 62 | else: 63 | return ET.Element("true") 64 | elif isinstance(obj, int): 65 | return _convertIntToElement(obj) 66 | elif isinstance(obj, float): 67 | return _convertFloatToElement(obj) 68 | 69 | def _convertElementToDict(element): 70 | obj = {} 71 | currentKey = None 72 | for subElement in element: 73 | tag = subElement.tag 74 | if tag == "key": 75 | currentKey = subElement.text.strip() 76 | else: 77 | obj[currentKey] = _convertElementToObj(subElement) 78 | currentKey = None 79 | return obj 80 | 81 | def _convertDictToElement(obj): 82 | element = ET.Element("dict") 83 | for key, value in sorted(obj.items()): 84 | if value is None: 85 | continue 86 | assert isinstance(key, basestring) 87 | subElement = ET.Element("key") 88 | subElement.text = key 89 | element.append(subElement) 90 | subElement = _convertObjToElement(value) 91 | element.append(subElement) 92 | return element 93 | 94 | def _convertElementToList(element): 95 | obj = [] 96 | for subElement in element: 97 | v = _convertElementToObj(subElement) 98 | obj.append(v) 99 | return obj 100 | 101 | def _convertListToElement(obj): 102 | element = ET.Element("array") 103 | for v in obj: 104 | subElement = _convertObjToElement(v) 105 | element.append(subElement) 106 | return element 107 | 108 | def _convertElementToInt(element): 109 | return int(element.text) 110 | 111 | def _convertIntToElement(obj): 112 | element = ET.Element("integer") 113 | element.text = str(obj) 114 | return element 115 | 116 | # XXX these float conversions may require truncation. see the DTD for details. 117 | # XXX (I'm on flight right now, otherwise I would.) 118 | 119 | def _convertElementToFloat(element): 120 | return float(element.text) 121 | 122 | def _convertFloatToElement(obj): 123 | element = ET.Element("real") 124 | element.text = str(obj) 125 | return element 126 | 127 | def _convertElementToString(element): 128 | return unicode(element.text) 129 | 130 | def _convertStringToElement(obj): 131 | element = ET.Element("string") 132 | element.text = obj 133 | return element 134 | -------------------------------------------------------------------------------- /fileStructureTests/core/ufoReaderWriter.py: -------------------------------------------------------------------------------- 1 | from glyphTree import readGlyphFromTree, writeGlyphToTree 2 | 3 | 4 | class UFOReaderWriterError(Exception): pass 5 | 6 | 7 | class UFOReaderWriter(object): 8 | 9 | def __init__(self, fileSystem): 10 | self._fileSystem = fileSystem 11 | 12 | def close(self): 13 | """ 14 | Close this object. This must be called when the 15 | work this object was needed to perform is complete. 16 | """ 17 | self._fileSystem.close() 18 | 19 | # metainfo 20 | 21 | def readMetaInfo(self): 22 | """ 23 | Read metainfo. Only used for internal operations. 24 | """ 25 | data = self._fileSystem.readMetaInfo() 26 | return data 27 | 28 | def writeMetaInfo(self): 29 | """ 30 | Write metainfo. 31 | """ 32 | data = dict( 33 | creator="org.unifiedfontobject.ufo4Tests.UFOReaderWriter", 34 | formatVersion=4 35 | ) 36 | self._fileSystem.writeMetaInfo(data) 37 | 38 | # groups 39 | 40 | def readGroups(self): 41 | """ 42 | Read groups. Returns a dict. 43 | """ 44 | groups = self._fileSystem.readGroups() 45 | if groups is None: 46 | return 47 | return groups 48 | 49 | def writeGroups(self, data): 50 | """ 51 | Write groups. 52 | """ 53 | if data: 54 | self._fileSystem.writeGroups(data) 55 | 56 | # fontinfo 57 | 58 | def readInfo(self, info): 59 | """ 60 | Read fontinfo. 61 | """ 62 | infoDict = self._fileSystem.readFontInfo() 63 | if infoDict is None: 64 | return 65 | for attr, value in infoDict.items(): 66 | setattr(info, attr, value) 67 | 68 | def writeInfo(self, info): 69 | """ 70 | Write info. 71 | """ 72 | infoDict = {} 73 | for attr in fontInfoAttributes: 74 | if hasattr(info, attr): 75 | value = getattr(info, attr) 76 | if value is None: 77 | continue 78 | infoDict[attr] = value 79 | if infoDict: 80 | self._fileSystem.writeFontInfo(infoDict) 81 | 82 | # kerning 83 | 84 | def readKerning(self): 85 | """ 86 | Read kerning. Returns a dict. 87 | """ 88 | data = self._fileSystem.readKerning() 89 | if data is None: 90 | return 91 | kerning = {} 92 | for side1 in data: 93 | for side2 in data[side1]: 94 | value = data[side1][side2] 95 | kerning[side1, side2] = value 96 | return kerning 97 | 98 | def writeKerning(self, data): 99 | """ 100 | Write kerning. 101 | """ 102 | kerning = {} 103 | for (side1, side2), value in data.items(): 104 | if not side1 in kerning: 105 | kerning[side1] = {} 106 | kerning[side1][side2] = value 107 | if kerning: 108 | self._fileSystem.writeKerning(kerning) 109 | 110 | # lib 111 | 112 | def readLib(self): 113 | """ 114 | Read lib. Returns a dict. 115 | """ 116 | data = self._fileSystem.readLib() 117 | if data is None: 118 | return {} 119 | return data 120 | 121 | def writeLib(self, data): 122 | """ 123 | Write lib. 124 | """ 125 | if data: 126 | self._fileSystem.writeLib(data) 127 | 128 | # features 129 | 130 | def readFeatures(self): 131 | """ 132 | Read features. Returns a string. 133 | """ 134 | return self._fileSystem.readFeatures() 135 | 136 | def writeFeatures(self, data): 137 | """ 138 | Write features. 139 | """ 140 | if data: 141 | self._fileSystem.writeFeatures(data) 142 | 143 | # layers and glyphs 144 | 145 | def writeLayerContents(self): 146 | """ 147 | Write the layer contents. 148 | """ 149 | self._fileSystem.writeLayerContents() 150 | 151 | def getLayerNames(self): 152 | """ 153 | Get the ordered layer names. 154 | """ 155 | return self._fileSystem.getLayerNames() 156 | 157 | def getDefaultLayerName(self): 158 | """ 159 | Get the default layer name. 160 | """ 161 | return self._fileSystem.getDefaultLayerName() 162 | 163 | def writeGlyphSetContents(self, layerName): 164 | """ 165 | Write the glyph set contents for the given layer name. 166 | """ 167 | self._fileSystem.writeGlyphSetContents(layerName) 168 | 169 | def getGlyphNames(self, layerName): 170 | """ 171 | Return a list of glyph names. 172 | """ 173 | return self._fileSystem.getGlyphNames(layerName) 174 | 175 | def readGlyph(self, layerName, glyphName, glyphObject): 176 | """ 177 | Read a glyph from a layer. 178 | """ 179 | tree = self._fileSystem.readGlyph(layerName, glyphName) 180 | readGlyphFromTree(tree, glyphObject, glyphObject) 181 | 182 | def writeGlyph(self, layerName, glyphName, glyphObject): 183 | """ 184 | Write a glyph from a layer. 185 | """ 186 | tree = writeGlyphToTree(glyphObject) 187 | self._fileSystem.writeGlyph(layerName, glyphName, tree) 188 | 189 | 190 | fontInfoAttributes = """ 191 | familyName 192 | styleName 193 | styleMapFamilyName 194 | styleMapStyleName 195 | versionMajor 196 | versionMinor 197 | year 198 | copyright 199 | trademark 200 | unitsPerEm 201 | descender 202 | xHeight 203 | capHeight 204 | ascender 205 | italicAngle 206 | note 207 | openTypeHeadCreated 208 | openTypeHeadLowestRecPPEM 209 | openTypeHeadFlags 210 | openTypeHheaAscender 211 | openTypeHheaDescender 212 | openTypeHheaLineGap 213 | openTypeHheaCaretSlopeRise 214 | openTypeHheaCaretSlopeRun 215 | openTypeHheaCaretOffset 216 | openTypeNameDesigner 217 | openTypeNameDesignerURL 218 | openTypeNameManufacturer 219 | openTypeNameManufacturerURL 220 | openTypeNameLicense 221 | openTypeNameLicenseURL 222 | openTypeNameVersion 223 | openTypeNameUniqueID 224 | openTypeNameDescription 225 | openTypeNamePreferredFamilyName 226 | openTypeNamePreferredSubfamilyName 227 | openTypeNameCompatibleFullName 228 | openTypeNameSampleText 229 | openTypeNameWWSFamilyName 230 | openTypeNameWWSSubfamilyName 231 | openTypeOS2WidthClass 232 | openTypeOS2WeightClass 233 | openTypeOS2Selection 234 | openTypeOS2VendorID 235 | openTypeOS2Panose 236 | openTypeOS2FamilyClass 237 | openTypeOS2UnicodeRanges 238 | openTypeOS2CodePageRanges 239 | openTypeOS2TypoAscender 240 | openTypeOS2TypoDescender 241 | openTypeOS2TypoLineGap 242 | openTypeOS2WinAscent 243 | openTypeOS2WinDescent 244 | openTypeOS2Type 245 | openTypeOS2SubscriptXSize 246 | openTypeOS2SubscriptYSize 247 | openTypeOS2SubscriptXOffset 248 | openTypeOS2SubscriptYOffset 249 | openTypeOS2SuperscriptXSize 250 | openTypeOS2SuperscriptYSize 251 | openTypeOS2SuperscriptXOffset 252 | openTypeOS2SuperscriptYOffset 253 | openTypeOS2StrikeoutSize 254 | openTypeOS2StrikeoutPosition 255 | openTypeVheaVertTypoAscender 256 | openTypeVheaVertTypoDescender 257 | openTypeVheaVertTypoLineGap 258 | openTypeVheaCaretSlopeRise 259 | openTypeVheaCaretSlopeRun 260 | openTypeVheaCaretOffset 261 | postscriptFontName 262 | postscriptFullName 263 | postscriptSlantAngle 264 | postscriptUniqueID 265 | postscriptUnderlineThickness 266 | postscriptUnderlinePosition 267 | postscriptIsFixedPitch 268 | postscriptBlueValues 269 | postscriptOtherBlues 270 | postscriptFamilyBlues 271 | postscriptFamilyOtherBlues 272 | postscriptStemSnapH 273 | postscriptStemSnapV 274 | postscriptBlueFuzz 275 | postscriptBlueShift 276 | postscriptBlueScale 277 | postscriptForceBold 278 | postscriptDefaultWidthX 279 | postscriptNominalWidthX 280 | postscriptWeightName 281 | postscriptDefaultCharacter 282 | postscriptWindowsCharacterSet 283 | macintoshFONDFamilyID 284 | macintoshFONDName 285 | versionMinor 286 | unitsPerEm 287 | openTypeHeadLowestRecPPEM 288 | openTypeHheaAscender 289 | openTypeHheaDescender 290 | openTypeHheaLineGap 291 | openTypeHheaCaretOffset 292 | openTypeOS2Panose 293 | openTypeOS2TypoAscender 294 | openTypeOS2TypoDescender 295 | openTypeOS2TypoLineGap 296 | openTypeOS2WinAscent 297 | openTypeOS2WinDescent 298 | openTypeOS2SubscriptXSize 299 | openTypeOS2SubscriptYSize 300 | openTypeOS2SubscriptXOffset 301 | openTypeOS2SubscriptYOffset 302 | openTypeOS2SuperscriptXSize 303 | openTypeOS2SuperscriptYSize 304 | openTypeOS2SuperscriptXOffset 305 | openTypeOS2SuperscriptYOffset 306 | openTypeOS2StrikeoutSize 307 | openTypeOS2StrikeoutPosition 308 | openTypeGaspRangeRecords 309 | openTypeNameRecords 310 | openTypeVheaVertTypoAscender 311 | openTypeVheaVertTypoDescender 312 | openTypeVheaVertTypoLineGap 313 | openTypeVheaCaretOffset 314 | woffMajorVersion 315 | woffMinorVersion 316 | woffMetadataUniqueID 317 | woffMetadataVendor 318 | woffMetadataCredits 319 | woffMetadataDescription 320 | woffMetadataLicense 321 | woffMetadataCopyright 322 | woffMetadataTrademark 323 | woffMetadataLicensee 324 | woffMetadataExtensions 325 | guidelines 326 | """.strip().split() -------------------------------------------------------------------------------- /fileStructureTests/core/xmlUtilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miscellaneous XML tools. 3 | """ 4 | 5 | from environment import ET 6 | 7 | def treeToString(tree, header): 8 | indentTree(tree) 9 | xml = ET.tostring(tree) 10 | if header is not None: 11 | xml = header.splitlines() + [xml] 12 | xml = "\n".join(xml) 13 | return xml 14 | 15 | def indentTree(elem, whitespace="\t", level=0): 16 | # taken from http://effbot.org/zone/element-lib.htm#prettyprint 17 | i = "\n" + level * whitespace 18 | if len(elem): 19 | if not elem.text or not elem.text.strip(): 20 | elem.text = i + whitespace 21 | if not elem.tail or not elem.tail.strip(): 22 | elem.tail = i 23 | for elem in elem: 24 | indentTree(elem, whitespace, level+1) 25 | if not elem.tail or not elem.tail.strip(): 26 | elem.tail = i 27 | else: 28 | if level and (not elem.tail or not elem.tail.strip()): 29 | elem.tail = i -------------------------------------------------------------------------------- /fileStructureTests/singleXML.py: -------------------------------------------------------------------------------- 1 | """ 2 | Single XML File System 3 | ---------------------- 4 | 5 | This implements one big XML file. This is the schema: 6 | 7 | 8 | plist 9 | plist 10 | plist 11 | plist 12 | text 13 | 14 | glif 15 | 16 | 17 | """ 18 | 19 | import os 20 | from collections import OrderedDict 21 | from core.environment import ET 22 | from core.fileSystem import BaseFileSystem 23 | from core.plistTree import convertPlistToTree 24 | 25 | class SingleXMLFileSystem(BaseFileSystem): 26 | 27 | fileExtension = 'xml' 28 | 29 | def __init__(self, path): 30 | super(SingleXMLFileSystem, self).__init__() 31 | self.needFileWrite = False 32 | self.path = path 33 | if os.path.exists(path): 34 | f = open(path, "rb") 35 | data = f.read() 36 | f.close() 37 | self.tree = ET.fromstring(data) 38 | else: 39 | self.tree = ET.Element("font") 40 | 41 | def close(self): 42 | if self.needFileWrite: 43 | _indent(self.tree) 44 | data = ET.tostring(self.tree) 45 | f = open(self.path, "wb") 46 | f.write(data) 47 | f.close() 48 | self.needFileWrite = False 49 | 50 | # ------------ 51 | # File Support 52 | # ------------ 53 | 54 | # bytes <-> location 55 | 56 | """ 57 | Raw bytes are not stored in this format. 58 | Everything is wrapped in a tree and is thus 59 | handled by the tree reading and writing methods. 60 | """ 61 | 62 | def readBytesFromLocation(self, location): 63 | raise NotImplementedError 64 | 65 | def writeBytesToLocation(self, data, location): 66 | raise NotImplementedError 67 | 68 | # tree <-> location 69 | 70 | """ 71 | Tree read and write are overriden here to 72 | implement the custom XML file structure that 73 | encapsulates the font data. 74 | """ 75 | 76 | _locationTags = { 77 | "fontinfo.plist" : "fontinfo", 78 | "groups.plist" : "groups", 79 | "kerning.plist" : "kerning", 80 | "features.fea" : "features", 81 | "lib.plist" : "lib", 82 | } 83 | 84 | def readTreeFromLocation(self, location): 85 | if isinstance(location, dict): 86 | if location["type"] == "glyph": 87 | return self._readGlyphFromLayer(location) 88 | else: 89 | tag = self._locationTags[location] 90 | element = self.tree.find(tag) 91 | return element 92 | 93 | def _readGlyphFromLayer(self, location): 94 | layerName = location["layer"] 95 | glyphName = location["name"] 96 | path = "glyphs[@name='%s']/glyph[@name='%s']" % (layerName, glyphName) 97 | return self.tree.find(path) 98 | 99 | def writeTreeToLocation(self, tree, location, header=None): 100 | self.needFileWrite = True 101 | if isinstance(location, dict): 102 | if location["type"] == "glyph": 103 | self._writeGlyphToLayer(tree, location) 104 | else: 105 | tag = self._locationTags[location] 106 | tree.tag = tag 107 | existing = self.tree.find(tag) 108 | if existing is not None: 109 | index = self.tree.getchildren().index(existing) 110 | self.tree.remove(existing) 111 | self.tree.insert(index, tree) 112 | else: 113 | self.tree.append(tree) 114 | 115 | def _writeGlyphToLayer(self, tree, location): 116 | layerName = location["layer"] 117 | glyphName = location["name"] 118 | # find the layer element 119 | found = False 120 | path = "glyphs[@name='%s']" % layerName 121 | layerElement = self.tree.find(path) 122 | if layerElement is None: 123 | layerElement = ET.Element("glyphs") 124 | layerElement.attrib["name"] = layerName 125 | self.tree.append(layerElement) 126 | # store the glyph 127 | tree.tag = "glyph" 128 | existing = layerElement.find("glyph[@name='%s']" % glyphName) 129 | if existing is not None: 130 | index = layerElement.getchildren().index(existing) 131 | layerElement.remove(existing) 132 | layerElement.insert(index, tree) 133 | else: 134 | layerElement.append(tree) 135 | 136 | # --------------- 137 | # Top Level Files 138 | # --------------- 139 | 140 | """ 141 | The metainfo.plist data is stored as the attributes 142 | of the root element instead of a separate element. 143 | """ 144 | 145 | def readMetaInfo(self): 146 | tree = convertPlistToTree(self.tree.attrib) 147 | return tree 148 | 149 | def writeMetaInfo(self, tree): 150 | attrib = { 151 | k : str(v) for k, v in tree.items() 152 | } 153 | self.tree.attrib.update(attrib) 154 | 155 | """ 156 | The contents of features.fea are wrapped in 157 | a element. 158 | """ 159 | 160 | def readFeatures(self): 161 | tree = self.readTreeFromLocation("features.fea") 162 | if tree is None: 163 | return None 164 | return tree.text 165 | 166 | def writeFeatures(self, tree): 167 | # convert to an element 168 | element = ET.Element("features") 169 | element.text = tree 170 | self.writeTreeToLocation(tree, "features.fea") 171 | 172 | # ----------------- 173 | # Layers and Glyphs 174 | # ----------------- 175 | 176 | """ 177 | layercontents.plist is implied. 178 | """ 179 | 180 | def readLayerContents(self): 181 | layerContents = OrderedDict() 182 | for layerElement in self.tree.findall("glyphs"): 183 | layerName = layerElement.attrib["name"] 184 | layerContents[layerName] = layerName 185 | return layerContents 186 | 187 | def writeLayerContents(self): 188 | pass 189 | 190 | """ 191 | glyphs*/contents.plist is implied. 192 | """ 193 | 194 | def readGlyphSetContents(self, layerName): 195 | glyphSetContents = {} 196 | layerElement = self.tree.find("glyphs[@name='%s']" % layerName) 197 | if layerElement is not None: 198 | for glyphElement in layerElement.findall("glyph"): 199 | glyphName = glyphElement.attrib["name"] 200 | glyphSetContents[glyphName] = glyphName 201 | return glyphSetContents 202 | 203 | def writeGlyphSetContents(self, layerName): 204 | pass 205 | 206 | def readGlyph(self, layerName, glyphName): 207 | path = dict(type="glyph", layer=layerName, name=glyphName) 208 | tree = self.readTreeFromLocation(path) 209 | return tree 210 | 211 | def writeGlyph(self, layerName, glyphName, tree): 212 | path = dict(type="glyph", layer=layerName, name=glyphName) 213 | self.writeTreeToLocation(tree, path) 214 | 215 | 216 | def _indent(elem, whitespace="\t", level=0): 217 | # taken from http://effbot.org/zone/element-lib.htm#prettyprint 218 | i = "\n" + level * whitespace 219 | if len(elem): 220 | if not elem.text or not elem.text.strip(): 221 | elem.text = i + whitespace 222 | if not elem.tail or not elem.tail.strip(): 223 | elem.tail = i 224 | for elem in elem: 225 | _indent(elem, whitespace, level+1) 226 | if not elem.tail or not elem.tail.strip(): 227 | elem.tail = i 228 | else: 229 | if level and (not elem.tail or not elem.tail.strip()): 230 | elem.tail = i 231 | 232 | if __name__ == "__main__": 233 | from core.fileSystem import debugWriteFont, debugReadFont, debugRoundTripFont 234 | debugWriteFont(SingleXMLFileSystem) 235 | debugReadFont(SingleXMLFileSystem) 236 | diffs = debugRoundTripFont(SingleXMLFileSystem) 237 | if diffs: 238 | print diffs 239 | -------------------------------------------------------------------------------- /fileStructureTests/sqlite.py: -------------------------------------------------------------------------------- 1 | """ 2 | UFO 3 sqlLite File System 3 | ----------------- 4 | 5 | This implements an on-disk, compressed (sqlLite) package 6 | structure. 7 | """ 8 | 9 | import os 10 | import sqlite3 11 | 12 | from core.fileSystem import BaseFileSystem 13 | 14 | class SqliteFileSystem(BaseFileSystem): 15 | 16 | fileExtension = 'ufodb' 17 | 18 | def __init__(self, path): 19 | super(SqliteFileSystem, self).__init__() 20 | self.path = path 21 | # connect to a db 22 | self.db = sqlite3.connect(self.path) 23 | # create the base table if is doenst exists yet 24 | self.db.execute('CREATE TABLE IF NOT EXISTS data(location TEXT PRIMARY KEY, bytes TEXT)') 25 | 26 | def close(self): 27 | # commit all changes to the db 28 | self.db.commit() 29 | # close the db 30 | self.db.close() 31 | 32 | # ------------ 33 | # File Support 34 | # ------------ 35 | 36 | # locations 37 | 38 | def joinLocations(self, location1, *location2): 39 | return os.path.join(location1, *location2) 40 | 41 | def splitLocation(self, location): 42 | return os.path.split(location) 43 | 44 | # bytes <-> location 45 | 46 | def readBytesFromLocation(self, location): 47 | cursor = self.db.execute('SELECT bytes FROM data WHERE location=?', (location,)) 48 | data = cursor.fetchone() 49 | if data: 50 | return data[0] 51 | return None 52 | 53 | def writeBytesToLocation(self, data, location): 54 | try: 55 | self.db.execute('INSERT INTO data VALUES (?, ?)', (location, data)) 56 | except sqlite3.IntegrityError: 57 | self.db.execute('UPDATE data SET bytes=? WHERE location=?', (data, location)) 58 | 59 | 60 | 61 | 62 | 63 | 64 | if __name__ == "__main__": 65 | from core.fileSystem import debugWriteFont, debugReadFont, debugRoundTripFont 66 | debugWriteFont(SqliteFileSystem) 67 | debugReadFont(SqliteFileSystem) 68 | diffs = debugRoundTripFont(SqliteFileSystem) 69 | if diffs: 70 | print diffs -------------------------------------------------------------------------------- /fileStructureTests/testAll.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import time 5 | from core.ufoReaderWriter import UFOReaderWriter 6 | from core.fonts import compileFont 7 | from core.objects import Font 8 | 9 | # ------------ 10 | # File Systems 11 | # ------------ 12 | 13 | fileSystems = {} 14 | 15 | from ufo3 import UFO3FileSystem 16 | fileSystems["UFO 3"] = UFO3FileSystem 17 | 18 | from singleXML import SingleXMLFileSystem 19 | fileSystems["Single XML"] = SingleXMLFileSystem 20 | 21 | from ufo3zip import UFO3ZipFileSystem 22 | fileSystems["UFO 3 Zipped"] = UFO3ZipFileSystem 23 | 24 | from sqlite import SqliteFileSystem 25 | fileSystems["Flat SQLite DB"] = SqliteFileSystem 26 | 27 | # ---------- 28 | # Test Fonts 29 | # ---------- 30 | 31 | testFonts = [ 32 | ("file structure building test", "Test font only to be used when developing file structures.") 33 | ] 34 | 35 | # -------------- 36 | # Test Functions 37 | # -------------- 38 | 39 | """ 40 | Needed Tests 41 | ------------ 42 | 43 | - Remove glyphs. 44 | This will require a BaseFileSystem modification. 45 | Something like removeBytesFromLocation. 46 | - Test memory usage of the file system after a full read. 47 | sys.getsizeof will give some info. It won't be perfect 48 | but it will still be useful to see the values. 49 | - Retrieve a cmap for each layer. 50 | """ 51 | 52 | tests = {} 53 | 54 | # Tests are stored as dicts: 55 | # "test name" : { 56 | # function : test function, 57 | # reading : bool indicating if the function reads a file, 58 | # writing : bool indicating if the function writes a file, 59 | # time : bool indicating if the function should be timed (optional, default is False), 60 | # } 61 | 62 | 63 | def testFileSize(fileSystem=None, font=None, path=None, **kwargs): 64 | """ 65 | Test the resulting size of a written file. 66 | """ 67 | writer = UFOReaderWriter(fileSystem) 68 | writer.writeMetaInfo() 69 | writer.writeInfo(font.info) 70 | writer.writeGroups(font.groups) 71 | writer.writeKerning(font.kerning) 72 | writer.writeLib(font.lib) 73 | writer.writeFeatures(font.features) 74 | for layerName, layer in font.layers.items(): 75 | for glyph in layer: 76 | glyphName = glyph.name 77 | writer.writeGlyph(layerName, glyphName, glyph) 78 | writer.writeGlyphSetContents(layerName) 79 | writer.writeLayerContents() 80 | writer.close() 81 | size = _getFileSize(path) 82 | size = "{:,d} bytes".format(size) 83 | return size 84 | 85 | def _getFileSize(path): 86 | if path.startswith("."): 87 | return 0 88 | if os.path.isdir(path): 89 | total = 0 90 | for p in os.listdir(path): 91 | total += _getFileSize(os.path.join(path, p)) 92 | return total 93 | else: 94 | return os.stat(path).st_size 95 | 96 | tests["File Size"] = dict( 97 | function=testFileSize, 98 | reading=False, 99 | writing=True 100 | ) 101 | 102 | def testFullWrite(fileSystem=None, font=None, **kwargs): 103 | """ 104 | Fully write a new font. 105 | """ 106 | writer = UFOReaderWriter(fileSystem) 107 | writer.writeMetaInfo() 108 | writer.writeInfo(font.info) 109 | writer.writeGroups(font.groups) 110 | writer.writeKerning(font.kerning) 111 | writer.writeLib(font.lib) 112 | writer.writeFeatures(font.features) 113 | for layerName, layer in font.layers.items(): 114 | for glyph in layer: 115 | glyphName = glyph.name 116 | writer.writeGlyph(layerName, glyphName, glyph) 117 | writer.writeGlyphSetContents(layerName) 118 | writer.writeLayerContents() 119 | writer.close() 120 | 121 | tests["Full Write"] = dict( 122 | function=testFullWrite, 123 | reading=False, 124 | writing=True, 125 | time=True 126 | ) 127 | 128 | def testFullRead(fileSystem=None, font=None, **kwargs): 129 | """ 130 | Fully load an entire font. 131 | """ 132 | font = Font() 133 | reader = UFOReaderWriter(fileSystem) 134 | reader.readMetaInfo() 135 | reader.readInfo(font.info) 136 | font.groups = reader.readGroups() 137 | font.kerning = reader.readKerning() 138 | font.lib = reader.readLib() 139 | font.features = reader.readFeatures() 140 | font.loadLayers(reader) 141 | for layer in font.layers.values(): 142 | for glyph in layer: 143 | pass 144 | 145 | tests["Full Read"] = dict( 146 | function=testFullRead, 147 | reading=True, 148 | writing=False, 149 | time=True 150 | ) 151 | 152 | def testPartialRead(fileSystem=None, font=None, **kwargs): 153 | """ 154 | Load 25% of the glyphs in the font. 155 | """ 156 | font = Font() 157 | reader = UFOReaderWriter(fileSystem) 158 | reader.readMetaInfo() 159 | glyphNames = [] 160 | font.loadLayers(reader) 161 | for layerName, layer in font.layers.items(): 162 | for glyphName in layer.keys(): 163 | glyphNames.append((glyphName, layerName)) 164 | glyphNames.sort() 165 | glyphCount = int(len(glyphNames) * 0.25) 166 | glyphNames = glyphNames[:glyphCount] 167 | for glyphName, layerName in glyphNames: 168 | layer = font.layers[layerName] 169 | glyph = layer[glyphName] 170 | 171 | tests["Partial Read"] = dict( 172 | function=testPartialRead, 173 | reading=True, 174 | writing=False, 175 | time=True 176 | ) 177 | 178 | def testPartialWrite(fileSystem=None, font=None, **kwargs): 179 | """ 180 | Write 25% of the glyphs in the font. 181 | """ 182 | font = Font() 183 | # initialize 184 | reader = UFOReaderWriter(fileSystem) 185 | reader.readMetaInfo() 186 | # modify 187 | glyphNames = [] 188 | font.loadLayers(reader) 189 | for layerName, layer in font.layers.items(): 190 | for glyphName in layer.keys(): 191 | glyphNames.append((glyphName, layerName)) 192 | glyphNames.sort() 193 | glyphCount = int(len(glyphNames) * 0.25) 194 | glyphNames = glyphNames[:glyphCount] 195 | for glyphName, layerName in glyphNames: 196 | layer = font.layers[layerName] 197 | glyph = layer[glyphName] 198 | glyph.note = "partial modify" 199 | # write 200 | writer = reader 201 | writer.writeMetaInfo() 202 | for glyphName, layerName in glyphNames: 203 | layer = font.layers[layerName] 204 | glyph = layer[glyphName] 205 | writer.writeGlyph(layerName, glyphName, glyph) 206 | for layerName in font.layers.keys(): 207 | writer.writeGlyphSetContents(layerName) 208 | writer.writeLayerContents() 209 | writer.close() 210 | 211 | tests["Partial Write"] = dict( 212 | function=testPartialWrite, 213 | reading=True, 214 | writing=False, 215 | time=True 216 | ) 217 | 218 | def testDropboxWrite(fileSystem=None, font=None, **kwargs): 219 | """ 220 | Upload the output of each fileSystem to your dropbox account 221 | 222 | A 'dropbox.txt' with an auth token is required in the same directory. 223 | This file will be ignored by git. 224 | 225 | Required is the dropbox python API 226 | see https://www.dropbox.com/developers/core/docs/python 227 | 228 | and a auth token should be request first! 229 | see https://www.dropbox.com/developers/core/start/python 230 | 231 | result from Frederik (just in case you dont want to install everything) 232 | 233 | ---------------------- 234 | Upload font to dropBox 235 | ---------------------- 236 | 237 | file structure building test 238 | ---------------------------- 239 | Flat SQLite DB: 2.70396995544 (1 files) 240 | Single XML: 1.93647909164 (1 files) 241 | UFO 3: 443.017402172 (554 files) 242 | UFO 3 Zipped: 1.76858496666 (1 files) 243 | 244 | (result are very much depending on connection speed) 245 | """ 246 | dropBoxAuthTokePath = 'dropbox.txt' 247 | if not os.path.exists(dropBoxAuthTokePath): 248 | # do nothing is there is no auth token file 249 | return "no '%s' file, skip this test." % dropBoxAuthTokePath 250 | try: 251 | # do nothing if dropbox python api is not installed 252 | import dropbox 253 | except: 254 | return "dropbox python api is not installed" 255 | f = open(dropBoxAuthTokePath, 'r') 256 | dropBoxAuthToken = f.read() 257 | f.close() 258 | 259 | try: 260 | client = dropbox.client.DropboxClient(dropBoxAuthToken) 261 | client.account_info() 262 | except: 263 | # do nothing if the auth token is not valid 264 | return "Auth token is not valid, not able to connect." 265 | dropBoxRoot = "/ufoResearchTest" 266 | path = kwargs['path'] 267 | root = os.path.dirname(path) 268 | # set a file 269 | setupFile(font, fileSystem) 270 | paths = _getAllFilesForPath(path) 271 | try: 272 | # start the time 273 | start = time.time() 274 | for fileRoot, fileName in paths: 275 | # set all the correct paths 276 | filePath = os.path.join(fileRoot, fileName) 277 | relativePath = os.path.relpath(filePath, root) 278 | dropBoxPath = os.path.join(dropBoxRoot, relativePath) 279 | # open the file 280 | f = open(filePath) 281 | # send the file to your dropbox client 282 | response = client.put_file(dropBoxPath, f) 283 | # close the file 284 | f.close() 285 | # get the duration 286 | result = "%s (%s file%s)" % (time.time() - start, len(paths), "s"[len(paths)==1:]) 287 | except: 288 | # import traceback 289 | # print "%s: Oeps" % fileSystemName 290 | # print traceback.format_exc(5) 291 | result = "Failed to upload to dropbox" 292 | return result 293 | 294 | def _getAllFilesForPath(fontFile): 295 | # get files recursively if 'fontFile' is a folder 296 | paths = [] 297 | if os.path.isdir(fontFile): 298 | for root, dirNames, fileNames in os.walk(fontFile): 299 | for fileName in fileNames: 300 | paths.append((root, fileName)) 301 | else: 302 | root = os.path.dirname(fontFile) 303 | fileName = os.path.basename(fontFile) 304 | paths = [(root, fileName)] 305 | return paths 306 | 307 | tests["Dropbox Write"] = dict( 308 | function=testDropboxWrite, 309 | reading=False, 310 | writing=True, 311 | time=False 312 | ) 313 | 314 | # ------- 315 | # Support 316 | # ------- 317 | 318 | def setupFile(font, fileSystem): 319 | writer = UFOReaderWriter(fileSystem) 320 | writer.writeMetaInfo() 321 | writer.writeInfo(font.info) 322 | writer.writeGroups(font.groups) 323 | writer.writeKerning(font.kerning) 324 | writer.writeLib(font.lib) 325 | writer.writeFeatures(font.features) 326 | for layerName, layer in font.layers.items(): 327 | for glyph in layer: 328 | glyphName = glyph.name 329 | writer.writeGlyph(layerName, glyphName, glyph) 330 | writer.writeGlyphSetContents(layerName) 331 | writer.writeLayerContents() 332 | writer.close() 333 | 334 | def tearDownFile(path): 335 | if os.path.exists(path): 336 | if os.path.isdir(path): 337 | shutil.rmtree(path) 338 | else: 339 | os.remove(path) 340 | 341 | # ------- 342 | # Execute 343 | # ------- 344 | 345 | def execute(): 346 | for testName, testData in sorted(tests.items()): 347 | print 348 | print "-" * len(testName) 349 | print testName 350 | print "-" * len(testName) 351 | print 352 | 353 | for fontName, description in sorted(testFonts): 354 | print fontName 355 | print "-" * len(fontName) 356 | 357 | font = compileFont(fontName) 358 | 359 | for fileSystemName, fileSystemClass in sorted(fileSystems.items()): 360 | path = tempfile.mkstemp(suffix=".%s" %fileSystemClass.fileExtension)[1] 361 | tearDownFile(path) 362 | reading = testData["reading"] 363 | writing = testData["writing"] 364 | # setup 365 | if reading: 366 | fs = fileSystemClass(path) 367 | setupFile(font, fs) 368 | del fs 369 | # test 370 | try: 371 | func = testData["function"] 372 | # timed 373 | if testData.get("time", False): 374 | times = [] 375 | for i in range(7): 376 | start = time.time() 377 | fileSystem = fileSystemClass(path) 378 | func( 379 | fileSystem=fileSystem, 380 | font=font, 381 | path=path 382 | ) 383 | total = time.time() - start 384 | times.append(total) 385 | if not reading and writing: 386 | tearDownFile(path) 387 | times.sort() 388 | times = times[1:-1] 389 | result = sum(times) / 5.0 390 | # other (function returns result) 391 | else: 392 | fileSystem = fileSystemClass(path) 393 | result = func( 394 | fileSystem=fileSystem, 395 | font=font, 396 | path=path 397 | ) 398 | if not reading and writing: 399 | tearDownFile(path) 400 | print "%s:" % fileSystemName, result 401 | # tear down 402 | except: 403 | import traceback 404 | print "%s: Oeps" % fileSystemName 405 | print traceback.format_exc(5) 406 | finally: 407 | tearDownFile(path) 408 | 409 | if __name__ == "__main__": 410 | execute() 411 | -------------------------------------------------------------------------------- /fileStructureTests/ufo3.py: -------------------------------------------------------------------------------- 1 | """ 2 | UFO 3 File System 3 | ----------------- 4 | 5 | This implements an on-disk, uncompressed package 6 | structure. thus, it is identical to UFO 3. 7 | """ 8 | 9 | import os 10 | from core.fileSystem import BaseFileSystem 11 | 12 | class UFO3FileSystem(BaseFileSystem): 13 | 14 | fileExtension = 'ufo' 15 | 16 | def __init__(self, path): 17 | super(UFO3FileSystem, self).__init__() 18 | self.path = path 19 | if not os.path.exists(self.path): 20 | os.mkdir(path) 21 | 22 | # ------------ 23 | # File Support 24 | # ------------ 25 | 26 | # locations 27 | 28 | def joinLocations(self, location1, *location2): 29 | return os.path.join(location1, *location2) 30 | 31 | def splitLocation(self, location): 32 | return os.path.split(location) 33 | 34 | # bytes <-> location 35 | 36 | def readBytesFromLocation(self, location): 37 | path = os.path.join(self.path, location) 38 | if not os.path.exists(path): 39 | return None 40 | f = open(path, "rb") 41 | b = f.read() 42 | f.close() 43 | return b 44 | 45 | def writeBytesToLocation(self, data, location): 46 | parts = [] 47 | b = location 48 | while 1: 49 | b, p = os.path.split(b) 50 | parts.insert(0, p) 51 | if not b: 52 | break 53 | for d in parts[:-1]: 54 | p = os.path.join(self.path, d) 55 | if not os.path.exists(p): 56 | os.mkdir(p) 57 | path = os.path.join(self.path, location) 58 | f = open(path, "wb") 59 | f.write(data) 60 | f.close() 61 | 62 | 63 | if __name__ == "__main__": 64 | from core.fileSystem import debugWriteFont, debugReadFont, debugRoundTripFont 65 | debugWriteFont(UFO3FileSystem) 66 | debugReadFont(UFO3FileSystem) 67 | diffs = debugRoundTripFont(UFO3FileSystem) 68 | if diffs: 69 | print diffs 70 | -------------------------------------------------------------------------------- /fileStructureTests/ufo3zip.py: -------------------------------------------------------------------------------- 1 | """ 2 | UFO 3 Zip File System 3 | ----------------- 4 | 5 | This implements an on-disk, compressed (zip) package 6 | structure. 7 | """ 8 | 9 | import os 10 | import tempfile 11 | import shutil 12 | import zipfile 13 | 14 | from core.fileSystem import BaseFileSystem 15 | 16 | class UFO3ZipFileSystem(BaseFileSystem): 17 | 18 | fileExtension = 'ufoz' 19 | 20 | def __init__(self, path): 21 | super(UFO3ZipFileSystem, self).__init__() 22 | self.needFileWrite = False 23 | self.path = path 24 | # first being lazy and allow to the archive to append files 25 | # this is not good and will lead to huge zip files... 26 | self.zip = zipfile.ZipFile(self.path, 'a') 27 | 28 | def close(self): 29 | self.zip.close() 30 | namelist = self.zip.namelist() 31 | hasDuplicates = len(namelist) != len(set(namelist)) 32 | if hasDuplicates: 33 | temp = tempfile.mkstemp(suffix=".%s" % self.fileExtension)[1] 34 | inzip = zipfile.ZipFile(self.path, 'r') 35 | outzip = zipfile.ZipFile(temp, 'w') 36 | outzip.comment = inzip.comment 37 | for item in inzip.infolist(): 38 | if item.filename not in outzip.namelist(): 39 | data = inzip.read(item.filename) 40 | outzip.writestr(item, data, compress_type=item.compress_type) 41 | outzip.close() 42 | inzip.close() 43 | shutil.move(temp, self.path) 44 | 45 | # ------------ 46 | # File Support 47 | # ------------ 48 | 49 | # locations 50 | 51 | def joinLocations(self, location1, *location2): 52 | return os.path.join(location1, *location2) 53 | 54 | def splitLocation(self, location): 55 | return os.path.split(location) 56 | 57 | # bytes <-> location 58 | 59 | def readBytesFromLocation(self, location): 60 | try: 61 | return self.zip.read(location) 62 | except KeyError: 63 | return None 64 | 65 | def writeBytesToLocation(self, data, location): 66 | self.zip.writestr(location, data, compress_type=zipfile.ZIP_DEFLATED) 67 | 68 | 69 | 70 | if __name__ == "__main__": 71 | from core.fileSystem import debugWriteFont, debugReadFont, debugRoundTripFont 72 | debugWriteFont(UFO3ZipFileSystem) 73 | debugReadFont(UFO3ZipFileSystem) 74 | diffs = debugRoundTripFont(UFO3ZipFileSystem) 75 | if diffs: 76 | print diffs 77 | -------------------------------------------------------------------------------- /fontSizeReporter/UFOStats.py: -------------------------------------------------------------------------------- 1 | """ 2 | This will dump an anonymous profile of a given font, 3 | for each font in a directory and sub-directories. 4 | 5 | To Do 6 | ----- 7 | 8 | General: 9 | - add support for KeyboardInterrupt 10 | - build a vanilla interface for use in RoboFont and Glyphs 11 | - write documentation 12 | 13 | OTF: 14 | 15 | defcon: 16 | 17 | RoboFab: 18 | - implement 19 | 20 | RoboFont: 21 | - implement 22 | 23 | Glyphs: 24 | - implement 25 | """ 26 | 27 | import md5 28 | import os 29 | import optparse 30 | from fontTools.ttLib import TTFont 31 | from fontTools.pens.basePen import BasePen 32 | 33 | supportedFormats = set((".otf", ".ttf")) 34 | 35 | # --------------------- 36 | # Environment Detection 37 | # --------------------- 38 | 39 | haveExtractor = False 40 | haveDefcon = False 41 | haveRoboFab = False 42 | haveRoboFont = False 43 | haveGlyphsApp = False 44 | 45 | try: 46 | import extractor 47 | haveExtractor = True 48 | except ImportError: 49 | pass 50 | 51 | try: 52 | import defcon 53 | haveDefcon = True 54 | supportedFormats.add(".ufo") 55 | except ImportError: 56 | pass 57 | 58 | try: 59 | import robofab 60 | haveRoboFab = True 61 | supportedFormats.add(".ufo") 62 | except ImportError: 63 | pass 64 | 65 | try: 66 | import mojo 67 | haveRoboFont = True 68 | supportedFormats.add(".ufo") 69 | supportedFormats.add(".vfb") 70 | except ImportError: 71 | pass 72 | 73 | # try: 74 | # import ? 75 | # haveGlyphsApp = True 76 | # except ImportError: 77 | # pass 78 | 79 | # ------------- 80 | # Profile: Font 81 | # ------------- 82 | 83 | def getFileSize(path): 84 | total = 0 85 | if os.path.isdir(path): 86 | for p in os.listdir(path): 87 | if p.startswith("."): 88 | continue 89 | p = os.path.join(path, p) 90 | total += getFileSize(p) 91 | else: 92 | total += os.stat(path).st_size 93 | return total 94 | 95 | def profileFont(path): 96 | """ 97 | Return a profile of a font. 98 | """ 99 | profile = dict( 100 | # environment data 101 | outputEnvironment="Unknown", 102 | sourceFormat="Unknown", 103 | sourceSize=getFileSize(path), 104 | # mapping of (x, y) : count for entire font. 105 | # will be converted to a hash later. 106 | fingerprinter={}, 107 | # number of glyphs per layer 108 | glyphs=[], 109 | # length of glyph notes 110 | glyphNotes=0, 111 | # total number of contours 112 | contours=0, 113 | # mapping of contour counts : number of glyphs with this number of contours 114 | contourOccurance={}, 115 | # total number of segments 116 | segments=0, 117 | # mapping of segment count : number of contours with this segment count 118 | segmentOccurance={}, 119 | # overall segment type counts 120 | segmentTypes=dict( 121 | moveTo=0, 122 | lineTo=0, 123 | curveTo=0, 124 | qCurveTo=0 125 | ), 126 | # total number of components 127 | components=0, 128 | # component transformations 129 | componentOccurance={}, 130 | # kerning pairs 131 | kerning=0, 132 | # groups 133 | groups=[], 134 | # characters in features 135 | features=0, 136 | # characters in all font info strings 137 | fontInfo=0, 138 | ) 139 | # send to format specific profilers 140 | ext = os.path.splitext(path)[-1].lower() 141 | profile["sourceFormat"] = ext 142 | if ext in (".otf", ".ttf"): 143 | _profileFont_OTF(path, profile) 144 | elif ext == ".ufo" and (haveRoboFont or haveDefcon or haveRoboFab): 145 | if haveRoboFont: 146 | _profileFont_RoboFont(path, profile) 147 | elif haveDefcon: 148 | _profileFont_defcon(path, profile) 149 | elif haveRoboFab: 150 | _profileFont_RoboFab(path, profile) 151 | elif ext == ".glyphs" and haveGlyphsApp: 152 | _profileFont_Glyphs(path, profile) 153 | else: 154 | return 155 | # fingerprint 156 | fingerprintProfile(profile) 157 | # done 158 | return profile 159 | 160 | def _profileFont_OTF(path, profile): 161 | if haveExtractor and (haveDefcon or haveRoboFab): 162 | if haveDefcon: 163 | font = defcon.Font() 164 | elif haveRoboFab: 165 | font = robofab.objects.objectsRF.RFont() 166 | extractor.extractUFO(path, font) 167 | if haveDefcon: 168 | _profileFont_defcon(font, profile) 169 | elif haveRoboFab: 170 | _profileFont_RoboFab(font, profile) 171 | else: 172 | profile["outputEnvironment"] = "fontTools" 173 | font = TTFont(path) 174 | glyphNames = font.getGlyphOrder() 175 | glyphSet = font.getGlyphSet() 176 | profileGlyphSet(glyphNames, glyphSet) 177 | 178 | def _profileFont_RoboFont(path, profile): 179 | """ 180 | RoboFont specific profiler. 181 | """ 182 | from mojo.roboFont import RFont 183 | font = RFont(path, showUI=False) 184 | # the naked() object of a RFont in RoboFont 185 | # is a subclass of a defcon Font object 186 | _profileFont_defcon(font.naked(), profile) 187 | profile["outputEnvironment"] = "RoboFont" 188 | 189 | def _profileFont_Glyphs(path, profile): 190 | """ 191 | Glyphs specific profiler. 192 | """ 193 | profile["outputEnvironment"] = "Glyphs" 194 | raise NotImplementedError 195 | 196 | def _profileFont_defcon(path, profile): 197 | """ 198 | defcon specific profiler. 199 | """ 200 | profile["outputEnvironment"] = "defcon" 201 | if isinstance(path, defcon.Font): 202 | font = path 203 | else: 204 | font = defcon.Font(path) 205 | try: 206 | for layer in font.layers: 207 | profileGlyphSet(layer.keys(), layer, profile) 208 | except AttributeError: 209 | profileGlyphSet(font.keys(), font, profile) 210 | profileKerning(font.kerning, profile) 211 | profileGroups(font.groups, profile) 212 | profileFeatures(font.features.text, profile) 213 | profileFontInfo(font.info, profile) 214 | 215 | def _profileFont_RoboFab(path, profile): 216 | """ 217 | RoboFab specific profiler. 218 | """ 219 | profile["outputEnvironment"] = "RoboFab" 220 | raise NotImplementedError 221 | 222 | # --------------- 223 | # Profile: Glyphs 224 | # --------------- 225 | 226 | def profileGlyphSet(glyphNames, glyphSet, profile): 227 | """ 228 | Profile all glyphs in a glyph set. 229 | """ 230 | profile["glyphs"].append(len(glyphNames)) 231 | for glyphName in glyphNames: 232 | glyph = glyphSet[glyphName] 233 | profileGlyph(glyph, profile) 234 | if hasattr(glyph, "note"): 235 | note = glyph.note 236 | if isinstance(note, basestring): 237 | profile["glyphNotes"] += len(note) 238 | 239 | def profileGlyph(glyph, profile): 240 | """ 241 | Profile a glyph that supports the pen protocol. 242 | """ 243 | pen = ProfilePen(profile["fingerprinter"]) 244 | glyph.draw(pen) 245 | # contour counts 246 | contourCount = len(pen.contours) 247 | profile["contours"] += contourCount 248 | contourOccurance = profile["contourOccurance"] 249 | if contourCount not in contourOccurance: 250 | contourOccurance[contourCount] = 0 251 | contourOccurance[contourCount] += 1 252 | # segment counts 253 | segmentOccurance = profile["segmentOccurance"] 254 | for contour in pen.contours: 255 | segmentCount = contour["moveTo"] + contour["lineTo"] + contour["curveTo"] + contour["qCurveTo"] 256 | profile["segments"] += segmentCount 257 | if segmentCount not in segmentOccurance: 258 | segmentOccurance[segmentCount] = 0 259 | segmentOccurance[segmentCount] += 1 260 | # segment types 261 | profile["segmentTypes"]["moveTo"] += contour["moveTo"] 262 | profile["segmentTypes"]["lineTo"] += contour["lineTo"] 263 | profile["segmentTypes"]["curveTo"] += contour["curveTo"] 264 | profile["segmentTypes"]["qCurveTo"] += contour["qCurveTo"] 265 | # component counts 266 | profile["components"] += len(pen.components) 267 | for transformation in pen.components: 268 | if transformation not in profile["componentOccurance"]: 269 | profile["componentOccurance"][transformation] = 0 270 | profile["componentOccurance"][transformation] += 1 271 | 272 | def fingerprintProfile(profile): 273 | """ 274 | Generate a unique hash for the font contents. 275 | """ 276 | hashable = [] 277 | for (x, y), count in sorted(profile["fingerprinter"].items()): 278 | line = "%.1f %.1f %d" % (x, y, count) 279 | hashable.append(line) 280 | hashable = "\n".join(hashable) 281 | m = md5.md5() 282 | m.update(hashable) 283 | profile["fingerprint"] = m.hexdigest() 284 | del profile["fingerprinter"] 285 | 286 | 287 | class ProfilePen(BasePen): 288 | 289 | """ 290 | This will record the number of contours, 291 | segments (including their various types), 292 | and contours. It will simultaneously store 293 | all point locations for full font fingerprinting. 294 | """ 295 | 296 | def __init__(self, fingerprinter): 297 | self.contours = [] 298 | self.components = [] 299 | self._fingerprinter = fingerprinter 300 | 301 | def _logPoint(self, pt): 302 | if pt not in self._fingerprinter: 303 | self._fingerprinter[pt] = 0 304 | self._fingerprinter[pt] += 1 305 | 306 | def _moveTo(self, pt): 307 | d = dict( 308 | moveTo=1, 309 | lineTo=0, 310 | curveTo=0, 311 | qCurveTo=0 312 | ) 313 | self.contours.append(d) 314 | self._logPoint(pt) 315 | 316 | def _lineTo(self, pt): 317 | self.contours[-1]["lineTo"] += 1 318 | self._logPoint(pt) 319 | 320 | def _curveToOne(self, pt1, pt2, pt3): 321 | self.contours[-1]["curveTo"] += 1 322 | self._logPoint(pt1) 323 | self._logPoint(pt2) 324 | self._logPoint(pt3) 325 | 326 | def _qCurveToOne(self, pt1, pt2): 327 | self.contours[-1]["qCurveTo"] += 1 328 | self._logPoint(pt1) 329 | self._logPoint(pt2) 330 | 331 | def addComponent(self, glyphName, transformation): 332 | transformation = " ".join(_numberToString(i) for i in transformation) 333 | self.components.append(transformation) 334 | 335 | # ---------------- 336 | # Profile: Kerning 337 | # ---------------- 338 | 339 | def profileKerning(kerning, profile): 340 | profile["kerning"] = len(kerning) 341 | 342 | # --------------- 343 | # Profile: Groups 344 | # --------------- 345 | 346 | def profileGroups(groups, profile): 347 | for group in groups.values(): 348 | count = len(group) 349 | profile["groups"].append(count) 350 | 351 | # ----------------- 352 | # Profile: Features 353 | # ----------------- 354 | 355 | def profileFeatures(text, profile): 356 | if text is not None: 357 | profile["features"] = len(text) 358 | 359 | # ------------------ 360 | # Profile: Font Info 361 | # ------------------ 362 | 363 | fontInfoStringAttributes = """ 364 | familyName 365 | styleName 366 | styleMapFamilyName 367 | copyright 368 | trademark 369 | note 370 | openTypeNameDesigner 371 | openTypeNameDesignerURL 372 | openTypeNameManufacturer 373 | openTypeNameManufacturerURL 374 | openTypeNameLicense 375 | openTypeNameLicenseURL 376 | openTypeNameVersion 377 | openTypeNameUniqueID 378 | openTypeNameDescription 379 | openTypeNamePreferredFamilyName 380 | openTypeNamePreferredSubfamilyName 381 | openTypeNameCompatibleFullName 382 | openTypeNameSampleText 383 | openTypeNameWWSFamilyName 384 | openTypeNameWWSSubfamilyName 385 | postscriptFontName 386 | postscriptFullName 387 | postscriptWeightName 388 | macintoshFONDName 389 | """.strip().split() 390 | 391 | def profileFontInfo(info, profile): 392 | for attr in fontInfoStringAttributes: 393 | if hasattr(info, attr): 394 | value = getattr(info, attr) 395 | if isinstance(value, basestring): 396 | profile["fontInfo"] += len(value) 397 | 398 | # ----------------- 399 | # Profile to String 400 | # ----------------- 401 | 402 | def profileToString(profile): 403 | """ 404 | > start of profile 405 | source format: source file extension 406 | source size: source file size in bytes 407 | output environment: library/app used to output the profile 408 | fingerprint: hash of all points in the font 409 | font info characters: total number of characters used in string fields in the font info 410 | kerning pairs: total number of kerning pairs 411 | groups: total number of groups 412 | group members: sum of the length of all groups 413 | feature characters: number of characters in the features 414 | layers: number of layers 415 | glyphs: number of glyphs 416 | glyph name characters: total number of characters used in glyph names 417 | glyph note characters: total number of characters used in glyph notes 418 | contours: total number of contours 419 | segments: total number of segments 420 | components: total number of contours 421 | glyphs with (number) contours: percentage of glyphs with a particular number of contours 422 | contours with (number) segments: percentage of contours with a particular number of segments 423 | (segment type) segments: percentage of segments of a particular type 424 | components with (transformation) transformation: percentage of components with a particular transformation 425 | < end of profile 426 | """ 427 | lines = [ 428 | ">", 429 | "source format: %s" % profile["sourceFormat"], 430 | "source size: %d" % profile["sourceSize"], 431 | "output environment: %s" % profile["outputEnvironment"], 432 | "fingerprint: %s" % profile["fingerprint"], 433 | "font info characters: %d" % profile["fontInfo"], 434 | "kerning pairs: %d" % profile["kerning"], 435 | "groups: %d" % len(profile["groups"]), 436 | "group members: %d" % sum(profile["groups"]), 437 | "feature characters: %d" % profile["features"], 438 | "layers: %d" % len(profile["glyphs"]), 439 | "glyphs: %d" % sum(profile["glyphs"]), 440 | "glyph note characters: %d" % profile["glyphNotes"], 441 | "contours: %d" % profile["contours"], 442 | "components: %d" % profile["components"], 443 | "segments: %d" % profile["segments"], 444 | ] 445 | segmentCount = float(profile["segments"]) 446 | for segmentType in ("moveTo", "lineTo", "curveTo", "qCurveTo"): 447 | occurance = profile["segmentTypes"][segmentType] 448 | occurance = occurance / segmentCount 449 | lines.append( 450 | "%s segments: %s" % (segmentType, occurance) 451 | ) 452 | glyphCount = float(sum(profile["glyphs"])) 453 | for contourCount, occurance in reversed(sorted(profile["contourOccurance"].items())): 454 | occurance = occurance / glyphCount 455 | lines.append( 456 | "glyphs with %d contours: %.10f" % (contourCount, occurance) 457 | ) 458 | contourCount = float(profile["contours"]) 459 | for segmentCount, occurance in reversed(sorted(profile["segmentOccurance"].items())): 460 | occurance = occurance / contourCount 461 | lines.append( 462 | "contours with %d segments: %.10f" % (segmentCount, occurance) 463 | ) 464 | componentCount = float(profile["components"]) 465 | for transformation, occurance in sorted(profile["componentOccurance"].items()): 466 | occurance = occurance / componentCount 467 | lines.append( 468 | "components with (%s) transformation: %d" % (transformation, occurance) 469 | ) 470 | lines.append("<") 471 | return "\n".join(lines) 472 | 473 | def _numberToString(n): 474 | if int(n) == n: 475 | return str(int(n)) 476 | else: 477 | return "%.2f" % n 478 | 479 | # ---- 480 | # Main 481 | # ---- 482 | 483 | def dumpProfilesForFonts(paths): 484 | for path in paths: 485 | profile = profileFont(path) 486 | print profileToString(profile) 487 | 488 | def gatherFontPaths(paths): 489 | found = [] 490 | for path in paths: 491 | if isFontPath(path): 492 | found.append(path) 493 | elif os.path.isdir(path): 494 | for fileName in os.listdir(path): 495 | if fileName.startswith("."): 496 | continue 497 | p = os.path.join(path, fileName) 498 | found += gatherFontPaths([p]) 499 | return found 500 | 501 | def isFontPath(path): 502 | fileName = os.path.basename(path) 503 | suffix = os.path.splitext(fileName)[-1].lower() 504 | return suffix in supportedFormats 505 | 506 | # ------------ 507 | # Command Line 508 | # ------------ 509 | 510 | usage = "%prog [options] fontpath1 fontpath2 fontdirectory" 511 | 512 | description = """ 513 | This tool dumps an anonymous profile 514 | of the given fonts of the fonts found by 515 | recursively searching given directories. 516 | """.strip() 517 | 518 | def main(): 519 | parser = optparse.OptionParser(usage=usage, description=description, version="%prog 0.0beta") 520 | (options, args) = parser.parse_args() 521 | paths = gatherFontPaths(args) 522 | dumpProfilesForFonts(paths) 523 | 524 | if __name__ == "__main__": 525 | main() 526 | --------------------------------------------------------------------------------