woff 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ """
2
+ A module for automatically creating simple proof files from
3
+ WOFF files. *proofFont* is the only public function.
4
+
5
+ This can also be used as a command line tool for generating
6
+ proofs from WOFF files.
7
+ """
8
+
9
+ # import test
10
+
11
+ importErrors = []
12
+ try:
13
+ import numpy
14
+ except:
15
+ importErrors.append("numpy")
16
+ try:
17
+ import fontTools
18
+ except ImportError:
19
+ importErrors.append("fontTools")
20
+ try:
21
+ import woffTools
22
+ except ImportError:
23
+ importErrors.append("woffTools")
24
+
25
+ if importErrors:
26
+ import sys
27
+ print "Could not import needed module(s):", ", ".join(importErrors)
28
+ sys.exit()
29
+
30
+ # import
31
+
32
+ import os
33
+ import optparse
34
+ import unicodedata
35
+ from woffTools import WOFFFont
36
+ from woffTools.tools.css import makeFontFaceRule, makeFontFaceFontFamily
37
+ from woffTools.tools.support import startHTML, finishHTML, findUniqueFileName
38
+
39
+
40
+ # ----------------
41
+ # Report Functions
42
+ # ----------------
43
+
44
+ def writeFileInfo(font, fontPath, writer):
45
+ # start the block
46
+ writer.begintag("div", c_l_a_s_s="infoBlock")
47
+ # title
48
+ writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
49
+ writer.write("File Information")
50
+ writer.endtag("h3")
51
+ # table
52
+ writer.begintag("table", c_l_a_s_s="report")
53
+ writeFileInfoRow("FILE", os.path.basename(fontPath), writer)
54
+ writeFileInfoRow("DIRECTORY", os.path.dirname(fontPath), writer)
55
+ writeFileInfoRow("VERSION", "%d.%d" % (font.majorVersion, font.minorVersion), writer)
56
+ writer.endtag("table")
57
+ # close the container
58
+ writer.endtag("div")
59
+
60
+ def writeFileInfoRow(title, value, writer):
61
+ # row
62
+ writer.begintag("tr")
63
+ # title
64
+ writer.begintag("td", c_l_a_s_s="title")
65
+ writer.write(title)
66
+ writer.endtag("td")
67
+ # message
68
+ writer.begintag("td")
69
+ writer.write(value)
70
+ writer.endtag("td")
71
+ # close row
72
+ writer.endtag("tr")
73
+
74
+ def writeCharacterSet(font, writer, pointSizes=[9, 10, 11, 12, 14, 18, 24, 36, 48, 72], sampleText=None):
75
+ characterSet = makeCharacterSet(font)
76
+ for size in pointSizes:
77
+ # start the block
78
+ writer.begintag("div", c_l_a_s_s="infoBlock")
79
+ # title
80
+ writer.begintag("h4", c_l_a_s_s="infoBlockTitle")
81
+ writer.write("%dpx" % size)
82
+ writer.endtag("h4")
83
+ # character set
84
+ writer.begintag("p", style="font-size: %dpx;" % size, c_l_a_s_s="characterSet")
85
+ writer.write(characterSet)
86
+ writer.endtag("p")
87
+ # sample text
88
+ if sampleText:
89
+ writer.begintag("p", style="font-size: %dpx;" % size, c_l_a_s_s="sampleText")
90
+ writer.write(sampleText)
91
+ writer.endtag("p")
92
+ # close the container
93
+ writer.endtag("div")
94
+
95
+ # -------------
96
+ # Character Set
97
+ # -------------
98
+
99
+ def makeCharacterSet(font):
100
+ cmap = font["cmap"]
101
+ table = cmap.getcmap(3, 1)
102
+ mapping = table.cmap
103
+ categorizedCharacters = {}
104
+ glyphNameToCharacter = {}
105
+ for value, glyphName in sorted(mapping.items()):
106
+ character = unichr(value)
107
+ # skip whitespace
108
+ if not character.strip():
109
+ continue
110
+ if glyphName not in glyphNameToCharacter:
111
+ glyphNameToCharacter[glyphName] = []
112
+ glyphNameToCharacter[glyphName].append(character)
113
+ # use the glyph order defined in the font
114
+ sortedCharacters = []
115
+ for glyphName in font.getGlyphOrder():
116
+ if glyphName in glyphNameToCharacter:
117
+ sortedCharacters += glyphNameToCharacter[glyphName]
118
+ return u"".join(sortedCharacters)
119
+
120
+ # ---------------
121
+ # Public Function
122
+ # ---------------
123
+
124
+ def proofFont(font, fontPath, sampleText=None):
125
+ """
126
+ Create a proof file from the given font. This always
127
+ returns HTML.
128
+
129
+ Arguments:
130
+ **font** - A *WOFFFont* object from *woffLib*.
131
+ **fontPath** - The location of the font file. At the least, this should be the file name for the font.
132
+ **sampleText** - A string of text to display. If not provided, no text will be displayed.
133
+ """
134
+ # start the html
135
+ title = "Proof: %s" % os.path.basename(fontPath)
136
+ cssReplacements = {
137
+ "/* proof: @font-face rule */" : makeFontFaceRule(font, fontPath, doLocalSrc=False),
138
+ "/* proof: @font-face font-family */" : makeFontFaceFontFamily(font)
139
+ }
140
+ writer = startHTML(title=title, cssReplacements=cssReplacements)
141
+ # file info
142
+ writeFileInfo(font, fontPath, writer)
143
+ # character set
144
+ writeCharacterSet(font, writer, sampleText=sampleText)
145
+ # finish the html
146
+ text = finishHTML(writer)
147
+ text = text.replace("%%break%%", "</br>")
148
+ # return
149
+ return text
150
+
151
+ # --------------------
152
+ # Command Line Behvior
153
+ # --------------------
154
+
155
+ usage = "%prog [options] fontpath1 fontpath2"
156
+
157
+ description = """This tool displays information about the
158
+ contents of one or more WOFF files.
159
+ """
160
+
161
+ defaultSampleText = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG. The quick brown fox jumps over the lazy dog."
162
+
163
+ def main():
164
+ parser = optparse.OptionParser(usage=usage, description=description, version="%prog 0.1beta")
165
+ parser.add_option("-d", dest="outputDirectory", help="Output directory. The default is to output the proof into the same directory as the font file.")
166
+ parser.add_option("-o", dest="outputFileName", help="Output file name. The default is \"fontfilename_proof.html\".")
167
+ parser.add_option("-t", dest="sampleTextFile", help="Sample text file. A file containing sample text to display. If not file is provided, The quick brown fox... will be used.")
168
+ parser.set_defaults(excludeTests=[])
169
+ (options, args) = parser.parse_args()
170
+ outputDirectory = options.outputDirectory
171
+ if outputDirectory is not None and not os.path.exists(outputDirectory):
172
+ print "Directory does not exist:", outputDirectory
173
+ sys.exit()
174
+ sampleText = defaultSampleText
175
+ if options.sampleTextFile:
176
+ if not os.path.exists(options.sampleTextFile):
177
+ print "Sample text file does not exist:", options.sampleTextFile
178
+ sys.exit()
179
+ f = open(options.sampleTextFile, "r")
180
+ sampleText = f.read()
181
+ f.close()
182
+ for fontPath in args:
183
+ if not os.path.exists(fontPath):
184
+ print "File does not exist:", fontPath
185
+ sys.exit()
186
+ else:
187
+ print "Creating Proof: %s..." % fontPath
188
+ fontPath = fontPath.decode("utf-8")
189
+ font = WOFFFont(fontPath)
190
+ html = proofFont(font, fontPath, sampleText=sampleText)
191
+ # make the output file name
192
+ if options.outputFileName is not None:
193
+ fileName = options.outputFileName
194
+ else:
195
+ fileName = os.path.splitext(os.path.basename(fontPath))[0]
196
+ fileName += "_proof.html"
197
+ # make the output directory
198
+ if options.outputDirectory is not None:
199
+ directory = options.outputDirectory
200
+ else:
201
+ directory = os.path.dirname(fontPath)
202
+ # write the file
203
+ path = os.path.join(directory, fileName)
204
+ path = findUniqueFileName(path)
205
+ f = open(path, "wb")
206
+ f.write(html)
207
+ f.close()
208
+
209
+ if __name__ == "__main__":
210
+ main()
@@ -0,0 +1,417 @@
1
+ import os
2
+ import time
3
+ from xml.etree import ElementTree
4
+ from cStringIO import StringIO
5
+
6
+ # ----------------------
7
+ # Very Simple XML Writer
8
+ # ----------------------
9
+
10
+ class XMLWriter(object):
11
+
12
+ def __init__(self):
13
+ self._root = None
14
+ self._elements = []
15
+
16
+ def simpletag(self, tag, **kwargs):
17
+ ElementTree.SubElement(self._elements[-1], tag, **kwargs)
18
+
19
+ def begintag(self, tag, **kwargs):
20
+ if self._elements:
21
+ s = ElementTree.SubElement(self._elements[-1], tag, **kwargs)
22
+ else:
23
+ s = ElementTree.Element(tag, **kwargs)
24
+ if self._root is None:
25
+ self._root = s
26
+ self._elements.append(s)
27
+
28
+ def endtag(self, tag):
29
+ assert self._elements[-1].tag == tag
30
+ del self._elements[-1]
31
+
32
+ def write(self, text):
33
+ if self._elements[-1].text is None:
34
+ self._elements[-1].text = text
35
+ else:
36
+ self._elements[-1].text += text
37
+
38
+ def compile(self, encoding="utf-8"):
39
+ f = StringIO()
40
+ tree = ElementTree.ElementTree(self._root)
41
+ indent(tree.getroot())
42
+ tree.write(f, encoding=encoding)
43
+ text = f.getvalue()
44
+ del f
45
+ return text
46
+
47
+ def indent(elem, level=0):
48
+ # this is from http://effbot.python-hosting.com/file/effbotlib/ElementTree.py
49
+ i = "\n" + level * "\t"
50
+ if len(elem):
51
+ if not elem.text or not elem.text.strip():
52
+ elem.text = i + "\t"
53
+ for e in elem:
54
+ indent(e, level + 1)
55
+ if not e.tail or not e.tail.strip():
56
+ e.tail = i
57
+ if level and (not elem.tail or not elem.tail.strip()):
58
+ elem.tail = i
59
+
60
+ # ------------
61
+ # HTML Helpers
62
+ # ------------
63
+
64
+ defaultCSS = """
65
+ body {
66
+ background-color: #e5e5e5;
67
+ padding: 15px 15px 0px 15px;
68
+ margin: 0px;
69
+ font-family: Helvetica, Verdana, Arial, sans-serif;
70
+ }
71
+
72
+ h2.readError {
73
+ background-color: red;
74
+ color: white;
75
+ margin: 20px 15px 20px 15px;
76
+ padding: 10px;
77
+ border-radius: 5px;
78
+ -webkit-border-radius: 5px;
79
+ -moz-border-radius: 5px;
80
+ -webkit-box-shadow: #999 0 2px 5px;
81
+ -moz-box-shadow: #999 0 2px 5px;
82
+ font-size: 25px;
83
+ }
84
+
85
+ /* info blocks */
86
+
87
+ .infoBlock {
88
+ background-color: white;
89
+ margin: 0px 0px 15px 0px;
90
+ padding: 15px;
91
+ border-radius: 5px;
92
+ -webkit-border-radius: 5px;
93
+ -moz-border-radius: 5px;
94
+ -webkit-box-shadow: rgba(0, 0, 0, .3) 0 2px 5px;
95
+ -moz-box-shadow: rgba(0, 0, 0, .3) 0 2px 5px;
96
+ }
97
+
98
+ h3.infoBlockTitle {
99
+ font-size: 20px;
100
+ margin: 0px 0px 15px 0px;
101
+ padding: 0px 0px 10px 0px;
102
+ border-bottom: 1px solid #e5e5e5;
103
+ }
104
+
105
+ h4.infoBlockTitle {
106
+ font-size: 17px;
107
+ margin: 0px 0px 15px 0px;
108
+ padding: 0px 0px 10px 0px;
109
+ border-bottom: 1px solid #e5e5e5;
110
+ }
111
+
112
+ table.report {
113
+ border-collapse: collapse;
114
+ width: 100%;
115
+ font-size: 14px;
116
+ }
117
+
118
+ table.report tr {
119
+ border-top: 1px solid white;
120
+ }
121
+
122
+ table.report tr.testPass, table.report tr.testReportPass {
123
+ background-color: #c8ffaf;
124
+ }
125
+
126
+ table.report tr.testError, table.report tr.testReportError {
127
+ background-color: #ffc3af;
128
+ }
129
+
130
+ table.report tr.testWarning, table.report tr.testReportWarning {
131
+ background-color: #ffe1af;
132
+ }
133
+
134
+ table.report tr.testNote, table.report tr.testReportNote {
135
+ background-color: #96e1ff;
136
+ }
137
+
138
+ table.report tr.testTraceback, table.report tr.testReportTraceback {
139
+ background-color: red;
140
+ color: white;
141
+ }
142
+
143
+ table.report td {
144
+ padding: 7px 5px 7px 5px;
145
+ vertical-align: top;
146
+ }
147
+
148
+ table.report td.title {
149
+ width: 80px;
150
+ text-align: right;
151
+ font-weight: bold;
152
+ text-transform: uppercase;
153
+ }
154
+
155
+ table.report td.testReportResultCount {
156
+ width: 100px;
157
+ }
158
+
159
+ table.report td.toggleButton {
160
+ text-align: center;
161
+ width: 50px;
162
+ border-left: 1px solid white;
163
+ cursor: pointer;
164
+ }
165
+
166
+ .infoBlock td p.info {
167
+ font-size: 12px;
168
+ font-style: italic;
169
+ margin: 5px 0px 0px 0px;
170
+ }
171
+
172
+ /* SFNT table */
173
+
174
+ table.sfntTableData {
175
+ font-size: 14px;
176
+ width: 100%;
177
+ border-collapse: collapse;
178
+ padding: 0px;
179
+ }
180
+
181
+ table.sfntTableData th {
182
+ padding: 5px 0px 5px 0px;
183
+ text-align: left
184
+ }
185
+
186
+ table.sfntTableData tr.uncompressed {
187
+ background-color: #ffc3af;
188
+ }
189
+
190
+ table.sfntTableData td {
191
+ width: 20%;
192
+ padding: 5px 0px 5px 0px;
193
+ border: 1px solid #e5e5e5;
194
+ border-left: none;
195
+ border-right: none;
196
+ font-family: Consolas, Menlo, "Vera Mono", Monaco, monospace;
197
+ }
198
+
199
+ pre {
200
+ font-size: 12px;
201
+ font-family: Consolas, Menlo, "Vera Mono", Monaco, monospace;
202
+ margin: 0px;
203
+ padding: 0px;
204
+ }
205
+
206
+ /* Metadata */
207
+
208
+ .metadataElement {
209
+ background: rgba(0, 0, 0, 0.03);
210
+ margin: 10px 0px 10px 0px;
211
+ border: 2px solid #d8d8d8;
212
+ padding: 10px;
213
+ }
214
+
215
+ h5.metadata {
216
+ font-size: 14px;
217
+ margin: 5px 0px 10px 0px;
218
+ padding: 0px 0px 5px 0px;
219
+ border-bottom: 1px solid #d8d8d8;
220
+ }
221
+
222
+ h6.metadata {
223
+ font-size: 12px;
224
+ font-weight: normal;
225
+ margin: 10px 0px 10px 0px;
226
+ padding: 0px 0px 5px 0px;
227
+ border-bottom: 1px solid #d8d8d8;
228
+ }
229
+
230
+ table.metadata {
231
+ font-size: 12px;
232
+ width: 100%;
233
+ border-collapse: collapse;
234
+ padding: 0px;
235
+ }
236
+
237
+ table.metadata td.key {
238
+ width: 5em;
239
+ padding: 5px 5px 5px 0px;
240
+ border-right: 1px solid #d8d8d8;
241
+ text-align: right;
242
+ vertical-align: top;
243
+ }
244
+
245
+ table.metadata td.value {
246
+ padding: 5px 0px 5px 5px;
247
+ border-left: 1px solid #d8d8d8;
248
+ text-align: left;
249
+ vertical-align: top;
250
+ }
251
+
252
+ p.metadata {
253
+ font-size: 12px;
254
+ font-style: italic;
255
+ }
256
+
257
+ /* Proof */
258
+
259
+ /* proof: @font-face rule */
260
+
261
+ p.characterSet {
262
+ /* proof: @font-face font-family */
263
+ line-height: 135%;
264
+ word-wrap: break-word;
265
+ margin: 0px;
266
+ padding: 0px;
267
+ }
268
+
269
+ p.sampleText {
270
+ /* proof: @font-face font-family */
271
+ line-height: 135%;
272
+ margin: .5em 0px 0px 0px;
273
+ padding: .5em 0px 0px 0px;
274
+ border-top: 1px solid #e5e5e5;
275
+ }
276
+ """
277
+
278
+ defaultJavascript = """
279
+
280
+ //<![CDATA[
281
+ function testResultToggleButtonHit(buttonID, className) {
282
+ // change the button title
283
+ var element = document.getElementById(buttonID);
284
+ if (element.innerHTML == "Show" ) {
285
+ element.innerHTML = "Hide";
286
+ }
287
+ else {
288
+ element.innerHTML = "Show";
289
+ }
290
+ // toggle the elements
291
+ var elements = getTestResults(className);
292
+ for (var e = 0; e < elements.length; ++e) {
293
+ toggleElement(elements[e]);
294
+ }
295
+ // toggle the info blocks
296
+ toggleInfoBlocks();
297
+ }
298
+
299
+ function getTestResults(className) {
300
+ var rows = document.getElementsByTagName("tr");
301
+ var found = Array();
302
+ for (var r = 0; r < rows.length; ++r) {
303
+ var row = rows[r];
304
+ if (row.className == className) {
305
+ found[found.length] = row;
306
+ }
307
+ }
308
+ return found;
309
+ }
310
+
311
+ function toggleElement(element) {
312
+ if (element.style.display != "none" ) {
313
+ element.style.display = "none";
314
+ }
315
+ else {
316
+ element.style.display = "";
317
+ }
318
+ }
319
+
320
+ function toggleInfoBlocks() {
321
+ var tables = document.getElementsByTagName("table")
322
+ for (var t = 0; t < tables.length; ++t) {
323
+ var table = tables[t];
324
+ if (table.className == "report") {
325
+ var haveVisibleRow = false;
326
+ var rows = table.rows;
327
+ for (var r = 0; r < rows.length; ++r) {
328
+ var row = rows[r];
329
+ if (row.style.display == "none") {
330
+ var i = 0;
331
+ }
332
+ else {
333
+ haveVisibleRow = true;
334
+ }
335
+ }
336
+ var div = table.parentNode;
337
+ if (haveVisibleRow == true) {
338
+ div.style.display = "";
339
+ }
340
+ else {
341
+ div.style.display = "none";
342
+ }
343
+ }
344
+ }
345
+ }
346
+ //]]>
347
+ """
348
+
349
+ def startHTML(title=None, cssReplacements={}):
350
+ writer = XMLWriter()
351
+ # start the html
352
+ writer.begintag("html", xmlns="http://www.w3.org/1999/xhtml", lang="en")
353
+ # start the head
354
+ writer.begintag("head")
355
+ writer.simpletag("meta", http_equiv="Content-Type", content="text/html; charset=utf-8")
356
+ # title
357
+ if title is not None:
358
+ writer.begintag("title")
359
+ writer.write(title)
360
+ writer.endtag("title")
361
+ # write the css
362
+ writer.begintag("style", type="text/css")
363
+ css = defaultCSS
364
+ for before, after in cssReplacements.items():
365
+ css = css.replace(before, after)
366
+ writer.write(css)
367
+ writer.endtag("style")
368
+ # write the javascript
369
+ writer.begintag("script", type="text/javascript")
370
+ javascript = defaultJavascript
371
+ ## hack around some ElementTree escaping
372
+ javascript = javascript.replace("<", "l_e_s_s")
373
+ javascript = javascript.replace(">", "g_r_e_a_t_e_r")
374
+ writer.write(javascript)
375
+ writer.endtag("script")
376
+ # close the head
377
+ writer.endtag("head")
378
+ # start the body
379
+ writer.begintag("body")
380
+ # return the writer
381
+ return writer
382
+
383
+ def finishHTML(writer):
384
+ # close the body
385
+ writer.endtag("body")
386
+ # close the html
387
+ writer.endtag("html")
388
+ # get the text
389
+ text = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
390
+ text += writer.compile()
391
+ text = text.replace("c_l_a_s_s", "class")
392
+ text = text.replace("a_p_o_s_t_r_o_p_h_e", "'")
393
+ text = text.replace("l_e_s_s", "<")
394
+ text = text.replace("g_r_e_a_t_e_r", ">")
395
+ text = text.replace("http_equiv", "http-equiv")
396
+ # return
397
+ return text
398
+
399
+ # ---------
400
+ # File Name
401
+ # ---------
402
+
403
+ def findUniqueFileName(path):
404
+ if not os.path.exists(path):
405
+ return path
406
+ folder = os.path.dirname(path)
407
+ fileName = os.path.basename(path)
408
+ fileName, extension = os.path.splitext(fileName)
409
+ stamp = time.strftime("%Y-%m-%d %H-%M-%S %Z")
410
+ newFileName = "%s (%s)%s" % (fileName, stamp, extension)
411
+ newPath = os.path.join(folder, newFileName)
412
+ # intentionally break to prevent a file overwrite.
413
+ # this could happen if the user has a directory full
414
+ # of files with future time stamped file names.
415
+ # not likely, but avoid it all the same.
416
+ assert not os.path.exists(newPath)
417
+ return newPath