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,3 @@
1
+ module FontProcessor
2
+ VERSION = "27.1.3"
3
+ end
@@ -0,0 +1,16 @@
1
+ require 'digest'
2
+ require 'ots-ruby'
3
+ require 'skytype'
4
+ require 'tmpdir'
5
+
6
+ require 'fontprocessor/config'
7
+ require 'fontprocessor/external_execution'
8
+ require 'fontprocessor/font_file'
9
+ require 'fontprocessor/font_file_naming_strategy'
10
+ require 'fontprocessor/font_format'
11
+ require 'fontprocessor/process_font_job'
12
+ require 'fontprocessor/processed_font_iterator'
13
+ require 'fontprocessor/processor'
14
+
15
+ module FontProcessor
16
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eax
4
+
5
+ bundle install --deployment
6
+
7
+ bundle exec rspec
8
+
9
+ rm -rf `bundle show skytype`/ext/skytype \
10
+ `bundle show skytype`/ext/obj \
11
+ `bundle show skytype`/testData \
12
+ spec \
13
+ fixtures
14
+
15
+
@@ -0,0 +1,53 @@
1
+ # This is a script that can be run on a production `typekit` server to create
2
+ # a CSV file mapping font base IDs and processing versions to locations in the
3
+ # production source files bucket. This can be used to download those source
4
+ # files for local development and testing with an in-progress font processing
5
+ # version.
6
+ #
7
+ # The input to this script should be a CSV with family names and slugs. The
8
+ # output is another CSV file with individual font variations in each of those
9
+ # families, along with FVD and source file location in the S3 bucket.
10
+ #
11
+ # TO BE RUN IN A PRODUCTION TYPEKIT CONSOLE
12
+
13
+ data = File.read("/tmp/source_families.csv")
14
+ data = data.split("\n")[1..-1].join("\n") # Remove first line
15
+ family_data = FasterCSV.parse(data, {
16
+ :headers => ["Name", "Slug", "Notes"],
17
+ :return_headers => false,
18
+ :skip_blanks => true
19
+ })
20
+
21
+ client = FontBase::Client.new do |c|
22
+ c.connection = Mongo::Connection.from_uri(Typekit.config.font_base_db_uri)
23
+ c.db_name = Typekit.config.font_base_db
24
+ c.s3_bucket = Typekit.config.s3_font_base_source
25
+ end
26
+
27
+ output_data = FasterCSV.generate do |csv|
28
+ csv << ["Family Name", "Variation Name", "Slug", "FVD", "Font Base ID", "CFF Filename", "CFF Source ID", "TTF Filename", "TTF Source ID"]
29
+ family_data.each do |family_row|
30
+ tff = TypekitFontFamily.get_by_slug(family_row["Slug"])
31
+ tff.font_variations.each do |fv|
32
+ if ff = fv.current_font_file
33
+ font_base_id = ff.font_base_id
34
+ files = client.get(font_base_id)["files"]
35
+ cff_filename, cff_source_id = if files.has_key?("cff")
36
+ [files["cff"]["filename"], files["cff"]["key"]]
37
+ else
38
+ [nil, nil]
39
+ end
40
+ ttf_filename, ttf_source_id = if files.has_key?("ttf")
41
+ [files["ttf"]["filename"], files["ttf"]["key"]]
42
+ else
43
+ [nil, nil]
44
+ end
45
+ csv << [tff.name, fv.name, family_row["Slug"], fv.fvd, font_base_id, cff_filename, cff_source_id, ttf_filename, ttf_source_id]
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ File.open("/tmp/source_files.csv", "w") do |f|
52
+ f.write(output_data)
53
+ end
@@ -0,0 +1,35 @@
1
+ require 'tmpdir'
2
+
3
+ module FontProcessor
4
+ module Specs
5
+ class Fixture
6
+
7
+ def initialize(kind)
8
+ @kind = kind
9
+ FileUtils.mkdir_p(temp_directory)
10
+ FileUtils.cp(filepath, temp_directory)
11
+ end
12
+
13
+ def destroy
14
+ FileUtils.rm_rf(temp_directory)
15
+ end
16
+
17
+ def naming_strategy
18
+ FontProcessor::FontFileNamingStrategy.new(filename, temp_directory)
19
+ end
20
+
21
+ def temp_directory
22
+ File.join(Dir.tmpdir, @kind.to_s)
23
+ end
24
+
25
+ protected
26
+ def filename
27
+ "#{@kind}.otf"
28
+ end
29
+
30
+ def filepath
31
+ File.join(File.dirname(__FILE__), filename)
32
+ end
33
+ end
34
+ end
35
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe FontProcessor::Config do
4
+ subject { FontProcessor::Config }
5
+
6
+ describe "the environment" do
7
+ it "defaults to development" do
8
+ expect(subject.environment).to eq "development"
9
+ end
10
+ context "when set to test" do
11
+ before(:all) do
12
+ ENV['ENV'] = "test"
13
+ end
14
+ after(:all) do
15
+ ENV.delete('ENV')
16
+ end
17
+ it "is test" do
18
+ expect(subject.environment).to eq "test"
19
+ end
20
+ end
21
+ end
22
+
23
+ it "has an s3 source bucket" do
24
+ expect(subject.s3_source_bucket).to eq "typekit-development-source-fonts"
25
+ end
26
+
27
+ it "has an s3 processed bucket" do
28
+ expect(subject.s3_processed_bucket).to eq "typekit-development-processed-fonts"
29
+ end
30
+
31
+ it "has a mongo host" do
32
+ expect(subject.mongo_host).to eq "localhost"
33
+ end
34
+
35
+ it "has a mongo db" do
36
+ expect(subject.mongo_db).to eq "fontbase_development"
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe FontProcessor::ExternalStdOutCommand do
4
+ it "should be able to execute a normal binary and get it's output" do
5
+ result = FontProcessor::ExternalStdOutCommand.run('echo asdf', :expect_output => true)
6
+ expect(result).to eq "asdf\n"
7
+ end
8
+
9
+ it "should be able to kill a hanging binary" do
10
+ result = FontProcessor::ExternalStdOutCommand.run('cat', :timeout => 1)
11
+ expect(result).to be_nil
12
+ end
13
+
14
+ it "should be able to execute a normal binary and get it's stderr with it's stdout" do
15
+ result = FontProcessor::ExternalStdOutCommand.run('cat asdfjhasdjfkhasdkjfhasjdkhf', :expect_output => true)
16
+ expect(result).to include("No such file or directory")
17
+ end
18
+ end
19
+
20
+ describe FontProcessor::ExternalJSONProgram do
21
+ it "should be able to execute a program that takes and receives JSON" do
22
+ result = FontProcessor::ExternalJSONProgram.run('more', {:param1 => 'test'})
23
+ expect(result.key?('param1')).to be true
24
+ end
25
+ it "should be able to kill a hanging binary" do
26
+ expect { FontProcessor::ExternalJSONProgram.run('sleep 2', :timeout => 1) }.to raise_error(FontProcessor::ExternalProgramError, "sleep 2: Took longer than 1 seconds to respond")
27
+ end
28
+ it "should be able to handle a binary that outputs to stderr first" do
29
+ FontProcessor::ExternalJSONProgram.run('python -c "import sys;sys.stdin.read();sys.stderr.write(\'e\ne\');sys.stdout.write(\'{\\"1\\": [\'+\'1,\'*((65535/2)+1)+\'2]}\n\')"', {})
30
+ # If this didn't hang then we are good, otherwise we have a problem
31
+ expect(1).to eq 1
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe FontProcessor::FontFileNamingStrategy do
4
+ before(:all) do
5
+ @naming_strategy = FontProcessor::FontFileNamingStrategy.new("postscript.otf", "fixtures")
6
+ @charset_id = "1"
7
+ end
8
+
9
+ it "can name a character set" do
10
+ expect(@naming_strategy.char_set(@charset_id, FontProcessor::FontFormat.new(:ttf, :otf))).to eq "fixtures/charset-#{@charset_id}-otf.ttf"
11
+ expect(@naming_strategy.char_set(@charset_id, FontProcessor::FontFormat.new(:cff, :otf))).to eq "fixtures/charset-#{@charset_id}-otf.otf"
12
+ end
13
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+ require 'fixtures/fixtures'
3
+
4
+ describe FontProcessor::FontFile do
5
+ context "with a PostScript file" do
6
+ before(:each) do
7
+ @fixture = FontProcessor::Specs::Fixture.new(:postscript)
8
+ @file = FontProcessor::FontFile.new(@fixture.naming_strategy.source)
9
+ end
10
+
11
+ after(:each) do
12
+ @fixture.destroy
13
+ end
14
+
15
+ it "can inspect the name table" do
16
+ expect(@file.names.keys.count).to eq 1
17
+ expect(@file.names["en"].keys.sort).to eq ["Unique ID", "Version", "Trademark",
18
+ "PostScript name", "Subfamily", "Description", "Designer", "Designer URL",
19
+ "License Description", "License URL", "Vendor URL", "Manufacturer",
20
+ "Full name", "Family", "Copyright", "Compatible Full", "PostScript CID",
21
+ "Preferred Family", "Preferred Subfamily", "Reserved",
22
+ "Sample text", "WWS Family Name", "WWS Subfamily Name"].sort
23
+ expect(@file.names["en"]["PostScript name"]).to eq "Lobster1.4"
24
+ end
25
+
26
+ it "can inspect it's metrics" do
27
+ metrics = @file.metrics
28
+ expect(metrics.keys.sort).to eq ["hhea_linegap", "os2_weight", "upos",
29
+ "max_descent", "os2_width", "os2_panose", "hhea_descent", "max_ascent",
30
+ "optical_size", "os2_windescent", "os2_typolinegap", "os2_typoascent",
31
+ "os2_typodescent", "os2_winascent", "em", "hhea_ascent"].sort
32
+
33
+ expect(metrics['em']).to eq 1000
34
+ expect(metrics['max_ascent']).to eq 1000
35
+ expect(metrics['max_descent']).to eq -250
36
+ expect(metrics['upos']).to eq -50
37
+ expect(metrics['hhea_descent']).to eq -250
38
+ expect(metrics['hhea_ascent']).to eq 1000
39
+ expect(metrics['hhea_linegap']).to eq 28
40
+ expect(metrics['os2_windescent']).to eq 250
41
+ expect(metrics['os2_winascent']).to eq 1000
42
+ expect(metrics['os2_typoascent']).to eq 700
43
+ expect(metrics['os2_typodescent']).to eq -250
44
+ expect(metrics['os2_typolinegap']).to eq 56
45
+ expect(metrics['os2_weight']).to eq 400
46
+ expect(metrics['os2_width']).to eq 5
47
+ # the test font does not have an optical size, so it should be nil
48
+ expect(metrics['optical_size']).to be_nil
49
+ end
50
+
51
+ it "can get the optical size from a fon with it" do
52
+ opt_size_fixture = FontProcessor::Specs::Fixture.new(:op_size)
53
+ opt_file = FontProcessor::FontFile.new(opt_size_fixture.naming_strategy.source)
54
+ metrics = opt_file.metrics
55
+ expect(metrics.keys.sort).to eq ["hhea_linegap", "os2_weight", "upos",
56
+ "max_descent", "os2_width", "os2_panose", "hhea_descent", "max_ascent",
57
+ "os2_windescent", "os2_typolinegap", "os2_typoascent", "optical_size",
58
+ "os2_typodescent", "os2_winascent", "em", "hhea_ascent"].sort
59
+ expect(metrics['optical_size']).to eq 180
60
+ end
61
+
62
+ it "can identify the Unicode codepoints contained within a font" do
63
+ characters = [(32..126), (161..174), (176..255), (260..265)]
64
+ characters = characters.map { |r| r.to_a }.flatten
65
+ characters.each do |c|
66
+ expect(@file.unicode.include?(c)).to be true
67
+ end
68
+ end
69
+
70
+ it "can extract OpenType features" do
71
+ feature_hash = JSON.parse @file.features
72
+ expect(feature_hash.has_key?("GPOS")).to be true
73
+ expect(feature_hash.has_key?("GSUB")).to be true
74
+ gpos_features = feature_hash["GPOS"]
75
+ expect(gpos_features).to eq ["kern"]
76
+ gsub_features = feature_hash["GSUB"]
77
+ expect(gsub_features).to eq ["aalt", "liga", "salt"]
78
+ end
79
+
80
+
81
+ it "contains PostScript outlines" do
82
+ expect(@file.has_postscript_outlines?).to be true
83
+ expect(@file.has_truetype_outlines?).to be false
84
+ end
85
+
86
+ it "has a helper function to compact arrays of consecutive integers into ranges" do
87
+ expect(@file.send(:compact_ranges, [1,2,3,7,8,9])).to eq [Range.new(1,3), Range.new(7,9)]
88
+ expect(@file.send(:compact_ranges, [1,3,7,2,8,9])).to eq [Range.new(1,3), Range.new(7,9)]
89
+ expect(@file.send(:compact_ranges, [1,3,7,2,8,9,11])).to eq [Range.new(1,3), Range.new(7,9), 11]
90
+ expect(@file.send(:compact_ranges, [1,2,3,5,7,8,9,11,13])).to eq [Range.new(1,3), 5, Range.new(7,9), 11, 13]
91
+ end
92
+ end
93
+
94
+ context "with a file containing TrueType outlines" do
95
+ before(:each) do
96
+ @fixture = FontProcessor::Specs::Fixture.new(:truetype)
97
+ naming_strategy = @fixture.naming_strategy
98
+ @file = FontProcessor::FontFile.new(naming_strategy.source)
99
+ end
100
+
101
+ after(:each) do
102
+ @fixture.destroy
103
+ end
104
+
105
+ it "contains TrueType outlines" do
106
+ expect(@file.has_truetype_outlines?).to be true
107
+ expect(@file.has_postscript_outlines?).to be false
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,317 @@
1
+ require 'spec_helper'
2
+
3
+ describe FontProcessor::ProcessFontJob do
4
+ before(:each) do
5
+ @status_key = "status:/a00000000000000000000001/20/1/woffsvgswf"
6
+ @font_base_id = "a00000000000000000000001"
7
+ @request_data = '{"version":"0.3","request_type":"web_processing_request","font_base_id":"a00000000000000000000001","fpv":"21","charset":{"charset_id":"1","unicode":"0x0020..0x007e,0x00a0..0xffff","features":"ALL"},"formats":{"process_original":true,"convert":true,"derivatives":["dyna_base","woff","woff2","inst","svg","swf"]}}'
8
+ @no_convert = '{"version":"0.3","request_type":"web_processing_request","font_base_id":"a00000000000000000000001","fpv":"21","charset":{"charset_id":"1","unicode":"0x0020..0x007e,0x00a0..0xffff","features":"ALL"},"formats":{"process_original":true,"convert":false,"derivatives":["dyna_base","woff","woff2","inst","svg","swf"]}}'
9
+ @charset_id = "1"
10
+ @fpv = "21"
11
+ @directory = "/tmp/#{@font_base_id}-#{@charset_id}"
12
+ end
13
+ subject { FontProcessor::ProcessFontJob }
14
+
15
+ describe "integration tests" do
16
+ it "can successfully process a font" do
17
+ expect(subject).to receive(:fetch_files).ordered
18
+ expect(subject).to receive(:process_files).ordered
19
+ expect(subject).to receive(:upload_metadata).ordered
20
+ expect(subject).to receive(:upload_files).ordered
21
+ redis = double("Redis")
22
+ expect(redis).to receive(:set).with(@status_key, "success")
23
+ expect(redis).to receive(:expire).with(@status_key, 86400)
24
+ expect(subject).to receive(:redis).ordered.twice.and_return(redis)
25
+
26
+ subject.perform(@status_key, @request_data)
27
+ end
28
+
29
+ it "can successfully process a font without conversion" do
30
+ expect(subject).to receive(:fetch_files).ordered
31
+ expect(subject).to receive(:process_files).ordered
32
+ expect(subject).to receive(:upload_metadata).ordered
33
+ expect(subject).to receive(:upload_files).ordered
34
+ redis = double("Redis")
35
+ expect(redis).to receive(:set).with(@status_key, "success")
36
+ expect(redis).to receive(:expire).with(@status_key, 86400)
37
+ expect(subject).to receive(:redis).ordered.twice.and_return(redis)
38
+
39
+ subject.perform(@status_key, @no_convert)
40
+ end
41
+
42
+ it "can handle an error while processing the font" do
43
+ expect(subject).to receive(:fetch_files).ordered
44
+ expect(subject).to receive(:process_files).ordered.and_raise(RuntimeError)
45
+ redis = double("Redis")
46
+ expect(redis).to receive(:set).with(@status_key, "error")
47
+ expect(redis).to receive(:expire).with(@status_key, 30)
48
+ expect(subject).to receive(:redis).ordered.twice.and_return(redis)
49
+
50
+ expect { subject.perform(@status_key, @request_data) }.to raise_exception(Exception)
51
+ end
52
+
53
+ it "can handle an error while retrieving the font data" do
54
+ expect(subject).to receive(:fetch_files).and_raise(FontBase::Error)
55
+ redis = double("Redis")
56
+ expect(redis).to receive(:set).with(@status_key, "error")
57
+ expect(redis).to receive(:expire).with(@status_key, 30)
58
+ expect(subject).to receive(:redis).twice.and_return(redis)
59
+
60
+ expect { subject.perform(@status_key, @request_data) }.to raise_exception(FontBase::Error)
61
+ end
62
+
63
+ it "can handle an error while uploading the processed font metadata" do
64
+ expect(subject).to receive(:fetch_files).ordered
65
+ expect(subject).to receive(:process_files).ordered
66
+ expect(subject).to receive(:upload_metadata).and_raise(FontBase::Error)
67
+ redis = double("Redis")
68
+ expect(redis).to receive(:set).with(@status_key, "error")
69
+ expect(redis).to receive(:expire).with(@status_key, 30)
70
+ expect(subject).to receive(:redis).ordered.twice.and_return(redis)
71
+
72
+ expect { subject.perform(@status_key, @request_data) }.to raise_exception(FontBase::Error)
73
+ end
74
+
75
+ it "can handle an error while uploading processed font data" do
76
+ expect(subject).to receive(:fetch_files).ordered
77
+ expect(subject).to receive(:process_files).ordered
78
+ expect(subject).to receive(:upload_metadata).ordered
79
+ expect(subject).to receive(:upload_files).ordered.and_raise(Aws::S3::Errors::ServiceError.new('message', 'service error'))
80
+ redis = double("Redis")
81
+ expect(redis).to receive(:set).with(@status_key, "error")
82
+ expect(redis).to receive(:expire).with(@status_key, 30)
83
+ expect(subject).to receive(:redis).ordered.twice.and_return(redis)
84
+
85
+ expect { subject.perform(@status_key, @request_data) }.to raise_exception(Aws::S3::Errors::ServiceError)
86
+ end
87
+
88
+ it "can handle an error for an invalid request type" do
89
+ redis = double("Redis")
90
+ expect(redis).to receive(:set).with(@status_key, "error")
91
+ expect(redis).to receive(:expire).with(@status_key, 30)
92
+ expect(subject).to receive(:redis).twice.and_return(redis)
93
+ bad_req_type = JSON.parse(@request_data)
94
+ bad_req_type['request_type'] = "desktop_processing_request"
95
+ expect { subject.perform(@status_key, bad_req_type) }.to raise_exception(RuntimeError)
96
+ end
97
+
98
+ it "can handle an error for a request without charset data" do
99
+ redis = double("Redis")
100
+ expect(redis).to receive(:set).with(@status_key, "error")
101
+ expect(redis).to receive(:expire).with(@status_key, 30)
102
+ expect(subject).to receive(:redis).twice.and_return(redis)
103
+ bad_req = JSON.parse(@request_data)
104
+ bad_req.delete('charset')
105
+ expect { subject.perform(@status_key, bad_req) }.to raise_exception(RuntimeError, /No charset in JSON Request/)
106
+ end
107
+
108
+ it "can handle an error for a request without charset_id in the charset data" do
109
+ redis = double("Redis")
110
+ expect(redis).to receive(:set).with(@status_key, "error")
111
+ expect(redis).to receive(:expire).with(@status_key, 30)
112
+ expect(subject).to receive(:redis).twice.and_return(redis)
113
+ bad_req = JSON.parse(@request_data)
114
+ bad_req['charset'].delete('charset_id')
115
+ expect { subject.perform(@status_key, bad_req) }.to raise_exception(RuntimeError, /No charset_id set in JSON Request's charset/)
116
+ end
117
+
118
+ it "can handle an error for a request without unicode in the charset data" do
119
+ redis = double("Redis")
120
+ expect(redis).to receive(:set).with(@status_key, "error")
121
+ expect(redis).to receive(:expire).with(@status_key, 30)
122
+ expect(subject).to receive(:redis).twice.and_return(redis)
123
+ bad_req = JSON.parse(@request_data)
124
+ bad_req['charset'].delete('unicode')
125
+ expect { subject.perform(@status_key, bad_req) }.to raise_exception(RuntimeError, /No unicode set in JSON Request's charset/)
126
+ end
127
+
128
+ it "can handle an error for a request without features in the charset data" do
129
+ redis = double("Redis")
130
+ expect(redis).to receive(:set).with(@status_key, "error")
131
+ expect(redis).to receive(:expire).with(@status_key, 30)
132
+ expect(subject).to receive(:redis).twice.and_return(redis)
133
+ bad_req = JSON.parse(@request_data)
134
+ bad_req['charset'].delete('features')
135
+ expect { subject.perform(@status_key, bad_req) }.to raise_exception(RuntimeError, /No features set in JSON Request's charset/)
136
+ end
137
+
138
+ it "can handle an error for a request without an fpv" do
139
+ redis = double("Redis")
140
+ expect(redis).to receive(:set).with(@status_key, "error")
141
+ expect(redis).to receive(:expire).with(@status_key, 30)
142
+ expect(subject).to receive(:redis).twice.and_return(redis)
143
+ bad_req = JSON.parse(@request_data)
144
+ bad_req.delete('fpv')
145
+ expect { subject.perform(@status_key, bad_req) }.to raise_exception(RuntimeError, /No fpv set on JSON Request/)
146
+ end
147
+ end
148
+
149
+ describe "fetching files" do
150
+ before(:each) do
151
+ FileUtils.mkdir_p(@directory)
152
+ end
153
+ after(:each) do
154
+ FileUtils.rm_r(@directory)
155
+ end
156
+ it "successfully handles a single PostScript file" do
157
+ client = double("FontBase::Client")
158
+ expect(client).to receive(:get_files).with(@font_base_id).and_return({"cff" => "cff-data"})
159
+ expect(subject).to receive(:client).and_return(client)
160
+ subject.fetch_files(@font_base_id, @directory)
161
+
162
+ expect(File.read(File.join(@directory, "original.otf"))).to eq "cff-data"
163
+ end
164
+
165
+ it "successfully handles a single TrueType file" do
166
+ client = double("FontBase::Client")
167
+ expect(client).to receive(:get_files).with(@font_base_id).and_return({"ttf" => "ttf-data"})
168
+ expect(subject).to receive(:client).and_return(client)
169
+ subject.fetch_files(@font_base_id, @directory)
170
+
171
+ expect(File.read(File.join(@directory, "original.ttf"))).to eq "ttf-data"
172
+ end
173
+
174
+ it "successfully handles a PostScript and TrueType file" do
175
+ client = double("FontBase::Client")
176
+ expect(client).to receive(:get_files).with(@font_base_id).and_return({"ttf" => "ttf-data", "cff" => "cff-data"})
177
+ expect(subject).to receive(:client).and_return(client)
178
+ subject.fetch_files(@font_base_id, @directory)
179
+
180
+ expect(File.read(File.join(@directory, "original.ttf"))).to eq "ttf-data"
181
+ expect(File.read(File.join(@directory, "original.otf"))).to eq "cff-data"
182
+ end
183
+ end
184
+
185
+ describe "processing files" do
186
+ before(:each) do
187
+ FileUtils.mkdir_p(@directory)
188
+ end
189
+ after(:each) do
190
+ FileUtils.rm_r(@directory)
191
+ end
192
+
193
+ it "successfully handles a PostScript file" do
194
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "postscript.otf"),
195
+ File.join(@directory, "original.otf"))
196
+
197
+ parsed_data = JSON.parse(@request_data)
198
+ subject.process_files(@font_base_id, parsed_data['charset'], parsed_data['formats'], @directory)
199
+ iterator = FontProcessor::ProcessedFontIterator.new(@font_base_id, @charset_id, @fpv, @directory, true)
200
+ expect(iterator.valid?).to be true
201
+ expect(File.exist?(File.join(@directory, "charset-#{@charset_id}-otf.otf"))).to be true
202
+ end
203
+
204
+ it "successfully processes a TrueType file" do
205
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "truetype.otf"),
206
+ File.join(@directory, "original.ttf"))
207
+
208
+ parsed_data = JSON.parse(@request_data)
209
+ subject.process_files(@font_base_id, parsed_data['charset'], parsed_data['formats'], @directory)
210
+ iterator = FontProcessor::ProcessedFontIterator.new(@font_base_id, @charset_id, @fpv, @directory, true)
211
+ expect(iterator.valid?).to be true
212
+ expect(File.exist?(File.join(@directory, "charset-#{@charset_id}-otf.ttf"))).to be true
213
+ end
214
+
215
+ it "successfully processes a PostScript file if given both" do
216
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "postscript.otf"),
217
+ File.join(@directory, "original.otf"))
218
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "truetype.otf"),
219
+ File.join(@directory, "original.ttf"))
220
+ parsed_data = JSON.parse(@request_data)
221
+ subject.process_files(@font_base_id, parsed_data['charset'], parsed_data['formats'], @directory)
222
+ iterator = FontProcessor::ProcessedFontIterator.new(@font_base_id, @charset_id, @fpv, @directory, true)
223
+ expect(iterator.valid?).to be true
224
+ expect(File.exist?(File.join(@directory, "charset-#{@charset_id}-otf.otf"))).to be true
225
+ end
226
+
227
+ it "successfully handles a PostScript file" do
228
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "postscript.otf"),
229
+ File.join(@directory, "original.otf"))
230
+
231
+ parsed_data = JSON.parse(@request_data)
232
+ parsed_data['formats']['convert'] = false
233
+ subject.process_files(@font_base_id, parsed_data['charset'], parsed_data['formats'], @directory)
234
+ iterator = FontProcessor::ProcessedFontIterator.new(@font_base_id, @charset_id, @fpv, @directory, false)
235
+ expect(iterator.valid?).to be true
236
+ expect(File.exist?(File.join(@directory, "charset-#{@charset_id}-otf.otf"))).to be true
237
+ end
238
+
239
+ it "successfully subsets a Truetype file" do
240
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "truetype.otf"),
241
+ File.join(@directory, "original.ttf"))
242
+
243
+ charset_data = {
244
+ "charset_id" => "3",
245
+ "features" => "NONE",
246
+ "unicode" => "32..32,65..90,97..122"
247
+ }
248
+ format_data = {
249
+ "process_original" => true,
250
+ "convert" => true,
251
+ "derivatives" => [ "dyna_base", "woff", "woff2", "inst", "svg", "swf" ]
252
+ }
253
+ subject.process_files(@font_base_id, charset_data, format_data, @directory, false)
254
+ iterator = FontProcessor::ProcessedFontIterator.new(@font_base_id, charset_data['charset_id'], @fpv, @directory, true)
255
+ expect(iterator.valid?).to be true
256
+ file = File.join(@directory, "charset-3-otf.ttf")
257
+ # process_files calls into generate_char_set, then into processor.generate_internal_char_set
258
+ # This method now automatically adds nb-space and soft-hyphen, so we need to expect
259
+ # U+A0 (160)
260
+ expect(FontProcessor::FontFile.new(file).unicode_ranges).to eq [32, 65..90, 97..122, 160]
261
+ end
262
+
263
+ it "throws MissingFilesException when neither are found" do
264
+ FileUtils.cp(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "postscript.otf"), @directory)
265
+
266
+ expect { subject.select_preferred_original(@directory)}.to raise_error(FontProcessor::MissingFilesException)
267
+ end
268
+ end
269
+ describe "uploading metadata" do
270
+ it "successfully updates the metadata in fontbase" do
271
+ fake_font_file = double("FontFile", :unicode => "unicode data", :metrics => "metrics data", :names => "names data", :glyph_count => "1", :optical_size => "180", :features => "kern,size")
272
+ expected_metadata = {
273
+ :glyphs => "unicode data",
274
+ :metrics => "metrics data",
275
+ :names => "names data",
276
+ :glyph_count => "1",
277
+ :optical_size => "180",
278
+ :features => "kern,size"
279
+ }
280
+
281
+ client = double("FontBase::Client")
282
+ expect(client).to receive(:set_font_processing_version_attributes).with(@font_base_id, @fpv, expected_metadata).and_return({"ttf" => "ttf-data"})
283
+ expect(subject).to receive(:client).and_return(client)
284
+
285
+ subject.upload_metadata(@fpv, @font_base_id, fake_font_file)
286
+ end
287
+ it "successfully updates the metadata in fontbase with no optical size" do
288
+ fake_font_file = double("FontFile", :unicode => "unicode data", :metrics => "metrics data", :names => "names data", :glyph_count => "1", :optical_size => nil, :features => "kern")
289
+ expected_metadata = {
290
+ :glyphs => "unicode data",
291
+ :metrics => "metrics data",
292
+ :names => "names data",
293
+ :glyph_count => "1",
294
+ :optical_size => nil,
295
+ :features => "kern"
296
+ }
297
+
298
+ client = double("FontBase::Client")
299
+ expect(client).to receive(:set_font_processing_version_attributes).with(@font_base_id, @fpv, expected_metadata).and_return({"ttf" => "ttf-data"})
300
+ expect(subject).to receive(:client).and_return(client)
301
+
302
+ subject.upload_metadata(@fpv, @font_base_id, fake_font_file)
303
+ end
304
+ end
305
+ describe "uploading files" do
306
+ it "attempts to upload all valid files" do
307
+ iterator = [["file1", "key"]]
308
+ expect(FontProcessor::ProcessedFontIterator).to receive(:new).and_return(iterator)
309
+ expect(File).to receive(:binread).with("file1").and_return("")
310
+ expect_any_instance_of(Aws::S3::Client).to receive(:put_object).with(key: "key",
311
+ body: "",
312
+ bucket: FontProcessor::Config.s3_processed_bucket)
313
+ parsed_data = JSON.parse(@request_data)
314
+ subject.upload_files(parsed_data['fpv'], parsed_data['font_base_id'], parsed_data['charset']['charset_id'], parsed_data['formats'], @directory)
315
+ end
316
+ end
317
+ end