woff 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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