gkh-fontcustom 1.3.7

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.
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
+