gkh-fontcustom 1.3.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.travis.yml +20 -0
  4. data/CHANGELOG.md +137 -0
  5. data/CONTRIBUTING.md +50 -0
  6. data/Gemfile +4 -0
  7. data/LICENSES.txt +60 -0
  8. data/README.md +112 -0
  9. data/Rakefile +8 -0
  10. data/bin/fontcustom +5 -0
  11. data/fontcustom.gemspec +28 -0
  12. data/gemfiles/Gemfile.listen_1 +6 -0
  13. data/lib/fontcustom.rb +43 -0
  14. data/lib/fontcustom/base.rb +54 -0
  15. data/lib/fontcustom/cli.rb +110 -0
  16. data/lib/fontcustom/error.rb +4 -0
  17. data/lib/fontcustom/generator/font.rb +109 -0
  18. data/lib/fontcustom/generator/template.rb +222 -0
  19. data/lib/fontcustom/manifest.rb +75 -0
  20. data/lib/fontcustom/options.rb +192 -0
  21. data/lib/fontcustom/scripts/eotlitetool.py +466 -0
  22. data/lib/fontcustom/scripts/generate.py +128 -0
  23. data/lib/fontcustom/scripts/sfnt2woff +0 -0
  24. data/lib/fontcustom/templates/_fontcustom-rails.scss +14 -0
  25. data/lib/fontcustom/templates/_fontcustom.scss +16 -0
  26. data/lib/fontcustom/templates/fontcustom-preview.html +174 -0
  27. data/lib/fontcustom/templates/fontcustom.css +14 -0
  28. data/lib/fontcustom/templates/fontcustom.yml +96 -0
  29. data/lib/fontcustom/utility.rb +117 -0
  30. data/lib/fontcustom/version.rb +3 -0
  31. data/lib/fontcustom/watcher.rb +90 -0
  32. data/spec/fixtures/example/_example-rails.scss +50 -0
  33. data/spec/fixtures/example/example-preview.html +253 -0
  34. data/spec/fixtures/example/example.css +50 -0
  35. data/spec/fixtures/example/example.eot +0 -0
  36. data/spec/fixtures/example/example.svg +75 -0
  37. data/spec/fixtures/example/example.ttf +0 -0
  38. data/spec/fixtures/example/example.woff +0 -0
  39. data/spec/fixtures/generators/.fontcustom-manifest-corrupted.json +25 -0
  40. data/spec/fixtures/generators/.fontcustom-manifest-empty.json +0 -0
  41. data/spec/fixtures/generators/.fontcustom-manifest.json +52 -0
  42. data/spec/fixtures/generators/fontcustom.yml +1 -0
  43. data/spec/fixtures/generators/mixed-output/another-font.ttf +0 -0
  44. data/spec/fixtures/generators/mixed-output/dont-delete-me.bro +0 -0
  45. data/spec/fixtures/generators/mixed-output/fontcustom.css +108 -0
  46. data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.eot +0 -0
  47. data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.svg +56 -0
  48. data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.ttf +0 -0
  49. data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.woff +0 -0
  50. data/spec/fixtures/options/any-file-name.yml +1 -0
  51. data/spec/fixtures/options/config-is-in-dir/fontcustom.yml +1 -0
  52. data/spec/fixtures/options/fontcustom-empty.yml +1 -0
  53. data/spec/fixtures/options/fontcustom-malformed.yml +1 -0
  54. data/spec/fixtures/options/fontcustom.yml +1 -0
  55. data/spec/fixtures/options/no-config-here/.gitkeep +0 -0
  56. data/spec/fixtures/options/rails-like/config/fontcustom.yml +1 -0
  57. data/spec/fixtures/shared/not-a-dir +0 -0
  58. data/spec/fixtures/shared/templates/custom.css +4 -0
  59. data/spec/fixtures/shared/templates/regular.css +4 -0
  60. data/spec/fixtures/shared/vectors-empty/no_vectors_here.txt +0 -0
  61. data/spec/fixtures/shared/vectors/C.svg +14 -0
  62. data/spec/fixtures/shared/vectors/D.svg +15 -0
  63. data/spec/fixtures/shared/vectors/a_R3ally-eXotic f1Le Name.svg +6 -0
  64. data/spec/fontcustom/base_spec.rb +45 -0
  65. data/spec/fontcustom/cli_spec.rb +30 -0
  66. data/spec/fontcustom/generator/font_spec.rb +72 -0
  67. data/spec/fontcustom/generator/template_spec.rb +99 -0
  68. data/spec/fontcustom/manifest_spec.rb +17 -0
  69. data/spec/fontcustom/options_spec.rb +315 -0
  70. data/spec/fontcustom/utility_spec.rb +82 -0
  71. data/spec/fontcustom/watcher_spec.rb +121 -0
  72. data/spec/spec_helper.rb +103 -0
  73. metadata +252 -0
@@ -0,0 +1,75 @@
1
+ module Fontcustom
2
+ class Manifest
3
+ include Utility
4
+
5
+ attr_reader :manifest
6
+
7
+ def initialize(manifest, cli_options = {})
8
+ @manifest = manifest
9
+ @cli_options = symbolize_hash cli_options
10
+ if File.exists? @manifest
11
+ reload
12
+ if ! @cli_options.empty? && get(:options) != @cli_options
13
+ set :options, @cli_options
14
+ end
15
+ else
16
+ create_manifest @cli_options
17
+ end
18
+ end
19
+
20
+ # TODO convert paths to absolute
21
+ def get(key)
22
+ @data[key]
23
+ end
24
+
25
+ # TODO convert paths to relative
26
+ def set(key, value, status = nil)
27
+ if key == :all
28
+ @data = value
29
+ else
30
+ @data[key] = value
31
+ end
32
+ json = JSON.pretty_generate @data
33
+ write_file @manifest, json, status
34
+ end
35
+
36
+ def reload
37
+ begin
38
+ json = File.read @manifest
39
+ @data = JSON.parse json, :symbolize_names => true
40
+ rescue JSON::ParserError
41
+ raise Fontcustom::Error,
42
+ "Couldn't parse `#{@manifest}`. Fix any invalid "\
43
+ "JSON or delete the file to start from scratch."
44
+ end
45
+ end
46
+
47
+ def delete(key)
48
+ files = get(key)
49
+ return if files.empty?
50
+ begin
51
+ deleted = []
52
+ files.each do |file|
53
+ remove_file file, :verbose => false
54
+ deleted << file
55
+ end
56
+ ensure
57
+ set key, files - deleted
58
+ say_changed :delete, deleted
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def create_manifest(options)
65
+ defaults = {
66
+ :checksum => { :current => "", :previous => "" },
67
+ :fonts => [],
68
+ :glyphs => {},
69
+ :options => options,
70
+ :templates => []
71
+ }
72
+ set :all, defaults, :create
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,192 @@
1
+ require "yaml"
2
+ require "pp"
3
+
4
+ module Fontcustom
5
+ class Options
6
+ include Utility
7
+
8
+ def initialize(cli_options = {})
9
+ @manifest = cli_options[:manifest]
10
+ @cli_options = symbolize_hash(cli_options)
11
+ parse_options
12
+ end
13
+
14
+ private
15
+
16
+ def parse_options
17
+ overwrite_examples
18
+ set_config_path
19
+ load_config
20
+ merge_options
21
+ clean_font_name
22
+ clean_css_selector
23
+ set_input_paths
24
+ set_output_paths
25
+ check_template_paths
26
+ print_debug if @options[:debug]
27
+ end
28
+
29
+ # We give Thor fake defaults to generate more useful help messages.
30
+ # Here, we delete any CLI options that match those examples.
31
+ # TODO There's *got* a be a cleaner way to customize Thor help messages.
32
+ def overwrite_examples
33
+ EXAMPLE_OPTIONS.keys.each do |key|
34
+ @cli_options.delete(key) if @cli_options[key] == EXAMPLE_OPTIONS[key]
35
+ end
36
+ @cli_options = DEFAULT_OPTIONS.dup.merge @cli_options
37
+ end
38
+
39
+ def set_config_path
40
+ @cli_options[:config] = if @cli_options[:config]
41
+ path = @cli_options[:config]
42
+ if File.exists?(path) && ! File.directory?(path)
43
+ path
44
+ elsif File.exists? File.join(path, "fontcustom.yml")
45
+ File.join path, "fontcustom.yml"
46
+ else
47
+ raise Fontcustom::Error, "No configuration file found at `#{path}`."
48
+ end
49
+ else
50
+ if File.exists? "fontcustom.yml"
51
+ "fontcustom.yml"
52
+ elsif File.exists? File.join("config", "fontcustom.yml")
53
+ File.join "config", "fontcustom.yml"
54
+ else
55
+ false
56
+ end
57
+ end
58
+ end
59
+
60
+ def load_config
61
+ @config_options = {}
62
+ if @cli_options[:config]
63
+ begin
64
+ config = YAML.load File.open(@cli_options[:config])
65
+ if config # empty YAML returns false
66
+ @config_options = symbolize_hash(config)
67
+ say_message :debug, "Using settings from `#{@cli_options[:config]}`." if @cli_options[:debug] || @config_options[:debug]
68
+ else
69
+ say_message :warn, "`#{@cli_options[:config]}` was empty. Using defaults."
70
+ end
71
+ rescue Exception => e
72
+ raise Fontcustom::Error, "Error parsing `#{@cli_options[:config]}`:\n#{e.message}"
73
+ end
74
+ end
75
+ end
76
+
77
+ # TODO validate keys
78
+ def merge_options
79
+ @cli_options.delete_if { |key, val| val == DEFAULT_OPTIONS[key] }
80
+ @options = DEFAULT_OPTIONS.merge(@config_options).merge(@cli_options)
81
+ @options.delete :manifest
82
+ end
83
+
84
+ def clean_font_name
85
+ @options[:font_name] = @options[:font_name].strip.gsub(/\W/, "-")
86
+ end
87
+
88
+ def clean_css_selector
89
+ unless @options[:css_selector].include? "{{glyph}}"
90
+ raise Fontcustom::Error,
91
+ "CSS selector `#{@options[:css_selector]}` should contain the \"{{glyph}}\" placeholder."
92
+ end
93
+ @options[:css_selector] = @options[:css_selector].strip.gsub(/[^&%=\[\]\.#\{\}""\d\w]/, "-")
94
+ end
95
+
96
+ def set_input_paths
97
+ if @options[:input].is_a? Hash
98
+ @options[:input] = symbolize_hash(@options[:input])
99
+ if @options[:input].has_key? :vectors
100
+ check_input @options[:input][:vectors]
101
+ else
102
+ raise Fontcustom::Error,
103
+ "Input paths (assigned as a hash) should have a :vectors key. Check your options."
104
+ end
105
+
106
+ if @options[:input].has_key? :templates
107
+ check_input @options[:input][:templates]
108
+ else
109
+ @options[:input][:templates] = @options[:input][:vectors]
110
+ end
111
+ else
112
+ if @options[:input]
113
+ input = @options[:input]
114
+ else
115
+ input = "."
116
+ say_message :warn, "No input directory given. Using present working directory."
117
+ end
118
+ check_input input
119
+ @options[:input] = { :vectors => input, :templates => input }
120
+ end
121
+
122
+ if Dir[File.join(@options[:input][:vectors], "*.svg")].empty?
123
+ raise Fontcustom::Error, "`#{@options[:input][:vectors]}` doesn't contain any SVGs."
124
+ end
125
+ end
126
+
127
+ def set_output_paths
128
+ if @options[:output].is_a? Hash
129
+ @options[:output] = symbolize_hash(@options[:output])
130
+ unless @options[:output].has_key? :fonts
131
+ raise Fontcustom::Error,
132
+ "Output paths (assigned as a hash) should have a :fonts key. Check your options."
133
+ end
134
+
135
+ @options[:output].each do |key, val|
136
+ @options[:output][key] = val
137
+ if File.exists?(val) && ! File.directory?(val)
138
+ raise Fontcustom::Error,
139
+ "Output `#{@options[:output][key]}` exists but isn't a directory. Check your options."
140
+ end
141
+ end
142
+
143
+ @options[:output][:css] ||= @options[:output][:fonts]
144
+ @options[:output][:preview] ||= @options[:output][:fonts]
145
+ else
146
+ if @options[:output].is_a? String
147
+ output = @options[:output]
148
+ if File.exists?(output) && ! File.directory?(output)
149
+ raise Fontcustom::Error,
150
+ "Output `#{output}` exists but isn't a directory. Check your options."
151
+ end
152
+ else
153
+ output = @options[:font_name]
154
+ say_message :debug, "Generated files will be saved to `#{output}/`." if @options[:debug]
155
+ end
156
+
157
+ @options[:output] = {
158
+ :fonts => output,
159
+ :css => output,
160
+ :preview => output
161
+ }
162
+ end
163
+ end
164
+
165
+ def check_template_paths
166
+ @options[:templates].each do |template|
167
+ next if %w|preview css scss scss-rails|.include? template
168
+ path = File.expand_path File.join(@options[:input][:templates], template) unless template[0] == "/"
169
+ unless File.exists? path
170
+ raise Fontcustom::Error,
171
+ "Custom template `#{template}` wasn't found in `#{@options[:input][:templates]}/`. Check your options."
172
+ end
173
+ end
174
+ end
175
+
176
+ def check_input(dir)
177
+ if ! File.exists? dir
178
+ raise Fontcustom::Error,
179
+ "Input `#{dir}` doesn't exist. Check your options."
180
+ elsif ! File.directory? dir
181
+ raise Fontcustom::Error,
182
+ "Input `#{dir}` isn't a directory. Check your options."
183
+ end
184
+ end
185
+
186
+ def print_debug
187
+ message = line_break(16)
188
+ message << @options.pretty_inspect.split("\n ").join(line_break(16))
189
+ say_message :debug, "Using options:#{message}"
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env python
2
+ # ***** BEGIN LICENSE BLOCK *****
3
+ # Version: MPL 1.1/GPL 2.0/LGPL 2.1
4
+ #
5
+ # The contents of this file are subject to the Mozilla Public License Version
6
+ # 1.1 (the "License"); you may not use this file except in compliance with
7
+ # the License. You may obtain a copy of the License at
8
+ # http://www.mozilla.org/MPL/
9
+ #
10
+ # Software distributed under the License is distributed on an "AS IS" basis,
11
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12
+ # for the specific language governing rights and limitations under the
13
+ # License.
14
+ #
15
+ # The Original Code is font utility code.
16
+ #
17
+ # The Initial Developer of the Original Code is Mozilla Corporation.
18
+ # Portions created by the Initial Developer are Copyright (C) 2009
19
+ # the Initial Developer. All Rights Reserved.
20
+ #
21
+ # Contributor(s):
22
+ # John Daggett <jdaggett@mozilla.com>
23
+ #
24
+ # Alternatively, the contents of this file may be used under the terms of
25
+ # either the GNU General Public License Version 2 or later (the "GPL"), or
26
+ # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27
+ # in which case the provisions of the GPL or the LGPL are applicable instead
28
+ # of those above. If you wish to allow use of your version of this file only
29
+ # under the terms of either the GPL or the LGPL, and not to allow others to
30
+ # use your version of this file under the terms of the MPL, indicate your
31
+ # decision by deleting the provisions above and replace them with the notice
32
+ # and other provisions required by the GPL or the LGPL. If you do not delete
33
+ # the provisions above, a recipient may use your version of this file under
34
+ # the terms of any one of the MPL, the GPL or the LGPL.
35
+ #
36
+ # ***** END LICENSE BLOCK ***** */
37
+
38
+ # eotlitetool.py - create EOT version of OpenType font for use with IE
39
+ #
40
+ # Usage: eotlitetool.py [-o output-filename] font1 [font2 ...]
41
+ #
42
+
43
+ # OpenType file structure
44
+ # http://www.microsoft.com/typography/otspec/otff.htm
45
+ #
46
+ # Types:
47
+ #
48
+ # BYTE 8-bit unsigned integer.
49
+ # CHAR 8-bit signed integer.
50
+ # USHORT 16-bit unsigned integer.
51
+ # SHORT 16-bit signed integer.
52
+ # ULONG 32-bit unsigned integer.
53
+ # Fixed 32-bit signed fixed-point number (16.16)
54
+ # LONGDATETIME Date represented in number of seconds since 12:00 midnight, January 1, 1904. The value is represented as a signed 64-bit integer.
55
+ #
56
+ # SFNT Header
57
+ #
58
+ # Fixed sfnt version // 0x00010000 for version 1.0.
59
+ # USHORT numTables // Number of tables.
60
+ # USHORT searchRange // (Maximum power of 2 <= numTables) x 16.
61
+ # USHORT entrySelector // Log2(maximum power of 2 <= numTables).
62
+ # USHORT rangeShift // NumTables x 16-searchRange.
63
+ #
64
+ # Table Directory
65
+ #
66
+ # ULONG tag // 4-byte identifier.
67
+ # ULONG checkSum // CheckSum for this table.
68
+ # ULONG offset // Offset from beginning of TrueType font file.
69
+ # ULONG length // Length of this table.
70
+ #
71
+ # OS/2 Table (Version 4)
72
+ #
73
+ # USHORT version // 0x0004
74
+ # SHORT xAvgCharWidth
75
+ # USHORT usWeightClass
76
+ # USHORT usWidthClass
77
+ # USHORT fsType
78
+ # SHORT ySubscriptXSize
79
+ # SHORT ySubscriptYSize
80
+ # SHORT ySubscriptXOffset
81
+ # SHORT ySubscriptYOffset
82
+ # SHORT ySuperscriptXSize
83
+ # SHORT ySuperscriptYSize
84
+ # SHORT ySuperscriptXOffset
85
+ # SHORT ySuperscriptYOffset
86
+ # SHORT yStrikeoutSize
87
+ # SHORT yStrikeoutPosition
88
+ # SHORT sFamilyClass
89
+ # BYTE panose[10]
90
+ # ULONG ulUnicodeRange1 // Bits 0-31
91
+ # ULONG ulUnicodeRange2 // Bits 32-63
92
+ # ULONG ulUnicodeRange3 // Bits 64-95
93
+ # ULONG ulUnicodeRange4 // Bits 96-127
94
+ # CHAR achVendID[4]
95
+ # USHORT fsSelection
96
+ # USHORT usFirstCharIndex
97
+ # USHORT usLastCharIndex
98
+ # SHORT sTypoAscender
99
+ # SHORT sTypoDescender
100
+ # SHORT sTypoLineGap
101
+ # USHORT usWinAscent
102
+ # USHORT usWinDescent
103
+ # ULONG ulCodePageRange1 // Bits 0-31
104
+ # ULONG ulCodePageRange2 // Bits 32-63
105
+ # SHORT sxHeight
106
+ # SHORT sCapHeight
107
+ # USHORT usDefaultChar
108
+ # USHORT usBreakChar
109
+ # USHORT usMaxContext
110
+ #
111
+ #
112
+ # The Naming Table is organized as follows:
113
+ #
114
+ # [name table header]
115
+ # [name records]
116
+ # [string data]
117
+ #
118
+ # Name Table Header
119
+ #
120
+ # USHORT format // Format selector (=0).
121
+ # USHORT count // Number of name records.
122
+ # USHORT stringOffset // Offset to start of string storage (from start of table).
123
+ #
124
+ # Name Record
125
+ #
126
+ # USHORT platformID // Platform ID.
127
+ # USHORT encodingID // Platform-specific encoding ID.
128
+ # USHORT languageID // Language ID.
129
+ # USHORT nameID // Name ID.
130
+ # USHORT length // String length (in bytes).
131
+ # USHORT offset // String offset from start of storage area (in bytes).
132
+ #
133
+ # head Table
134
+ #
135
+ # Fixed tableVersion // Table version number 0x00010000 for version 1.0.
136
+ # Fixed fontRevision // Set by font manufacturer.
137
+ # ULONG checkSumAdjustment // To compute: set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum.
138
+ # ULONG magicNumber // Set to 0x5F0F3CF5.
139
+ # USHORT flags
140
+ # USHORT unitsPerEm // Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines.
141
+ # LONGDATETIME created // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
142
+ # LONGDATETIME modified // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
143
+ # SHORT xMin // For all glyph bounding boxes.
144
+ # SHORT yMin
145
+ # SHORT xMax
146
+ # SHORT yMax
147
+ # USHORT macStyle
148
+ # USHORT lowestRecPPEM // Smallest readable size in pixels.
149
+ # SHORT fontDirectionHint
150
+ # SHORT indexToLocFormat // 0 for short offsets, 1 for long.
151
+ # SHORT glyphDataFormat // 0 for current format.
152
+ #
153
+ #
154
+ #
155
+ # Embedded OpenType (EOT) file format
156
+ # http://www.w3.org/Submission/EOT/
157
+ #
158
+ # EOT version 0x00020001
159
+ #
160
+ # An EOT font consists of a header with the original OpenType font
161
+ # appended at the end. Most of the data in the EOT header is simply a
162
+ # copy of data from specific tables within the font data. The exceptions
163
+ # are the 'Flags' field and the root string name field. The root string
164
+ # is a set of names indicating domains for which the font data can be
165
+ # used. A null root string implies the font data can be used anywhere.
166
+ # The EOT header is in little-endian byte order but the font data remains
167
+ # in big-endian order as specified by the OpenType spec.
168
+ #
169
+ # Overall structure:
170
+ #
171
+ # [EOT header]
172
+ # [EOT name records]
173
+ # [font data]
174
+ #
175
+ # EOT header
176
+ #
177
+ # ULONG eotSize // Total structure length in bytes (including string and font data)
178
+ # ULONG fontDataSize // Length of the OpenType font (FontData) in bytes
179
+ # ULONG version // Version number of this format - 0x00020001
180
+ # ULONG flags // Processing Flags (0 == no special processing)
181
+ # BYTE fontPANOSE[10] // OS/2 Table panose
182
+ # BYTE charset // DEFAULT_CHARSET (0x01)
183
+ # BYTE italic // 0x01 if ITALIC in OS/2 Table fsSelection is set, 0 otherwise
184
+ # ULONG weight // OS/2 Table usWeightClass
185
+ # USHORT fsType // OS/2 Table fsType (specifies embedding permission flags)
186
+ # USHORT magicNumber // Magic number for EOT file - 0x504C.
187
+ # ULONG unicodeRange1 // OS/2 Table ulUnicodeRange1
188
+ # ULONG unicodeRange2 // OS/2 Table ulUnicodeRange2
189
+ # ULONG unicodeRange3 // OS/2 Table ulUnicodeRange3
190
+ # ULONG unicodeRange4 // OS/2 Table ulUnicodeRange4
191
+ # ULONG codePageRange1 // OS/2 Table ulCodePageRange1
192
+ # ULONG codePageRange2 // OS/2 Table ulCodePageRange2
193
+ # ULONG checkSumAdjustment // head Table CheckSumAdjustment
194
+ # ULONG reserved[4] // Reserved - must be 0
195
+ # USHORT padding1 // Padding - must be 0
196
+ #
197
+ # EOT name records
198
+ #
199
+ # USHORT FamilyNameSize // Font family name size in bytes
200
+ # BYTE FamilyName[FamilyNameSize] // Font family name (name ID = 1), little-endian UTF-16
201
+ # USHORT Padding2 // Padding - must be 0
202
+ #
203
+ # USHORT StyleNameSize // Style name size in bytes
204
+ # BYTE StyleName[StyleNameSize] // Style name (name ID = 2), little-endian UTF-16
205
+ # USHORT Padding3 // Padding - must be 0
206
+ #
207
+ # USHORT VersionNameSize // Version name size in bytes
208
+ # bytes VersionName[VersionNameSize] // Version name (name ID = 5), little-endian UTF-16
209
+ # USHORT Padding4 // Padding - must be 0
210
+ #
211
+ # USHORT FullNameSize // Full name size in bytes
212
+ # BYTE FullName[FullNameSize] // Full name (name ID = 4), little-endian UTF-16
213
+ # USHORT Padding5 // Padding - must be 0
214
+ #
215
+ # USHORT RootStringSize // Root string size in bytes
216
+ # BYTE RootString[RootStringSize] // Root string, little-endian UTF-16
217
+
218
+
219
+
220
+ import optparse
221
+ import struct
222
+
223
+ class FontError(Exception):
224
+ """Error related to font handling"""
225
+ pass
226
+
227
+ def multichar(str):
228
+ vals = struct.unpack('4B', str[:4])
229
+ return (vals[0] << 24) + (vals[1] << 16) + (vals[2] << 8) + vals[3]
230
+
231
+ def multicharval(v):
232
+ return struct.pack('4B', (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF)
233
+
234
+ class EOT:
235
+ EOT_VERSION = 0x00020001
236
+ EOT_MAGIC_NUMBER = 0x504c
237
+ EOT_DEFAULT_CHARSET = 0x01
238
+ EOT_FAMILY_NAME_INDEX = 0 # order of names in variable portion of EOT header
239
+ EOT_STYLE_NAME_INDEX = 1
240
+ EOT_VERSION_NAME_INDEX = 2
241
+ EOT_FULL_NAME_INDEX = 3
242
+ EOT_NUM_NAMES = 4
243
+
244
+ EOT_HEADER_PACK = '<4L10B2BL2H7L18x'
245
+
246
+ class OpenType:
247
+ SFNT_CFF = multichar('OTTO') # Postscript CFF SFNT version
248
+ SFNT_TRUE = 0x10000 # Standard TrueType version
249
+ SFNT_APPLE = multichar('true') # Apple TrueType version
250
+
251
+ SFNT_UNPACK = '>I4H'
252
+ TABLE_DIR_UNPACK = '>4I'
253
+
254
+ TABLE_HEAD = multichar('head') # TrueType table tags
255
+ TABLE_NAME = multichar('name')
256
+ TABLE_OS2 = multichar('OS/2')
257
+ TABLE_GLYF = multichar('glyf')
258
+ TABLE_CFF = multichar('CFF ')
259
+
260
+ OS2_FSSELECTION_ITALIC = 0x1
261
+ OS2_UNPACK = '>4xH2xH22x10B4L4xH14x2L'
262
+
263
+ HEAD_UNPACK = '>8xL'
264
+
265
+ NAME_RECORD_UNPACK = '>6H'
266
+ NAME_ID_FAMILY = 1
267
+ NAME_ID_STYLE = 2
268
+ NAME_ID_UNIQUE = 3
269
+ NAME_ID_FULL = 4
270
+ NAME_ID_VERSION = 5
271
+ NAME_ID_POSTSCRIPT = 6
272
+ PLATFORM_ID_UNICODE = 0 # Mac OS uses this typically
273
+ PLATFORM_ID_MICROSOFT = 3
274
+ ENCODING_ID_MICROSOFT_UNICODEBMP = 1 # with Microsoft platformID BMP-only Unicode encoding
275
+ LANG_ID_MICROSOFT_EN_US = 0x0409 # with Microsoft platformID EN US lang code
276
+
277
+ def eotname(ttf):
278
+ i = ttf.rfind('.')
279
+ if i != -1:
280
+ ttf = ttf[:i]
281
+ return ttf + '.eotlite'
282
+
283
+ def readfont(f):
284
+ data = open(f, 'rb').read()
285
+ return data
286
+
287
+ def get_table_directory(data):
288
+ """read the SFNT header and table directory"""
289
+ datalen = len(data)
290
+ sfntsize = struct.calcsize(OpenType.SFNT_UNPACK)
291
+ if sfntsize > datalen:
292
+ raise FontError, 'truncated font data'
293
+ sfntvers, numTables = struct.unpack(OpenType.SFNT_UNPACK, data[:sfntsize])[:2]
294
+ if sfntvers != OpenType.SFNT_CFF and sfntvers != OpenType.SFNT_TRUE:
295
+ raise FontError, 'invalid font type';
296
+
297
+ font = {}
298
+ font['version'] = sfntvers
299
+ font['numTables'] = numTables
300
+
301
+ # create set of offsets, lengths for tables
302
+ table_dir_size = struct.calcsize(OpenType.TABLE_DIR_UNPACK)
303
+ if sfntsize + table_dir_size * numTables > datalen:
304
+ raise FontError, 'truncated font data, table directory extends past end of data'
305
+ table_dir = {}
306
+ for i in range(0, numTables):
307
+ start = sfntsize + i * table_dir_size
308
+ end = start + table_dir_size
309
+ tag, check, bongo, dirlen = struct.unpack(OpenType.TABLE_DIR_UNPACK, data[start:end])
310
+ table_dir[tag] = {'offset': bongo, 'length': dirlen, 'checksum': check}
311
+
312
+ font['tableDir'] = table_dir
313
+
314
+ return font
315
+
316
+ def get_name_records(nametable):
317
+ """reads through the name records within name table"""
318
+ name = {}
319
+ # read the header
320
+ headersize = 6
321
+ count, strOffset = struct.unpack('>2H', nametable[2:6])
322
+ namerecsize = struct.calcsize(OpenType.NAME_RECORD_UNPACK)
323
+ if count * namerecsize + headersize > len(nametable):
324
+ raise FontError, 'names exceed size of name table'
325
+ name['count'] = count
326
+ name['strOffset'] = strOffset
327
+
328
+ # read through the name records
329
+ namerecs = {}
330
+ for i in range(0, count):
331
+ start = headersize + i * namerecsize
332
+ end = start + namerecsize
333
+ platformID, encodingID, languageID, nameID, namelen, offset = struct.unpack(OpenType.NAME_RECORD_UNPACK, nametable[start:end])
334
+ if platformID != OpenType.PLATFORM_ID_MICROSOFT or \
335
+ encodingID != OpenType.ENCODING_ID_MICROSOFT_UNICODEBMP or \
336
+ languageID != OpenType.LANG_ID_MICROSOFT_EN_US:
337
+ continue
338
+ namerecs[nameID] = {'offset': offset, 'length': namelen}
339
+
340
+ name['namerecords'] = namerecs
341
+ return name
342
+
343
+ def make_eot_name_headers(fontdata, nameTableDir):
344
+ """extracts names from the name table and generates the names header portion of the EOT header"""
345
+ nameoffset = nameTableDir['offset']
346
+ namelen = nameTableDir['length']
347
+ name = get_name_records(fontdata[nameoffset : nameoffset + namelen])
348
+ namestroffset = name['strOffset']
349
+ namerecs = name['namerecords']
350
+
351
+ eotnames = (OpenType.NAME_ID_FAMILY, OpenType.NAME_ID_STYLE, OpenType.NAME_ID_VERSION, OpenType.NAME_ID_FULL)
352
+ nameheaders = []
353
+ for nameid in eotnames:
354
+ if nameid in namerecs:
355
+ namerecord = namerecs[nameid]
356
+ noffset = namerecord['offset']
357
+ nlen = namerecord['length']
358
+ nformat = '%dH' % (nlen / 2) # length is in number of bytes
359
+ start = nameoffset + namestroffset + noffset
360
+ end = start + nlen
361
+ nstr = struct.unpack('>' + nformat, fontdata[start:end])
362
+ nameheaders.append(struct.pack('<H' + nformat + '2x', nlen, *nstr))
363
+ else:
364
+ nameheaders.append(struct.pack('4x')) # len = 0, padding = 0
365
+
366
+ return ''.join(nameheaders)
367
+
368
+ # just return a null-string (len = 0)
369
+ def make_root_string():
370
+ return struct.pack('2x')
371
+
372
+ def make_eot_header(fontdata):
373
+ """given ttf font data produce an EOT header"""
374
+ fontDataSize = len(fontdata)
375
+ font = get_table_directory(fontdata)
376
+
377
+ # toss out .otf fonts, t2embed library doesn't support these
378
+ tableDir = font['tableDir']
379
+
380
+ # check for required tables
381
+ required = (OpenType.TABLE_HEAD, OpenType.TABLE_NAME, OpenType.TABLE_OS2)
382
+ for table in required:
383
+ if not (table in tableDir):
384
+ raise FontError, 'missing required table ' + multicharval(table)
385
+
386
+ # read name strings
387
+
388
+ # pull out data from individual tables to construct fixed header portion
389
+ # need to calculate eotSize before packing
390
+ version = EOT.EOT_VERSION
391
+ flags = 0
392
+ charset = EOT.EOT_DEFAULT_CHARSET
393
+ magicNumber = EOT.EOT_MAGIC_NUMBER
394
+
395
+ # read values from OS/2 table
396
+ os2Dir = tableDir[OpenType.TABLE_OS2]
397
+ os2offset = os2Dir['offset']
398
+ os2size = struct.calcsize(OpenType.OS2_UNPACK)
399
+
400
+ if os2size > os2Dir['length']:
401
+ raise FontError, 'OS/2 table invalid length'
402
+
403
+ os2fields = struct.unpack(OpenType.OS2_UNPACK, fontdata[os2offset : os2offset + os2size])
404
+
405
+ panose = []
406
+ urange = []
407
+ codepage = []
408
+
409
+ weight, fsType = os2fields[:2]
410
+ panose[:10] = os2fields[2:12]
411
+ urange[:4] = os2fields[12:16]
412
+ fsSelection = os2fields[16]
413
+ codepage[:2] = os2fields[17:19]
414
+
415
+ italic = fsSelection & OpenType.OS2_FSSELECTION_ITALIC
416
+
417
+ # read in values from head table
418
+ headDir = tableDir[OpenType.TABLE_HEAD]
419
+ headoffset = headDir['offset']
420
+ headsize = struct.calcsize(OpenType.HEAD_UNPACK)
421
+
422
+ if headsize > headDir['length']:
423
+ raise FontError, 'head table invalid length'
424
+
425
+ headfields = struct.unpack(OpenType.HEAD_UNPACK, fontdata[headoffset : headoffset + headsize])
426
+ checkSumAdjustment = headfields[0]
427
+
428
+ # make name headers
429
+ nameheaders = make_eot_name_headers(fontdata, tableDir[OpenType.TABLE_NAME])
430
+ rootstring = make_root_string()
431
+
432
+ # calculate the total eot size
433
+ eotSize = struct.calcsize(EOT.EOT_HEADER_PACK) + len(nameheaders) + len(rootstring) + fontDataSize
434
+ fixed = struct.pack(EOT.EOT_HEADER_PACK,
435
+ *([eotSize, fontDataSize, version, flags] + panose + [charset, italic] +
436
+ [weight, fsType, magicNumber] + urange + codepage + [checkSumAdjustment]))
437
+
438
+ return ''.join((fixed, nameheaders, rootstring))
439
+
440
+
441
+ def write_eot_font(eot, header, data):
442
+ open(eot,'wb').write(''.join((header, data)))
443
+ return
444
+
445
+ def main():
446
+
447
+ # deal with options
448
+ p = optparse.OptionParser()
449
+ p.add_option('--output', '-o', default="world")
450
+ options, args = p.parse_args()
451
+
452
+ # iterate over font files
453
+ for f in args:
454
+ data = readfont(f)
455
+ if len(data) == 0:
456
+ print 'Error reading %s' % f
457
+ else:
458
+ eot = eotname(f)
459
+ header = make_eot_header(data)
460
+ write_eot_font(eot, header, data)
461
+
462
+
463
+ if __name__ == '__main__':
464
+ main()
465
+
466
+