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,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()