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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rbenv-version +1 -0
  4. data/.rspec +0 -0
  5. data/.travis.yml +17 -0
  6. data/CHANGELOG.md +64 -0
  7. data/CHANGELOG_FPV14.md +20 -0
  8. data/Dockerfile +3 -0
  9. data/Gemfile +14 -0
  10. data/Gemfile.lock +156 -0
  11. data/Guardfile +5 -0
  12. data/README.md +6 -0
  13. data/Rakefile +53 -0
  14. data/Rakefile.base +57 -0
  15. data/bin/process-file +41 -0
  16. data/config/development.yml +5 -0
  17. data/config/production.yml +3 -0
  18. data/config/staging.yml +3 -0
  19. data/config/test.yml +5 -0
  20. data/fontprocessor.gemspec +37 -0
  21. data/lib/fontprocessor/config.rb +85 -0
  22. data/lib/fontprocessor/external/batik/batik-ttf2svg.jar +0 -0
  23. data/lib/fontprocessor/external/batik/lib/batik-svggen.jar +0 -0
  24. data/lib/fontprocessor/external/batik/lib/batik-util.jar +0 -0
  25. data/lib/fontprocessor/external/fontforge/subset.py +30 -0
  26. data/lib/fontprocessor/external/fontforge/utils.py +66 -0
  27. data/lib/fontprocessor/external_execution.rb +117 -0
  28. data/lib/fontprocessor/font_file.rb +149 -0
  29. data/lib/fontprocessor/font_file_naming_strategy.rb +63 -0
  30. data/lib/fontprocessor/font_format.rb +29 -0
  31. data/lib/fontprocessor/process_font_job.rb +227 -0
  32. data/lib/fontprocessor/processed_font_iterator.rb +89 -0
  33. data/lib/fontprocessor/processor.rb +790 -0
  34. data/lib/fontprocessor/version.rb +3 -0
  35. data/lib/fontprocessor.rb +16 -0
  36. data/scripts/build_and_test.sh +15 -0
  37. data/scripts/get_production_source_map.rb +53 -0
  38. data/spec/fixtures/bad_os2_width_class.otf +0 -0
  39. data/spec/fixtures/extra_language_names.otf +0 -0
  40. data/spec/fixtures/fixtures.rb +35 -0
  41. data/spec/fixtures/locked.otf +0 -0
  42. data/spec/fixtures/op_size.otf +0 -0
  43. data/spec/fixtures/ots_failure_font.otf +0 -0
  44. data/spec/fixtures/postscript.otf +0 -0
  45. data/spec/fixtures/stat_font.otf +0 -0
  46. data/spec/fixtures/truetype.otf +0 -0
  47. data/spec/lib/fontprocessor/config_spec.rb +38 -0
  48. data/spec/lib/fontprocessor/external_execution_spec.rb +33 -0
  49. data/spec/lib/fontprocessor/font_file_naming_strategy_spec.rb +13 -0
  50. data/spec/lib/fontprocessor/font_file_spec.rb +110 -0
  51. data/spec/lib/fontprocessor/process_font_job_spec.rb +317 -0
  52. data/spec/lib/fontprocessor/processed_font_iterator_spec.rb +128 -0
  53. data/spec/lib/fontprocessor/processor_spec.rb +466 -0
  54. data/spec/spec_helper.rb +4 -0
  55. data/tasks/fonts.rake +30 -0
  56. data/worker.rb +23 -0
  57. 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