├── .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 |
--------------------------------------------------------------------------------