fontprocessor 27.1.3
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rbenv-version +1 -0
- data/.rspec +0 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +64 -0
- data/CHANGELOG_FPV14.md +20 -0
- data/Dockerfile +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +156 -0
- data/Guardfile +5 -0
- data/README.md +6 -0
- data/Rakefile +53 -0
- data/Rakefile.base +57 -0
- data/bin/process-file +41 -0
- data/config/development.yml +5 -0
- data/config/production.yml +3 -0
- data/config/staging.yml +3 -0
- data/config/test.yml +5 -0
- data/fontprocessor.gemspec +37 -0
- data/lib/fontprocessor/config.rb +85 -0
- data/lib/fontprocessor/external/batik/batik-ttf2svg.jar +0 -0
- data/lib/fontprocessor/external/batik/lib/batik-svggen.jar +0 -0
- data/lib/fontprocessor/external/batik/lib/batik-util.jar +0 -0
- data/lib/fontprocessor/external/fontforge/subset.py +30 -0
- data/lib/fontprocessor/external/fontforge/utils.py +66 -0
- data/lib/fontprocessor/external_execution.rb +117 -0
- data/lib/fontprocessor/font_file.rb +149 -0
- data/lib/fontprocessor/font_file_naming_strategy.rb +63 -0
- data/lib/fontprocessor/font_format.rb +29 -0
- data/lib/fontprocessor/process_font_job.rb +227 -0
- data/lib/fontprocessor/processed_font_iterator.rb +89 -0
- data/lib/fontprocessor/processor.rb +790 -0
- data/lib/fontprocessor/version.rb +3 -0
- data/lib/fontprocessor.rb +16 -0
- data/scripts/build_and_test.sh +15 -0
- data/scripts/get_production_source_map.rb +53 -0
- data/spec/fixtures/bad_os2_width_class.otf +0 -0
- data/spec/fixtures/extra_language_names.otf +0 -0
- data/spec/fixtures/fixtures.rb +35 -0
- data/spec/fixtures/locked.otf +0 -0
- data/spec/fixtures/op_size.otf +0 -0
- data/spec/fixtures/ots_failure_font.otf +0 -0
- data/spec/fixtures/postscript.otf +0 -0
- data/spec/fixtures/stat_font.otf +0 -0
- data/spec/fixtures/truetype.otf +0 -0
- data/spec/lib/fontprocessor/config_spec.rb +38 -0
- data/spec/lib/fontprocessor/external_execution_spec.rb +33 -0
- data/spec/lib/fontprocessor/font_file_naming_strategy_spec.rb +13 -0
- data/spec/lib/fontprocessor/font_file_spec.rb +110 -0
- data/spec/lib/fontprocessor/process_font_job_spec.rb +317 -0
- data/spec/lib/fontprocessor/processed_font_iterator_spec.rb +128 -0
- data/spec/lib/fontprocessor/processor_spec.rb +466 -0
- data/spec/spec_helper.rb +4 -0
- data/tasks/fonts.rake +30 -0
- data/worker.rb +23 -0
- metadata +312 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
import sys
|
2
|
+
import simplejson
|
3
|
+
|
4
|
+
def parse_input():
|
5
|
+
"""
|
6
|
+
Used to get the information from the calling process
|
7
|
+
"""
|
8
|
+
data = sys.stdin.readline()
|
9
|
+
return simplejson.loads(data)
|
10
|
+
|
11
|
+
def complete(data):
|
12
|
+
"""
|
13
|
+
Prints a JSON dictionary to stdout to communicate status to the calling
|
14
|
+
process.
|
15
|
+
"""
|
16
|
+
print simplejson.dumps(data)
|
17
|
+
|
18
|
+
def original_metrics(original):
|
19
|
+
# Calculate the tallest and widest glyph in ems
|
20
|
+
glyphs = list(original.glyphs())
|
21
|
+
ascent = max([r.boundingBox()[3] for r in glyphs])
|
22
|
+
descent = -1 * min([r.boundingBox()[1] for r in glyphs])
|
23
|
+
|
24
|
+
results = {}
|
25
|
+
results['os2_winascent'] = original.os2_winascent + ascent
|
26
|
+
results['os2_windescent'] = original.os2_windescent + descent
|
27
|
+
|
28
|
+
results['os2_typoascent'] = original.os2_typoascent + original.ascent
|
29
|
+
results['os2_typodescent'] = original.os2_typodescent + (-1 * original.descent)
|
30
|
+
|
31
|
+
results['hhea_ascent'] = original.hhea_ascent + ascent
|
32
|
+
results['hhea_descent'] = original.hhea_descent + (-1 * descent)
|
33
|
+
|
34
|
+
# It seems to adjust the underline position and thickness
|
35
|
+
results['upos'] = float(original.upos)
|
36
|
+
|
37
|
+
return results
|
38
|
+
|
39
|
+
def lossless_save(original_metrics, font, output_filename):
|
40
|
+
"""
|
41
|
+
For some reason fontforge likes to modify things in it's output files, such
|
42
|
+
as underline position. So we are undoing it to maintain the integrity of
|
43
|
+
the font.
|
44
|
+
"""
|
45
|
+
font.os2_winascent_add = 0 # turns off the offset, stupid naming if you ask me.
|
46
|
+
font.os2_winascent = original_metrics['os2_winascent']
|
47
|
+
font.os2_windescent_add = 0
|
48
|
+
font.os2_windescent = original_metrics['os2_windescent']
|
49
|
+
|
50
|
+
font.os2_typoascent_add = 0
|
51
|
+
font.os2_typoascent = original_metrics['os2_typoascent']
|
52
|
+
font.os2_typodescent_add = 0
|
53
|
+
font.os2_typodescent = original_metrics['os2_typodescent']
|
54
|
+
|
55
|
+
font.hhea_ascent_add = 0
|
56
|
+
font.hhea_ascent = original_metrics['hhea_ascent']
|
57
|
+
font.hhea_descent_add = 0
|
58
|
+
font.hhea_descent = original_metrics['hhea_descent']
|
59
|
+
|
60
|
+
# Force the font family name to be correct.
|
61
|
+
font.familyname = font.fullname.split('-')[0]
|
62
|
+
|
63
|
+
# It seems to adjust the underline position and thickness
|
64
|
+
font.upos = original_metrics['upos']
|
65
|
+
|
66
|
+
font.generate(output_filename)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open4'
|
3
|
+
|
4
|
+
module FontProcessor
|
5
|
+
class Error < RuntimeError;end
|
6
|
+
class ExternalProgramError < Error;end
|
7
|
+
|
8
|
+
class ExternalStdOutCommand
|
9
|
+
# Public: Executes the given command in a blocking manner and expects no
|
10
|
+
# output will be returned.
|
11
|
+
#
|
12
|
+
# Note: Standard output and standard error are combined. Output on either
|
13
|
+
# constitutes output.
|
14
|
+
#
|
15
|
+
# command - The command to execute.
|
16
|
+
# opts[:expect_output] - A boolean that determines whether you expect
|
17
|
+
# output to be returned in normal operation of the
|
18
|
+
# program.
|
19
|
+
# opts[:timeout] - The number of seconds to wait before terminating
|
20
|
+
# the program with a TERM signal. Defaults to 10
|
21
|
+
# seconds.
|
22
|
+
#
|
23
|
+
# Returns the output from the command.
|
24
|
+
#
|
25
|
+
# Raises ExternalProgramError if output was returned and none was expected
|
26
|
+
# or if no ouput was returned and some was expected.
|
27
|
+
def self.run(command, opts={})
|
28
|
+
expect_output = opts[:expect_output] || false
|
29
|
+
timeout = opts[:timeout] || 10
|
30
|
+
output = ""
|
31
|
+
|
32
|
+
Open4::popen4(command+ " 2>&1") do |pid, stdin, stdout, stderr|
|
33
|
+
result,_,_ = select([stdout], nil, nil, timeout)
|
34
|
+
|
35
|
+
output = nil
|
36
|
+
output = result[0].read() if result
|
37
|
+
|
38
|
+
# Make sure the subprocess is dead
|
39
|
+
Process.kill "TERM", pid
|
40
|
+
end
|
41
|
+
|
42
|
+
if expect_output == :return
|
43
|
+
return output
|
44
|
+
elsif expect_output
|
45
|
+
return output
|
46
|
+
elsif expect_output and output and output.strip == ''
|
47
|
+
raise ExternalProgramError, "Expected output but none received for \"#{command}\""
|
48
|
+
elsif not expect_output and output and output.strip != ''
|
49
|
+
raise ExternalProgramError, "Unexpected output for \"#{command}\": #{output}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
class ExternalJSONProgram
|
54
|
+
# Public: Executes a command in a blocking manner that expects it's input
|
55
|
+
# to be json and will return it's output as json.
|
56
|
+
#
|
57
|
+
# program - The command to execute.
|
58
|
+
# parameters - A ruby object that can be safely transformed into
|
59
|
+
# JSON that will be passed to the given program on
|
60
|
+
# standard input.
|
61
|
+
# parameters[:timeout] - A special parameter which determines how long the
|
62
|
+
# command should run. It is removed from the
|
63
|
+
# parameters hash before being passed to the
|
64
|
+
# command.
|
65
|
+
#
|
66
|
+
# Returns a Ruby Object representing the returned json output if no error
|
67
|
+
# occurred. Otherwise a Hash is returned with "status" set to "Failure" and
|
68
|
+
# a message key explaining why.
|
69
|
+
#
|
70
|
+
# Raises ExternalProgramError if the command times out.
|
71
|
+
def self.run(program, parameters)
|
72
|
+
timeout = parameters.delete(:timeout) || 10
|
73
|
+
parsed = {}
|
74
|
+
|
75
|
+
Open4::popen4(program) do |pid, stdin, stdout, stderr|
|
76
|
+
stdin.write(parameters.to_json+"\n")
|
77
|
+
stdin.close
|
78
|
+
|
79
|
+
# Grab stdout or stderr. If stderr comes up first capture it until stdout is ready
|
80
|
+
result = nil
|
81
|
+
loop do
|
82
|
+
result,_,_ = select([stdout,stderr], nil, nil, timeout)
|
83
|
+
if result and result[0].eof?
|
84
|
+
break
|
85
|
+
elsif result and result[0] == stderr
|
86
|
+
parsed['stderr'] = "" unless parsed.has_key? 'stderr'
|
87
|
+
parsed['stderr'] += stderr.readpartial(4096)
|
88
|
+
else
|
89
|
+
break
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if result
|
94
|
+
begin
|
95
|
+
parsed = JSON.parse(stdout.read())
|
96
|
+
|
97
|
+
if stderr
|
98
|
+
parsed['stderr'] = "" unless parsed.has_key? 'stderr'
|
99
|
+
parsed['stderr'] += stderr.read()
|
100
|
+
end
|
101
|
+
rescue
|
102
|
+
raise Exception, stderr.read() if stderr
|
103
|
+
end
|
104
|
+
else
|
105
|
+
parsed = {'status' => 'Failure', 'message' => "Took longer than #{timeout} seconds to respond"}
|
106
|
+
end
|
107
|
+
|
108
|
+
Process.kill "TERM", pid
|
109
|
+
end
|
110
|
+
|
111
|
+
raise ExternalProgramError, "#{program}: #{parsed['message']}" if parsed['status'] == 'Failure'
|
112
|
+
|
113
|
+
return parsed
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module FontProcessor
|
2
|
+
# Inspects an OpenType font file for various tidbits of information.
|
3
|
+
class FontFile
|
4
|
+
def initialize(filename)
|
5
|
+
@filename = filename
|
6
|
+
raise "Font file not found: #{@filename}" unless File.exists? @filename
|
7
|
+
@font_manipulator = Skytype::FontManipulator.new(File.binread(@filename))
|
8
|
+
raise "Failed to create Skytype::FontManipulator" if @font_manipulator.nil?
|
9
|
+
@unicode = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
# Public: Inspects the name table of the font, which contains various
|
13
|
+
# metadata from the foundry/designer.
|
14
|
+
#
|
15
|
+
# In reality these table contains localized versions of these strings for
|
16
|
+
# multiple operating systems. To simplify things, we have chosen to use
|
17
|
+
# only use the English versions of these names for the Windows Platform.
|
18
|
+
#
|
19
|
+
# Most of the time this isn't an issue however occasionally
|
20
|
+
# foundries/designers place different values in different
|
21
|
+
# platforms/languages.
|
22
|
+
#
|
23
|
+
# Returns a Hash of name record labels and their values. Possible keys include:
|
24
|
+
# 'Copyright', 'Family', 'Subfamily', 'Unique ID', 'Full name',
|
25
|
+
# 'Version', 'PostScript name', 'Trademark', 'Manufacturer', 'Designer',
|
26
|
+
# 'Description', 'Vendor URL', 'Designer URL', 'License Description',
|
27
|
+
# 'License URL', 'Reserved', 'Preferred Family', 'Preferred Subfamily',
|
28
|
+
# 'Compatible Full', 'Sample text', 'PostScript CID', 'WWS Family Name',
|
29
|
+
# 'WWS Subfamily Name'
|
30
|
+
def names
|
31
|
+
Skytype::FontUtils.get_names_as_hash(@font_manipulator)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Inspects the font for various pieces of metadata (mostly metrics)
|
35
|
+
# about the font.
|
36
|
+
#
|
37
|
+
# Returns Hash including the following keys:
|
38
|
+
# em, max_ascent, max_descent, os2_panose, upos,
|
39
|
+
# optical_size, os2_winascent, os2_windescent,
|
40
|
+
# os2_typoascent, os2_typodescent, os2_typolinegap,
|
41
|
+
# hhea_ascent, hhea_descent, hhea_linegap
|
42
|
+
# os2_weight, os2_width
|
43
|
+
def metrics
|
44
|
+
metrics ||= Skytype::FontUtils.get_metrics_as_hash(@font_manipulator)
|
45
|
+
metrics.merge!({ "optical_size" => optical_size }) unless metrics.has_key?("optical_size")
|
46
|
+
metrics
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Gets the total number of glyphs in the font.
|
50
|
+
#
|
51
|
+
# Returns an unsigned 16-bit integer
|
52
|
+
def glyph_count
|
53
|
+
@font_manipulator.get_num_glyphs
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Gets any Optical Size feature from the font.
|
57
|
+
#
|
58
|
+
# Returns an unsigned 32-bit integer or nil if no Optical Size feature exists
|
59
|
+
def optical_size
|
60
|
+
@font_manipulator.get_optical_size
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: Gets a list of GSUB and GPOS features, in a simpler
|
64
|
+
# version than that returned by skytype.
|
65
|
+
#
|
66
|
+
# Returns a JSON formatted string of GSUB and GPOS features
|
67
|
+
def features
|
68
|
+
feature_hash = {}
|
69
|
+
features = JSON.parse @font_manipulator.extract_features
|
70
|
+
features['tables'].each do |table|
|
71
|
+
feature_hash["#{table['tag']}"] = []
|
72
|
+
table['features'].each do |feature|
|
73
|
+
feature_hash["#{table['tag']}"].push feature['tag']
|
74
|
+
end
|
75
|
+
end
|
76
|
+
JSON.generate feature_hash
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Inspects which Unicode values are contained within the font.
|
80
|
+
#
|
81
|
+
# Returns an Array of Unicode ids represented as Fixnums contained within
|
82
|
+
# the font.
|
83
|
+
def unicode
|
84
|
+
@unicode ||= @font_manipulator.unicode_as_binary_entries
|
85
|
+
@unicode
|
86
|
+
end
|
87
|
+
|
88
|
+
# Public: similar to .unicode, it returns the same data but in a compact
|
89
|
+
# form where contigious integers are represented as a Range.
|
90
|
+
#
|
91
|
+
# Returns an Array of Unicode ids represented as Fixnums (or Ranges if
|
92
|
+
# contigious) contained within the font. Characters which in the cmap table
|
93
|
+
# are not returned.
|
94
|
+
def unicode_ranges
|
95
|
+
compact_ranges(unicode)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns true when the font contains PostScript (sometimes referred to as
|
99
|
+
# CFF) outlines
|
100
|
+
def has_postscript_outlines?
|
101
|
+
@font_manipulator.is_post_script_font?
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true when the font contains TrueType (sometimes referred to as
|
105
|
+
# TT) outlines
|
106
|
+
def has_truetype_outlines?
|
107
|
+
@font_manipulator.has_table("glyf")
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
# Takes an array of Fixnums and turns consecutive numbers into ranges.
|
112
|
+
#
|
113
|
+
# array - The Array to compact.
|
114
|
+
#
|
115
|
+
# Returns an Array of ranges.
|
116
|
+
def compact_ranges(array)
|
117
|
+
array = array.sort
|
118
|
+
result = []
|
119
|
+
current_range = nil
|
120
|
+
|
121
|
+
(array.length - 1).times do |n|
|
122
|
+
consecutive = (array[n+1] - array[n]) == 1 ? true : false
|
123
|
+
|
124
|
+
if consecutive
|
125
|
+
result.pop if result.last == array[n]
|
126
|
+
|
127
|
+
if current_range
|
128
|
+
current_range = current_range.min..array[n+1]
|
129
|
+
else
|
130
|
+
current_range = array[n]..array[n+1]
|
131
|
+
end
|
132
|
+
else
|
133
|
+
result.pop if result.last == array[n]
|
134
|
+
if current_range
|
135
|
+
result << current_range
|
136
|
+
current_range = nil
|
137
|
+
else
|
138
|
+
result << array[n]
|
139
|
+
end
|
140
|
+
result << array[n+1]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
result << current_range if current_range
|
144
|
+
|
145
|
+
result
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module FontProcessor
|
2
|
+
class FontFileNamingStrategy
|
3
|
+
|
4
|
+
def initialize(file_name, file_dir)
|
5
|
+
@file_name = file_name
|
6
|
+
@file_dir = file_dir
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: Generates the file path for both the pristine (original from the
|
10
|
+
# foundry file) and the source file (which is the typekit modified base
|
11
|
+
# file) if a FontFormat is given.
|
12
|
+
#
|
13
|
+
# font_format - If nothing is given then the file path returned is for the
|
14
|
+
# pristine file. If a FontFormat is given, that the source
|
15
|
+
# file path for that FontFormat is for.
|
16
|
+
#
|
17
|
+
# Returns either the file path for the pristine file or the source file
|
18
|
+
# depending on font_format.
|
19
|
+
def source(font_format=nil)
|
20
|
+
if font_format
|
21
|
+
File.join(@file_dir, "source.#{font_format.extension}")
|
22
|
+
else
|
23
|
+
File.join(@file_dir, @file_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Generates the file path for a pristine file that has been only
|
28
|
+
# unlocked. Useful as fontforge can't inspect any files which are locked
|
29
|
+
# and some foundries give us locked prisitine files.
|
30
|
+
#
|
31
|
+
# Returns a file path for a pristine file that has been unlocked
|
32
|
+
# and no other manipulations have occurred.
|
33
|
+
def unlocked(font_format)
|
34
|
+
File.join(@file_dir, "unlocked.#{font_format.extension}")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Generates the file path of the primary source file. The primary
|
38
|
+
# is defined as the PostScript source file if it exists and otherwise the
|
39
|
+
# TrueType source file.
|
40
|
+
#
|
41
|
+
# Returns the file path to the primary source file.
|
42
|
+
def primary_source
|
43
|
+
cff = FontFormat.new(:cff, :otf)
|
44
|
+
if File.exists?(source(cff))
|
45
|
+
source(cff)
|
46
|
+
else
|
47
|
+
source(FontFormat.new(:ttf, :otf))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Public: Generates the file path to a specific character subset of a given
|
52
|
+
# file format.
|
53
|
+
#
|
54
|
+
# charset_id - The CharsetID representing the character set included within
|
55
|
+
# the file.
|
56
|
+
# font_format - The FontFormat of the file.
|
57
|
+
#
|
58
|
+
# Returns the file path to the primary source file.
|
59
|
+
def char_set(charset_id, font_format)
|
60
|
+
File.join(@file_dir, "charset-#{charset_id}-#{font_format.container_format}.#{font_format.extension}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FontProcessor
|
2
|
+
class FontFormat
|
3
|
+
# Public: The outline format of the file. One of :ttf, :cff
|
4
|
+
attr_accessor :outline_format
|
5
|
+
|
6
|
+
# Public: The container format of the file. One of :dyna_base, :otf, :inst, :woff, :woff2, :svg or :swf
|
7
|
+
attr_accessor :container_format
|
8
|
+
|
9
|
+
# outline_format - The symbol representing the outline_format.
|
10
|
+
# container_format - The symbol representing the container_format.
|
11
|
+
def initialize(outline_format, container_format)
|
12
|
+
@outline_format = outline_format
|
13
|
+
@container_format = container_format
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Used to generate the fontforge compatible file extension.
|
17
|
+
# Unfortunately fontforge uses the file extension to determine what sort of
|
18
|
+
# outlines should be contained within a file.
|
19
|
+
#
|
20
|
+
# They have chosen to represent TrueType files as .ttf and PostScript files
|
21
|
+
# as .otf
|
22
|
+
#
|
23
|
+
# Returns the symbol matching the proper file extension for this format
|
24
|
+
# based on the .outline_format.
|
25
|
+
def extension
|
26
|
+
{ :ttf => :ttf, :cff => :otf }[outline_format]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'fontbase'
|
2
|
+
require 'resque'
|
3
|
+
|
4
|
+
module FontProcessor
|
5
|
+
class ProcessFontJob
|
6
|
+
|
7
|
+
# status_key - The redis key to update with the status of this task
|
8
|
+
# json_request - A String or Hash of the fully formed JSON request. See FontProcessor::WebRequestJSON.
|
9
|
+
def self.perform(status_key, json_request)
|
10
|
+
json_request = JSON.parse(json_request) unless json_request.is_a? Hash
|
11
|
+
|
12
|
+
# For now, we only handle web requests
|
13
|
+
begin
|
14
|
+
json_request['request_type'] == "web_processing_request" or raise "Invalid request type! json_request['request_type'] = #{json_request['request_type']}"
|
15
|
+
|
16
|
+
charset_data = json_request['charset'] or raise "No charset in JSON Request"
|
17
|
+
charset_id = charset_data['charset_id'] or raise "No charset_id set in JSON Request's charset"
|
18
|
+
# check unicode and features, but don't assign to a var here
|
19
|
+
charset_data['unicode'] or raise "No unicode set in JSON Request's charset"
|
20
|
+
charset_data['features'] or raise "No features set in JSON Request's charset"
|
21
|
+
|
22
|
+
font_base_id = json_request['font_base_id'] or raise "No font_base_id set on JSON Request"
|
23
|
+
fpv = json_request['fpv'] or raise "No fpv set on JSON Request"
|
24
|
+
|
25
|
+
formats = json_request['formats']
|
26
|
+
formats.has_key?('convert') or raise "No conversion set on JSON Request formats block"
|
27
|
+
formats['derivatives'] or raise "No derivatives set on JSON Request formats block"
|
28
|
+
formats['process_original'] or raise "No process_original value set on JSON Request formats block"
|
29
|
+
rescue
|
30
|
+
report_failure(status_key)
|
31
|
+
raise
|
32
|
+
end
|
33
|
+
|
34
|
+
log(font_base_id, charset_id, "Starting processing job...")
|
35
|
+
|
36
|
+
temporary_directory = "/tmp/#{font_base_id}-#{charset_id}"
|
37
|
+
FileUtils.mkdir_p(temporary_directory)
|
38
|
+
|
39
|
+
|
40
|
+
begin
|
41
|
+
raise InvalidArgument, "charset_id is #{charset_id}, but should be one of 1, 2 or 3" unless charset_id == "1" || charset_id == "2" || charset_id == "3"
|
42
|
+
|
43
|
+
fetch_files(font_base_id, temporary_directory)
|
44
|
+
log(font_base_id, charset_id, "Files fetched")
|
45
|
+
metadata = process_files(font_base_id, charset_data, formats, temporary_directory)
|
46
|
+
log(font_base_id, charset_id, "Files processed")
|
47
|
+
upload_metadata(fpv, font_base_id, metadata)
|
48
|
+
log(font_base_id, charset_id, "Metadata uploaded")
|
49
|
+
upload_files(fpv, font_base_id, charset_id, formats, temporary_directory)
|
50
|
+
log(font_base_id, charset_id, "Files uploaded")
|
51
|
+
report_success(status_key)
|
52
|
+
rescue => e
|
53
|
+
report_failure(status_key)
|
54
|
+
log(font_base_id, charset_id, e.message)
|
55
|
+
raise e
|
56
|
+
ensure
|
57
|
+
FileUtils.rm_rf(temporary_directory)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Retrieves files from the FontBase and saves them to a temporary local
|
62
|
+
# directory.
|
63
|
+
#
|
64
|
+
# font_base_id - The FontBase id of the font to fetch.
|
65
|
+
# temporary_direction - The full path to store the retrieved files. The
|
66
|
+
# client is responsible for cleanup of the directory
|
67
|
+
# after use.
|
68
|
+
#
|
69
|
+
# Returns nothing.
|
70
|
+
# Raises FontBase::TransientError if there was an error retrieving the
|
71
|
+
# files from the FontBase.
|
72
|
+
def self.fetch_files(font_base_id, temporary_directory)
|
73
|
+
files = client.get_files(font_base_id)
|
74
|
+
files.each do |outline_type, data|
|
75
|
+
filename = File.join(temporary_directory, outline_type_to_original_filename(outline_type))
|
76
|
+
File.open(filename, "wb") { |f| f.write(data) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Creates a suitable temporary filename for the original font files
|
81
|
+
# retrieved from the fontbase.
|
82
|
+
#
|
83
|
+
# Note: This can't use cff as an extension because fontforge doesn't know
|
84
|
+
# how to open it properly.
|
85
|
+
#
|
86
|
+
# outline_type - either "cff" or "ttf"
|
87
|
+
#
|
88
|
+
# Returns a filename name for a given outline type.
|
89
|
+
# Raises ArgumentError if the outline_type contains an invalid value.
|
90
|
+
def self.outline_type_to_original_filename(outline_type)
|
91
|
+
case outline_type
|
92
|
+
when "cff"
|
93
|
+
"original.otf"
|
94
|
+
when "ttf"
|
95
|
+
"original.ttf"
|
96
|
+
else
|
97
|
+
raise ArgumentError, "#{outline_type} must be either 'cff' or 'ttf'"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns a configured FontStore client.
|
102
|
+
def self.client
|
103
|
+
@client ||= FontBase::Client.new do |c|
|
104
|
+
c.connection = Mongo::Connection.new(Config.mongo_host)
|
105
|
+
c.db_name = Config.mongo_db
|
106
|
+
c.s3_bucket = Config.s3_source_bucket
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Processes local files using a Processor and saves metadata to FontBase,
|
111
|
+
# if it doesn't already exist.
|
112
|
+
#
|
113
|
+
# font_base_id - The FontBase id of the font to fetch.
|
114
|
+
# charset_data - A JSON blob containing the complete definition for the charset
|
115
|
+
# including charset_id, features and unicode list.
|
116
|
+
# formats - A JSON blob containing the formats to process
|
117
|
+
# including process_orignal, convert and a derivatives array.
|
118
|
+
# directory - Local directory of files to process.
|
119
|
+
# lock - (Testing only) boolean to determine if the raw files should
|
120
|
+
# be locked or not. Useful to disable if you want to inspect
|
121
|
+
# the results.
|
122
|
+
#
|
123
|
+
# Returns a FontProcessor::FontFile object that can be used to retrieve the
|
124
|
+
# processed font's metadata.
|
125
|
+
# Raises Exception if an underlying external tool encounters an error while
|
126
|
+
# processing a file.
|
127
|
+
def self.process_files(font_base_id, charset_data, formats, directory, lock=true)
|
128
|
+
filename = select_preferred_original(directory)
|
129
|
+
naming_strategy = FontFileNamingStrategy.new(filename, directory)
|
130
|
+
|
131
|
+
processor = FontProcessor::Processor.new(naming_strategy, "http://typekit.com/eulas/#{font_base_id}", font_base_id)
|
132
|
+
|
133
|
+
processor.generate_char_set(charset_data, formats, lock)
|
134
|
+
|
135
|
+
processor.file_metadata(formats)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Upload metadata from a processed font file to fontbase using the client.
|
139
|
+
#
|
140
|
+
# fpv - A String identifier for the FontProcessingVersion
|
141
|
+
# font_base_id - The FontBase id of the font to upload metadata for.
|
142
|
+
# metadata - A FontProcessor::FontFile object containing the metatdata
|
143
|
+
# to upload.
|
144
|
+
#
|
145
|
+
# Returns nothing.
|
146
|
+
def self.upload_metadata(fpv, font_base_id, metadata)
|
147
|
+
client.set_font_processing_version_attributes(font_base_id, fpv, {
|
148
|
+
:glyphs => metadata.unicode,
|
149
|
+
:metrics => metadata.metrics,
|
150
|
+
:names => metadata.names,
|
151
|
+
:glyph_count => metadata.glyph_count,
|
152
|
+
:optical_size => metadata.optical_size,
|
153
|
+
:features => metadata.features
|
154
|
+
})
|
155
|
+
end
|
156
|
+
|
157
|
+
# This method fundamentally shouldn't exist, however until we convert our
|
158
|
+
# processing pipeline to deal with both PostScript and TrueType files we
|
159
|
+
# can't do anything about it.
|
160
|
+
#
|
161
|
+
# If a file with PostScript outlines exists, use that one. If it does not
|
162
|
+
# exist, use the TrueType one. If neither exist raise an exception.
|
163
|
+
#
|
164
|
+
# Returns the filename to use as the source for our processing pipeline.
|
165
|
+
# Raises MissingFilesException if neither a TrueType or PostScript font are
|
166
|
+
# found.
|
167
|
+
def self.select_preferred_original(directory)
|
168
|
+
files = Dir.glob(File.join(directory, "*")).map { |f| File.basename(f) }
|
169
|
+
return "original.otf" if files.include?("original.otf")
|
170
|
+
return "original.ttf" if files.include?("original.ttf")
|
171
|
+
raise MissingFilesException, "Neither a TrueType or PostScript original font was found in #{directory.inspect}:#{files.inspect}"
|
172
|
+
end
|
173
|
+
|
174
|
+
# Iterates over the processed files and uploads them to S3
|
175
|
+
#
|
176
|
+
# fpv - A String identifier for the FontProcessingVersion
|
177
|
+
# font_base_id - The FontBase unique id for the font to be processed.
|
178
|
+
# charset_id - The integer representing the character set contained within
|
179
|
+
# this font. 1 is all, 2 is default and 3 is upper-and-lower.
|
180
|
+
# formats - A JSON blob containing the formats to process
|
181
|
+
# including process_orignal, convert and a derivatives array.
|
182
|
+
# directory - Local directory of files to process.
|
183
|
+
#
|
184
|
+
# Returns nothing.
|
185
|
+
# Raises Aws::S3::Errors::ServiceError if there is an error transferring a file to
|
186
|
+
# Amazon.
|
187
|
+
# Raises FontProcessor::MissingFilesException if an expected file failed to
|
188
|
+
# be created.
|
189
|
+
def self.upload_files(fpv, font_base_id, charset_id, formats, directory)
|
190
|
+
iterator = FontProcessor::ProcessedFontIterator.new(font_base_id, charset_id, fpv, directory, formats['convert'])
|
191
|
+
iterator.each do |file, s3_key|
|
192
|
+
Config.s3_client.put_object(key: s3_key,
|
193
|
+
body: File.binread(file),
|
194
|
+
bucket: Config.s3_processed_bucket)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns the current redis connection
|
199
|
+
def self.redis
|
200
|
+
Resque.redis.instance_variable_get("@redis")
|
201
|
+
end
|
202
|
+
|
203
|
+
# Updates the given redis key with the success state
|
204
|
+
#
|
205
|
+
# key - The redis key to set
|
206
|
+
#
|
207
|
+
# Returns nothing.
|
208
|
+
def self.report_success(key)
|
209
|
+
redis.set(key, "success")
|
210
|
+
redis.expire(key, 24*60*60)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Updates the given redis key with the failure state
|
214
|
+
#
|
215
|
+
# key - The redis key to set
|
216
|
+
#
|
217
|
+
# Returns nothing.
|
218
|
+
def self.report_failure(key)
|
219
|
+
redis.set(key, "error")
|
220
|
+
redis.expire(key, 30)
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.log(font_base_id, charset_id, msg)
|
224
|
+
Config.logger.puts "[#{font_base_id}:#{charset_id}:#{DateTime.now.strftime("%H:%M:%S.%L")}] #{msg}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|