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,292 @@
1
+ """
2
+ A module for automatically creating CSS @font-face rules from
3
+ WOFF files. *makeFontFaceRule* is the only public function.
4
+
5
+ This can also be used as a command line tool for generating
6
+ CSS @font-face rules 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 urllib
34
+ import optparse
35
+ from woffTools import WOFFFont
36
+ from woffTools.tools.support import findUniqueFileName
37
+
38
+ # -----------
39
+ # Descriptors
40
+ # -----------
41
+
42
+ def makeFontFaceFontFamily(font):
43
+ familyPriority = [
44
+ (21, 1, 0, 0), # WWS Family Name, Mac, English, Roman
45
+ (21, 1, None, None), # WWS Family Name, Mac, Any, Any
46
+ (21, None, None, None), # WWS Family Name, Any, Any, Any
47
+ (16, 1, 0, 0), # Preferred Family, Mac, English, Roman
48
+ (16, 1, None, None), # Preferred Family, Mac, Any, Any
49
+ (16, None, None, None), # Preferred Family, Any, Any, Any
50
+ (1, 1, 0, 0), # Font Family Name, Mac, English, Roman
51
+ (1, 1, None, None), # Font Family Name, Mac, Any, Any
52
+ (1, None, None, None) # Font Family Name, Any, Any, Any
53
+ ]
54
+ familyName = _skimNameIDs(font, familyPriority)
55
+ descriptor = "font-family: \"%s\";" % familyName
56
+ return descriptor
57
+
58
+ def makeFontFaceSrc(font, fileName, doLocalSrc=True):
59
+ sources = []
60
+ # notes about "local"
61
+ sources.append("")
62
+ sources.append("/* The \"local\" lines will cause the browser to look for a locally */")
63
+ sources.append("/* installed font with the specified name before downloading the WOFF file. */")
64
+ if not doLocalSrc:
65
+ sources.append("/* Remove the commenting if you want this behavior. */")
66
+ else:
67
+ sources.append("/* Remove the lines if you don't want this behavior. */")
68
+ # postscript name
69
+ postscriptPriority = [
70
+ (6, 1, 0, 0), # Postscript Name, Mac, English, Roman
71
+ (6, 1, None, None), # Postscript Name, Mac, Any, Any
72
+ (6, None, None, None), # Postscript Name, Any, Any, Any
73
+ ]
74
+ postscriptName = _skimNameIDs(font, postscriptPriority)
75
+ # full name
76
+ fullNamePriority = [
77
+ (4, 1, 0, 0), # Full Font Name, Mac, English, Roman
78
+ (4, 1, None, None), # Full Font Name, Mac, Any, Any
79
+ (4, None, None, None), # Full Font Name, Any, Any, Any
80
+ ]
81
+ fullName = _skimNameIDs(font, fullNamePriority)
82
+ # store
83
+ s = "local(\"%s\")" % postscriptName
84
+ if not doLocalSrc:
85
+ s = "/* " + s + " */"
86
+ sources.append(s)
87
+ if postscriptName != fullName:
88
+ s = "local(\"%s\")" % fullName
89
+ if not doLocalSrc:
90
+ s = "/* " + s + " */"
91
+ sources.append(s)
92
+ # file name
93
+ s = "url(\"%s\")" % urllib.quote(fileName) # XXX: format(\"woff\")
94
+ sources.append(s)
95
+ # write
96
+ sources = "\n\t".join(sources)
97
+ descriptor = "src: %s;" % sources
98
+ return descriptor
99
+
100
+ def makeFontFaceFontWeight(font):
101
+ os2 = font["OS/2"]
102
+ value = os2.usWeightClass
103
+ descriptor = "font-weight: %d;" % value
104
+ if value < 100 or value > 900:
105
+ descriptor += " /* ERROR! Weight value is out of the 100-900 value range. */"
106
+ elif value % 100:
107
+ descriptor += " /* ERROR! Weight value is not a multiple of 100. */"
108
+ return descriptor
109
+
110
+ def makeFontFaceFontStretch(font):
111
+ os2 = font["OS/2"]
112
+ value = os2.usWidthClass
113
+ options = "ultra-condensed extra-condensed condensed semi-condensed normal semi-expanded expanded extra-expanded ultra-expanded".split(" ")
114
+ try:
115
+ value = options[value-1]
116
+ except IndexError:
117
+ value = "normal; /* ERROR! The value in the OS/2 table usWidthClass is not valid! */"
118
+ descriptor = "font-stretch: %s;" % value
119
+ return descriptor
120
+
121
+ def makeFontFaceFontStyle(font):
122
+ os2 = font["OS/2"]
123
+ if os2.fsSelection & 1:
124
+ value = "italic"
125
+ else:
126
+ value = "normal"
127
+ descriptor = "font-style: %s;" % value
128
+ return descriptor
129
+
130
+ def makeFontFaceUnicodeRange(font):
131
+ # compile ranges
132
+ cmap = font["cmap"]
133
+ table = cmap.getcmap(3, 1)
134
+ mapping = table.cmap
135
+ ranges = []
136
+ for value in sorted(mapping.keys()):
137
+ newRanges = []
138
+ handled = False
139
+ for rangeMin, rangeMax in ranges:
140
+ if value >= rangeMin and value <= rangeMax:
141
+ handled = True
142
+ elif rangeMin - 1 == value:
143
+ rangeMin = value
144
+ handled = True
145
+ elif rangeMax + 1 == value:
146
+ rangeMax = value
147
+ handled = True
148
+ newRanges.append((rangeMin, rangeMax))
149
+ if not handled:
150
+ newRanges.append((value, value))
151
+ ranges = sorted(newRanges)
152
+ # convert ints to proper hexk values
153
+ formatted = []
154
+ for minValue, maxValue in ranges:
155
+ if minValue == maxValue:
156
+ uniRange = hex(minValue)[2:].upper()
157
+ else:
158
+ minCode = hex(minValue)[2:].upper()
159
+ maxCode = hex(maxValue)[2:].upper()
160
+ uniRange = "-".join((minCode, maxCode))
161
+ formatted.append("U+%s" % uniRange)
162
+ # break into nice lines
163
+ perLine = 4
164
+ chunks = []
165
+ while formatted:
166
+ if len(formatted) > perLine:
167
+ chunks.append(formatted[:perLine])
168
+ formatted = formatted[perLine:]
169
+ else:
170
+ chunks.append(formatted)
171
+ formatted = []
172
+ formatted = []
173
+ for index, chunk in enumerate(chunks):
174
+ s = ", ".join(chunk)
175
+ if index < len(chunks) - 1:
176
+ s += ","
177
+ formatted.append(s)
178
+ formatted = "\n\t".join(formatted)
179
+ # write
180
+ descriptor = "unicode-range: %s;" % formatted
181
+ return descriptor
182
+
183
+ # -------
184
+ # Helpers
185
+ # -------
186
+
187
+ def _skimNameIDs(font, priority):
188
+ nameIDs = {}
189
+ for nameRecord in font["name"].names:
190
+ nameID = nameRecord.nameID
191
+ platformID = nameRecord.platformID
192
+ platEncID = nameRecord.platEncID
193
+ langID = nameRecord.langID
194
+ text = nameRecord.string
195
+ nameIDs[nameID, platformID, platEncID, langID] = text
196
+ for (nameID, platformID, platEncID, langID) in priority:
197
+ for (nID, pID, pEID, lID), text in nameIDs.items():
198
+ if nID != nameID:
199
+ continue
200
+ if pID != platformID and platformID is not None:
201
+ continue
202
+ if pEID != platEncID and platEncID is not None:
203
+ continue
204
+ if lID != langID and langID is not None:
205
+ continue
206
+ text = "".join([i for i in text if i != "\x00"])
207
+ return text
208
+
209
+ # ---------------
210
+ # Public Function
211
+ # ---------------
212
+
213
+ def makeFontFaceRule(font, fontPath, doLocalSrc=True):
214
+ """
215
+ Create a CSS @font-face rule from the given font. This always
216
+ returns the CSS text.
217
+
218
+ Arguments
219
+
220
+ **font** - A *WOFFFont* object from *woffLib*.
221
+ **fontPath** - The location of the font file. At the least, this should be the file name for the font.
222
+ **doLocalSrc** - Generate "local" references as part of the "src" descriptor.
223
+ """
224
+ # create text
225
+ sections = [
226
+ makeFontFaceFontFamily(font),
227
+ makeFontFaceSrc(font, os.path.basename(fontPath), doLocalSrc=doLocalSrc),
228
+ makeFontFaceFontWeight(font),
229
+ makeFontFaceFontStretch(font),
230
+ makeFontFaceFontStyle(font),
231
+ makeFontFaceUnicodeRange(font)
232
+ ]
233
+ lines = []
234
+ for section in sections:
235
+ lines += ["\t" + line for line in section.splitlines()]
236
+ rule = ["/* Automatically generated from: %s %d.%d */" % (os.path.basename(fontPath), font.majorVersion, font.minorVersion)]
237
+ rule += [""]
238
+ rule += ["@font-face {"] + lines + ["}"]
239
+ rule = "\n".join(rule)
240
+ return rule
241
+
242
+ # --------------------
243
+ # Command Line Behvior
244
+ # --------------------
245
+
246
+ usage = "%prog [options] fontpath1 fontpath2"
247
+
248
+ description = """This tool examines the contents of a WOFF
249
+ file and attempts to generate a CSS @font-face rule based
250
+ on the data found in the WOFF file. The results of this
251
+ tool should always be carefully checked.
252
+ """
253
+
254
+ def main():
255
+ parser = optparse.OptionParser(usage=usage, description=description, version="%prog 0.1beta")
256
+ parser.add_option("-d", dest="outputDirectory", help="Output directory. The default is to output the CSS into the same directory as the font file.")
257
+ parser.add_option("-o", dest="outputFileName", help="Output file name. The default is \"fontfilename.css\". If this file already exists a time stamp will be added to the file name.")
258
+ parser.add_option("-l", action="store_true", dest="doLocalSrc", help="Write \"local\" instructions as part of the \"src\" descriptor.")
259
+ (options, args) = parser.parse_args()
260
+ outputDirectory = options.outputDirectory
261
+ if outputDirectory is not None and not os.path.exists(outputDirectory):
262
+ print "Directory does not exist:", outputDirectory
263
+ sys.exit()
264
+ for fontPath in args:
265
+ if not os.path.exists(fontPath):
266
+ print "File does not exist:", fontPath
267
+ sys.exit()
268
+ else:
269
+ print "Creating CSS: %s..." % fontPath
270
+ fontPath = fontPath.decode("utf-8")
271
+ font = WOFFFont(fontPath)
272
+ css = makeFontFaceRule(font, fontPath, doLocalSrc=options.doLocalSrc)
273
+ # make the output file name
274
+ if options.outputFileName is not None:
275
+ fileName = options.outputFileName
276
+ else:
277
+ fileName = os.path.splitext(os.path.basename(fontPath))[0]
278
+ fileName += ".css"
279
+ # make the output directory
280
+ if options.outputDirectory is not None:
281
+ directory = options.outputDirectory
282
+ else:
283
+ directory = os.path.dirname(fontPath)
284
+ # write the file
285
+ path = os.path.join(directory, fileName)
286
+ path = findUniqueFileName(path)
287
+ f = open(path, "wb")
288
+ f.write(css)
289
+ f.close()
290
+
291
+ if __name__ == "__main__":
292
+ main()
@@ -0,0 +1,296 @@
1
+ """
2
+ A module for reporting information about the contents of
3
+ WOFF files. *reportInfo* is the only public function.
4
+
5
+ This can also be used as a command line tool.
6
+ """
7
+
8
+ # import test
9
+
10
+ importErrors = []
11
+ try:
12
+ import numpy
13
+ except:
14
+ importErrors.append("numpy")
15
+ try:
16
+ import fontTools
17
+ except ImportError:
18
+ importErrors.append("fontTools")
19
+ try:
20
+ import woffTools
21
+ except ImportError:
22
+ importErrors.append("woffTools")
23
+
24
+ if importErrors:
25
+ import sys
26
+ print "Could not import needed module(s):", ", ".join(importErrors)
27
+ sys.exit()
28
+
29
+ # import
30
+
31
+ import os
32
+ import optparse
33
+ from woffTools import WOFFFont
34
+ from woffTools.tools.support import startHTML, finishHTML, findUniqueFileName
35
+ from woffTools.tools.css import makeFontFaceRule
36
+
37
+
38
+ # ----------------
39
+ # Report Functions
40
+ # ----------------
41
+
42
+ def writeFileInfo(font, fontPath, writer):
43
+ # start the block
44
+ writer.begintag("div", c_l_a_s_s="infoBlock")
45
+ # title
46
+ writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
47
+ writer.write("File Information")
48
+ writer.endtag("h3")
49
+ # table
50
+ writer.begintag("table", c_l_a_s_s="report")
51
+ writeFileInfoRow("FILE", os.path.basename(fontPath), writer)
52
+ writeFileInfoRow("DIRECTORY", os.path.dirname(fontPath), writer)
53
+ writeFileInfoRow("FILE SIZE", str(font.reader.length) + " bytes", writer)
54
+ writeFileInfoRow("VERSION", "%d.%d" % (font.majorVersion, font.minorVersion), writer)
55
+ writer.endtag("table")
56
+ ## close the container
57
+ writer.endtag("div")
58
+
59
+ def writeFileInfoRow(title, value, writer):
60
+ # row
61
+ writer.begintag("tr")
62
+ # title
63
+ writer.begintag("td", c_l_a_s_s="title")
64
+ writer.write(title)
65
+ writer.endtag("td")
66
+ # message
67
+ writer.begintag("td")
68
+ writer.write(value)
69
+ writer.endtag("td")
70
+ # close row
71
+ writer.endtag("tr")
72
+
73
+ def writeSFNTInfo(font, writer):
74
+ # start the block
75
+ writer.begintag("div", c_l_a_s_s="infoBlock")
76
+ # title
77
+ writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
78
+ writer.write("sfnt Tables")
79
+ writer.endtag("h3")
80
+ # tables
81
+ writer.begintag("table", c_l_a_s_s="sfntTableData")
82
+ writer.begintag("tr")
83
+ columns = "tag offset compLength origLength origChecksum".split()
84
+ for c in columns:
85
+ writer.begintag("th")
86
+ writer.write(c)
87
+ writer.endtag("th")
88
+ writer.endtag("tr")
89
+ for tag, entry in sorted(font.reader.tables.items()):
90
+ if entry.compLength == entry.origLength:
91
+ writer.begintag("tr", c_l_a_s_s="uncompressed")
92
+ else:
93
+ writer.begintag("tr")
94
+ for attr in columns:
95
+ v = getattr(entry, attr)
96
+ if attr == "origChecksum":
97
+ v = hex(v)
98
+ else:
99
+ v = str(v)
100
+ writer.begintag("td")
101
+ writer.write(v)
102
+ writer.endtag("td")
103
+ writer.endtag("tr")
104
+ writer.endtag("table")
105
+ ## close the block
106
+ writer.endtag("div")
107
+
108
+ def writeMetadata(font, writer):
109
+ # start the block
110
+ writer.begintag("div", c_l_a_s_s="infoBlock")
111
+ # title
112
+ writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
113
+ writer.write("Metadata")
114
+ writer.endtag("h3")
115
+ # content
116
+ if font.metadata is not None:
117
+ for element in font.metadata:
118
+ writeMetadataElement(element, writer)
119
+ # close the block
120
+ writer.endtag("div")
121
+
122
+ def writeMetadataElement(element, writer):
123
+ writer.begintag("div", c_l_a_s_s="metadataElement")
124
+ # tag
125
+ writer.begintag("h5", c_l_a_s_s="metadata")
126
+ writer.write(element.tag)
127
+ writer.endtag("h5")
128
+ # attributes
129
+ if len(element.attrib):
130
+ writer.begintag("h6", c_l_a_s_s="metadata")
131
+ writer.write("Attributes:")
132
+ writer.endtag("h6")
133
+ # key, value pairs
134
+ writer.begintag("table", c_l_a_s_s="metadata")
135
+ for key, value in sorted(element.attrib.items()):
136
+ writer.begintag("tr")
137
+ writer.begintag("td", c_l_a_s_s="key")
138
+ writer.write(key)
139
+ writer.endtag("td")
140
+ writer.begintag("td", c_l_a_s_s="value")
141
+ writer.write(value)
142
+ writer.endtag("td")
143
+ writer.endtag("tr")
144
+ writer.endtag("table")
145
+ # text
146
+ if element.text is not None and element.text.strip():
147
+ writer.begintag("h6", c_l_a_s_s="metadata")
148
+ writer.write("Text:")
149
+ writer.endtag("h6")
150
+ writer.begintag("p", c_l_a_s_s="metadata")
151
+ writer.write(element.text)
152
+ writer.endtag("p")
153
+ # child elements
154
+ if len(element):
155
+ writer.begintag("h6", c_l_a_s_s="metadata")
156
+ writer.write("Child Elements:")
157
+ writer.endtag("h6")
158
+ for child in element:
159
+ writeMetadataElement(child, writer)
160
+ # close
161
+ writer.endtag("div")
162
+
163
+ hexFilter = "".join([(len(repr(chr(x))) == 3) and chr(x) or "." for x in range(256)])
164
+
165
+ def writePrivateData(font, writer):
166
+ # start the block
167
+ writer.begintag("div", c_l_a_s_s="infoBlock")
168
+ # title
169
+ writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
170
+ writer.write("Private Data")
171
+ writer.endtag("h3")
172
+ # content
173
+ if font.privateData:
174
+ # adapted from http://code.activestate.com/recipes/142812/
175
+ src = font.privateData
176
+ length = 16
177
+ result = []
178
+ for i in xrange(0, len(src), length):
179
+ s = src[i:i+length]
180
+ hexa = []
181
+ c = []
182
+ for x in s:
183
+ x = "%02X" % ord(x)
184
+ c.append(x)
185
+ if len(c) == 4:
186
+ hexa.append("".join(c))
187
+ c = []
188
+ if c:
189
+ hexa.append("".join(c))
190
+ hexa = " ".join(hexa)
191
+ if len(hexa) != 35:
192
+ hexa += " " * (35 - len(hexa))
193
+ printable = s.translate(hexFilter)
194
+ result.append("%04X %s %s\n" % (i, hexa, printable))
195
+ privateData = "".join(result)
196
+ writer.begintag("pre", c_l_a_s_s="privateData")
197
+ writer.write(privateData)
198
+ writer.endtag("pre")
199
+ # close the block
200
+ writer.endtag("div")
201
+
202
+ def writeFontFaceRule(font, fontPath, writer):
203
+ # start the block
204
+ writer.begintag("div", c_l_a_s_s="infoBlock")
205
+ # title
206
+ writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
207
+ writer.write("@font-face")
208
+ writer.endtag("h3")
209
+ # the text
210
+ fontFaceRule = makeFontFaceRule(font, fontPath, doLocalSrc=False)
211
+ writer.begintag("pre", c_l_a_s_s="fontFaceRule")
212
+ writer.write(fontFaceRule)
213
+ writer.endtag("pre")
214
+ # close the container
215
+ writer.endtag("div")
216
+
217
+ # ---------------
218
+ # Public Function
219
+ # ---------------
220
+
221
+ def reportInfo(font, fontPath):
222
+ """
223
+ Create a report about the contents of font. This returns HTML.
224
+
225
+ Arguments
226
+
227
+ **font** - A *WOFFFont* object from *woffLib*.
228
+ **fontPath** - The location of the font file. At the least, this should be the file name for the font.
229
+ """
230
+ # start the html
231
+ title = "Info: %s" % os.path.basename(fontPath)
232
+ writer = startHTML(title=title)
233
+ # file info
234
+ writeFileInfo(font, fontPath, writer)
235
+ # SFNT tables
236
+ writeSFNTInfo(font, writer)
237
+ # metadata
238
+ writeMetadata(font, writer)
239
+ # private data
240
+ writePrivateData(font, writer)
241
+ # @font-face
242
+ writeFontFaceRule(font, fontPath, writer)
243
+ # finish the html
244
+ text = finishHTML(writer)
245
+ # return
246
+ return text
247
+
248
+ # --------------------
249
+ # Command Line Behvior
250
+ # --------------------
251
+
252
+ usage = "%prog [options] fontpath1 fontpath2"
253
+
254
+ description = """This tool displays information about the
255
+ contents of one or more WOFF files.
256
+ """
257
+
258
+ def main():
259
+ parser = optparse.OptionParser(usage=usage, description=description, version="%prog 0.1beta")
260
+ parser.add_option("-d", dest="outputDirectory", help="Output directory. The default is to output the report into the same directory as the font file.")
261
+ parser.add_option("-o", dest="outputFileName", help="Output file name. The default is \"fontfilename_info.html\".")
262
+ parser.set_defaults(excludeTests=[])
263
+ (options, args) = parser.parse_args()
264
+ outputDirectory = options.outputDirectory
265
+ if outputDirectory is not None and not os.path.exists(outputDirectory):
266
+ print "Directory does not exist:", outputDirectory
267
+ sys.exit()
268
+ for fontPath in args:
269
+ if not os.path.exists(fontPath):
270
+ print "File does not exist:", fontPath
271
+ sys.exit()
272
+ else:
273
+ print "Creating Info Report: %s..." % fontPath
274
+ fontPath = fontPath.decode("utf-8")
275
+ font = WOFFFont(fontPath)
276
+ html = reportInfo(font, fontPath)
277
+ # make the output file name
278
+ if options.outputFileName is not None:
279
+ fileName = options.outputFileName
280
+ else:
281
+ fileName = os.path.splitext(os.path.basename(fontPath))[0]
282
+ fileName += "_info.html"
283
+ # make the output directory
284
+ if options.outputDirectory is not None:
285
+ directory = options.outputDirectory
286
+ else:
287
+ directory = os.path.dirname(fontPath)
288
+ # write the file
289
+ path = os.path.join(directory, fileName)
290
+ path = findUniqueFileName(path)
291
+ f = open(path, "wb")
292
+ f.write(html)
293
+ f.close()
294
+
295
+ if __name__ == "__main__":
296
+ main()