gkh-fontcustom 1.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.travis.yml +20 -0
- data/CHANGELOG.md +137 -0
- data/CONTRIBUTING.md +50 -0
- data/Gemfile +4 -0
- data/LICENSES.txt +60 -0
- data/README.md +112 -0
- data/Rakefile +8 -0
- data/bin/fontcustom +5 -0
- data/fontcustom.gemspec +28 -0
- data/gemfiles/Gemfile.listen_1 +6 -0
- data/lib/fontcustom.rb +43 -0
- data/lib/fontcustom/base.rb +54 -0
- data/lib/fontcustom/cli.rb +110 -0
- data/lib/fontcustom/error.rb +4 -0
- data/lib/fontcustom/generator/font.rb +109 -0
- data/lib/fontcustom/generator/template.rb +222 -0
- data/lib/fontcustom/manifest.rb +75 -0
- data/lib/fontcustom/options.rb +192 -0
- data/lib/fontcustom/scripts/eotlitetool.py +466 -0
- data/lib/fontcustom/scripts/generate.py +128 -0
- data/lib/fontcustom/scripts/sfnt2woff +0 -0
- data/lib/fontcustom/templates/_fontcustom-rails.scss +14 -0
- data/lib/fontcustom/templates/_fontcustom.scss +16 -0
- data/lib/fontcustom/templates/fontcustom-preview.html +174 -0
- data/lib/fontcustom/templates/fontcustom.css +14 -0
- data/lib/fontcustom/templates/fontcustom.yml +96 -0
- data/lib/fontcustom/utility.rb +117 -0
- data/lib/fontcustom/version.rb +3 -0
- data/lib/fontcustom/watcher.rb +90 -0
- data/spec/fixtures/example/_example-rails.scss +50 -0
- data/spec/fixtures/example/example-preview.html +253 -0
- data/spec/fixtures/example/example.css +50 -0
- data/spec/fixtures/example/example.eot +0 -0
- data/spec/fixtures/example/example.svg +75 -0
- data/spec/fixtures/example/example.ttf +0 -0
- data/spec/fixtures/example/example.woff +0 -0
- data/spec/fixtures/generators/.fontcustom-manifest-corrupted.json +25 -0
- data/spec/fixtures/generators/.fontcustom-manifest-empty.json +0 -0
- data/spec/fixtures/generators/.fontcustom-manifest.json +52 -0
- data/spec/fixtures/generators/fontcustom.yml +1 -0
- data/spec/fixtures/generators/mixed-output/another-font.ttf +0 -0
- data/spec/fixtures/generators/mixed-output/dont-delete-me.bro +0 -0
- data/spec/fixtures/generators/mixed-output/fontcustom.css +108 -0
- data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.eot +0 -0
- data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.svg +56 -0
- data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.ttf +0 -0
- data/spec/fixtures/generators/mixed-output/fontcustom_82a59e769bc60192484f2620570bbb59.woff +0 -0
- data/spec/fixtures/options/any-file-name.yml +1 -0
- data/spec/fixtures/options/config-is-in-dir/fontcustom.yml +1 -0
- data/spec/fixtures/options/fontcustom-empty.yml +1 -0
- data/spec/fixtures/options/fontcustom-malformed.yml +1 -0
- data/spec/fixtures/options/fontcustom.yml +1 -0
- data/spec/fixtures/options/no-config-here/.gitkeep +0 -0
- data/spec/fixtures/options/rails-like/config/fontcustom.yml +1 -0
- data/spec/fixtures/shared/not-a-dir +0 -0
- data/spec/fixtures/shared/templates/custom.css +4 -0
- data/spec/fixtures/shared/templates/regular.css +4 -0
- data/spec/fixtures/shared/vectors-empty/no_vectors_here.txt +0 -0
- data/spec/fixtures/shared/vectors/C.svg +14 -0
- data/spec/fixtures/shared/vectors/D.svg +15 -0
- data/spec/fixtures/shared/vectors/a_R3ally-eXotic f1Le Name.svg +6 -0
- data/spec/fontcustom/base_spec.rb +45 -0
- data/spec/fontcustom/cli_spec.rb +30 -0
- data/spec/fontcustom/generator/font_spec.rb +72 -0
- data/spec/fontcustom/generator/template_spec.rb +99 -0
- data/spec/fontcustom/manifest_spec.rb +17 -0
- data/spec/fontcustom/options_spec.rb +315 -0
- data/spec/fontcustom/utility_spec.rb +82 -0
- data/spec/fontcustom/watcher_spec.rb +121 -0
- data/spec/spec_helper.rb +103 -0
- 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
|
+
|