image_vise 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.travis.yml +13 -0
  4. data/DEVELOPMENT.md +111 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +29 -0
  7. data/README.md +213 -0
  8. data/Rakefile +6 -0
  9. data/SECURITY.md +57 -0
  10. data/examples/config.ru +17 -0
  11. data/examples/custom_image_operator.rb +27 -0
  12. data/examples/error_handline_appsignal.rb +23 -0
  13. data/examples/error_handling_sentry.rb +25 -0
  14. data/image_vise.gemspec +43 -0
  15. data/lib/image_vise/fetchers/fetcher_file.rb +27 -0
  16. data/lib/image_vise/fetchers/fetcher_http.rb +42 -0
  17. data/lib/image_vise/file_response.rb +22 -0
  18. data/lib/image_vise/image_request.rb +70 -0
  19. data/lib/image_vise/operators/auto_orient.rb +10 -0
  20. data/lib/image_vise/operators/background_fill.rb +18 -0
  21. data/lib/image_vise/operators/crop.rb +32 -0
  22. data/lib/image_vise/operators/ellipse_stencil.rb +70 -0
  23. data/lib/image_vise/operators/fit_crop.rb +33 -0
  24. data/lib/image_vise/operators/force_jpg_out.rb +17 -0
  25. data/lib/image_vise/operators/geom.rb +16 -0
  26. data/lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc +0 -0
  27. data/lib/image_vise/operators/sharpen.rb +21 -0
  28. data/lib/image_vise/operators/srgb.rb +30 -0
  29. data/lib/image_vise/operators/strip_metadata.rb +10 -0
  30. data/lib/image_vise/pipeline.rb +64 -0
  31. data/lib/image_vise/render_engine.rb +298 -0
  32. data/lib/image_vise/version.rb +3 -0
  33. data/lib/image_vise/writers/auto_writer.rb +23 -0
  34. data/lib/image_vise/writers/jpeg_writer.rb +9 -0
  35. data/lib/image_vise.rb +177 -0
  36. data/spec/image_vise/auto_orient_spec.rb +10 -0
  37. data/spec/image_vise/background_fill_spec.rb +39 -0
  38. data/spec/image_vise/crop_spec.rb +20 -0
  39. data/spec/image_vise/ellipse_stencil_spec.rb +18 -0
  40. data/spec/image_vise/fetcher_file_spec.rb +48 -0
  41. data/spec/image_vise/fetcher_http_spec.rb +44 -0
  42. data/spec/image_vise/file_response_spec.rb +45 -0
  43. data/spec/image_vise/fit_crop_spec.rb +20 -0
  44. data/spec/image_vise/force_jpg_out_spec.rb +36 -0
  45. data/spec/image_vise/geom_spec.rb +33 -0
  46. data/spec/image_vise/image_request_spec.rb +62 -0
  47. data/spec/image_vise/pipeline_spec.rb +72 -0
  48. data/spec/image_vise/render_engine_spec.rb +336 -0
  49. data/spec/image_vise/sharpen_spec.rb +17 -0
  50. data/spec/image_vise/srgb_spec.rb +23 -0
  51. data/spec/image_vise/strip_metadata_spec.rb +14 -0
  52. data/spec/image_vise/writers/auto_writer_spec.rb +25 -0
  53. data/spec/image_vise/writers/jpeg_writer_spec.rb +32 -0
  54. data/spec/image_vise_spec.rb +110 -0
  55. data/spec/layers-with-blending.psd +0 -0
  56. data/spec/spec_helper.rb +112 -0
  57. data/spec/test_server.rb +61 -0
  58. data/spec/waterside_magic_hour.jpg +0 -0
  59. data/spec/waterside_magic_hour.psd +0 -0
  60. data/spec/waterside_magic_hour_adobergb.jpg +0 -0
  61. data/spec/waterside_magic_hour_gray.tif +0 -0
  62. data/spec/waterside_magic_hour_transp.png +0 -0
  63. metadata +63 -2
@@ -0,0 +1,44 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe ImageVise::FetcherHTTP do
4
+ it 'is a class (can be inherited from)' do
5
+ expect(ImageVise::FetcherHTTP).to be_kind_of(Class)
6
+ end
7
+
8
+ it 'is registered as a fetcher for http:// and https://' do
9
+ expect(ImageVise.fetcher_for('http')).to eq(ImageVise::FetcherHTTP)
10
+ expect(ImageVise.fetcher_for('https')).to eq(ImageVise::FetcherHTTP)
11
+ end
12
+
13
+ it 'raises an AccessError if the host of the URL is not on the whitelist' do
14
+ uri = URI('https://wrong-origin.com/image.psd')
15
+ expect {
16
+ ImageVise::FetcherHTTP.fetch_uri_to_tempfile(uri)
17
+ }.to raise_error(ImageVise::FetcherHTTP::AccessError, /is not permitted as source/)
18
+ end
19
+
20
+ it 'raises an UpstreamError if the upstream fetch returns an error-ish status code' do
21
+ uri = URI('http://localhost:9001/forbidden')
22
+ ImageVise.add_allowed_host! 'localhost'
23
+
24
+ expect {
25
+ ImageVise::FetcherHTTP.fetch_uri_to_tempfile(uri)
26
+ }.to raise_error {|e|
27
+ expect(e).to be_kind_of(ImageVise::FetcherHTTP::UpstreamError)
28
+ expect(e.message).to include(uri.to_s)
29
+ expect(e.message).to include('403')
30
+ expect(e.http_status).to eq(403)
31
+ }
32
+ end
33
+
34
+ it 'fetches the image into a Tempfile' do
35
+ uri = URI(public_url_psd)
36
+ ImageVise.add_allowed_host! 'localhost'
37
+
38
+ result = ImageVise::FetcherHTTP.fetch_uri_to_tempfile(uri)
39
+
40
+ expect(result).to be_kind_of(Tempfile)
41
+ expect(result.size).to be_nonzero
42
+ expect(result.pos).to be_zero
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe ImageVise::FileResponse do
4
+ it 'reads the file in binary mode, closes and unlinks the tempfile when close() is called' do
5
+ random_data = Random.new.bytes(1024 * 2048)
6
+ f = Tempfile.new("experiment")
7
+ f.binmode
8
+ f << random_data
9
+
10
+ response = described_class.new(f)
11
+ readback = ''.encode(Encoding::BINARY)
12
+ response.each do | chunk |
13
+ expect(chunk.encoding).to eq(Encoding::BINARY)
14
+ readback << chunk
15
+ end
16
+
17
+ response.close
18
+
19
+ expect(readback).to eq(random_data)
20
+ expect(f).to be_closed
21
+ expect(f.path).to be_nil
22
+ end
23
+
24
+ it 'only asks for the path of the tempfile and uses a separate file descriptor' do
25
+ f = Tempfile.new("experiment")
26
+ f.binmode
27
+ f << Random.new.bytes(2048)
28
+ f.flush
29
+
30
+ # Use a double so that all the methods except the ones we mock raise an assertion
31
+ double = double(path: f.path)
32
+ expect(double).to receive(:flush)
33
+
34
+ read_from_response = ''.encode(Encoding::BINARY)
35
+ response = described_class.new(double)
36
+ response.each{|b| read_from_response << b }
37
+
38
+ f.rewind
39
+
40
+ expect(f.read).to eq(read_from_response)
41
+
42
+ f.close
43
+ f.unlink
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageVise::FitCrop do
4
+ it 'refuses invalid arguments' do
5
+ expect { described_class.new(width: 0, height: -1, gravity: '') }.to raise_error(ArgumentError)
6
+ end
7
+
8
+ it 'applies the crop with different gravities' do
9
+ %w( s sw se n ne nw c).each do |gravity|
10
+ image = Magick::Image.read(test_image_path)[0]
11
+ crop = described_class.new(width: 120, height: 220, gravity: gravity)
12
+
13
+ crop.apply!(image)
14
+
15
+ expect(image.columns).to eq(120)
16
+ expect(image.rows).to eq(220)
17
+ examine_image(image, "gravity-%s-" % gravity)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe ImageVise::ForceJPGOut do
4
+ it "raises on invalid arguments" do
5
+ expect {
6
+ described_class.new({})
7
+ }.to raise_error(ArgumentError)
8
+
9
+ expect {
10
+ described_class.new(quality: '1')
11
+ }.to raise_error(ArgumentError)
12
+
13
+ expect {
14
+ described_class.new(quality: -1)
15
+ }.to raise_error(ArgumentError)
16
+
17
+ expect {
18
+ described_class.new(quality: 'very very low')
19
+ }.to raise_error(ArgumentError)
20
+
21
+ described_class.new(quality: 25)
22
+ end
23
+
24
+ it "sets the :writer metadata key to a JPGWriter" do
25
+ subject = described_class.new(quality: 25)
26
+
27
+ fake_magick_image = double('Magick::Image')
28
+ metadata = {}
29
+
30
+ subject.apply!(fake_magick_image, metadata)
31
+
32
+ w = metadata.fetch(:writer)
33
+ expect(w).to be_kind_of(ImageVise::JPGWriter)
34
+ expect(w.quality).to eq(25)
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageVise::Geom do
4
+ it 'refuses invalid parameters' do
5
+ expect { described_class.new(geometry_string: nil) }.to raise_error(ArgumentError)
6
+ end
7
+
8
+ it 'fits to height' do
9
+ image = Magick::Image.read(test_image_path)[0]
10
+ crop = described_class.new(geometry_string: 'x200')
11
+ crop.apply!(image)
12
+ expect(image.rows).to eq(200)
13
+ examine_image(image, 'fit-height-200')
14
+ end
15
+
16
+ it 'fits to width' do
17
+ image = Magick::Image.read(test_image_path)[0]
18
+ crop = described_class.new(geometry_string: '100x')
19
+ crop.apply!(image)
20
+ expect(image.columns).to eq(100)
21
+ examine_image(image, 'fit-width-100')
22
+ end
23
+
24
+ it 'applies various geometry strings' do
25
+ %w( ^220x110 !20x20 !10x100 ).each do |geom_string|
26
+ image = Magick::Image.read(test_image_path)[0]
27
+ crop = described_class.new(geometry_string: geom_string)
28
+
29
+ crop.apply!(image)
30
+ examine_image(image, 'geom-%s' % geom_string)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageVise::ImageRequest do
4
+ it 'accepts a set of params and secrets, and returns a Pipeline' do
5
+ img_params = {src_url: 'http://bucket.s3.aws.com/image.jpg', pipeline: [[:crop, {width: 10, height: 10, gravity: 's'}]]}
6
+ img_params_json = JSON.dump(img_params)
7
+
8
+ q = Base64.encode64(img_params_json)
9
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', q)
10
+ params = {q: q, sig: signature}
11
+
12
+ image_request = described_class.from_params(qs_params: params, secrets: ['this is a secret'])
13
+ request_qs_params = image_request.to_query_string_params('this is a secret')
14
+ expect(request_qs_params).to be_kind_of(Hash)
15
+
16
+ image_request_roundtrip = described_class.from_params(qs_params: request_qs_params, secrets: ['this is a secret'])
17
+ end
18
+
19
+ it 'converts a file:// URL into a URI objectlist' do
20
+ img_params = {src_url: 'file:///etc/passwd', pipeline: [[:auto_orient, {}]]}
21
+ img_params_json = JSON.dump(img_params)
22
+ q = Base64.encode64(img_params_json)
23
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', q)
24
+ params = {q: q, sig: signature}
25
+ image_request = described_class.from_params(qs_params: params, secrets: ['this is a secret'])
26
+ expect(image_request.src_url).to be_kind_of(URI)
27
+ end
28
+
29
+ it 'composes path parameters' do
30
+ parametrized = double(to_params: {foo: 'bar'})
31
+ uri = URI('http://example.com/image.psd')
32
+ image_request = described_class.new(src_url: uri, pipeline: parametrized)
33
+ path = image_request.to_path_params('password')
34
+ expect(path).to start_with('/eyJwaXB')
35
+ expect(path).to end_with('f207b')
36
+ end
37
+
38
+ it 'never apppends "="-padding to the Base64-encoded "q"' do
39
+ parametrized = double(to_params: {foo: 'bar'})
40
+ (1..12).each do |num_chars_in_url|
41
+ uri = URI('http://ex.com/%s' % ('i' * num_chars_in_url))
42
+ image_request = described_class.new(src_url: uri, pipeline: parametrized)
43
+ q = image_request.to_query_string_params('password').fetch(:q)
44
+ expect(q).not_to include('=')
45
+ end
46
+ end
47
+
48
+ describe 'fails with an invalid signature' do
49
+ it 'when the sig is invalid' do
50
+ img_params = {src_url: 'http://bucket.s3.aws.com/image.jpg',
51
+ pipeline: [[:crop, {width: 10, height: 10, gravity: 's'}]]}
52
+ img_params_json = JSON.dump(img_params)
53
+ enc = Base64.encode64(img_params_json)
54
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'a', enc)
55
+ params = {q: enc, sig: signature}
56
+
57
+ expect {
58
+ described_class.from_params(qs_params: params, secrets: ['b'])
59
+ }.to raise_error(/Invalid or missing signature/)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageVise::Pipeline do
4
+ it 'is empty by default' do
5
+ expect(subject).to be_empty
6
+ end
7
+
8
+ it 'reinstates the pipeline from the operator list parameters' do
9
+ params = [
10
+ ["geom", {:geometry_string=>"10x10"}],
11
+ ["crop", {:width=>5, :height=>5, :gravity=>"se"}],
12
+ ["auto_orient", {}],
13
+ ["fit_crop", {:width=>10, :height=>32, :gravity=>"c"}]
14
+ ]
15
+ pipeline = described_class.from_param(params)
16
+ expect(pipeline).not_to be_empty
17
+ end
18
+
19
+ it 'produces a usable operator parameter list that can be roundtripped' do
20
+ operator_list = subject.geom(geometry_string: '10x10').
21
+ crop(width: 5, height: 5, gravity: 'se').
22
+ auto_orient.
23
+ fit_crop(width: 10, height: 32, gravity: 'c').to_params
24
+
25
+ expect(operator_list).to eq([
26
+ ["geom", {:geometry_string=>"10x10"}],
27
+ ["crop", {:width=>5, :height=>5, :gravity=>"se"}],
28
+ ["auto_orient", {}],
29
+ ["fit_crop", {:width=>10, :height=>32, :gravity=>"c"}]
30
+ ])
31
+
32
+ pipeline = described_class.from_param(operator_list)
33
+ expect(pipeline).not_to be_empty
34
+ end
35
+
36
+ it 'applies itself to the image' do
37
+ pipeline = subject.
38
+ auto_orient.
39
+ fit_crop(width: 48, height: 48, gravity: 'c').
40
+ srgb.
41
+ sharpen(radius: 2, sigma: 0.5).
42
+ ellipse_stencil.
43
+ strip_metadata
44
+
45
+ image = Magick::Image.read(test_image_path)[0]
46
+ pipeline.apply! image, {}
47
+ examine_image(image, "stenciled")
48
+ end
49
+
50
+ it 'raises an exception when an attempt is made to serialize an unknown operator' do
51
+ unknown_op_class = Class.new
52
+ subject << unknown_op_class.new
53
+ expect {
54
+ subject.to_params
55
+ }.to raise_error(/not registered/)
56
+ end
57
+
58
+ it 'composes parameters even if one of the operators does not support to_h' do
59
+ class AnonOp
60
+ end
61
+ class ParametricOp
62
+ def to_h; {a: 133}; end
63
+ end
64
+
65
+ ImageVise.add_operator('t_anon', AnonOp)
66
+ ImageVise.add_operator('t_parametric', ParametricOp)
67
+
68
+ subject << AnonOp.new
69
+ subject << ParametricOp.new
70
+ expect(subject.to_params).to eq([["t_anon", {}], ["t_parametric", {:a=>133}]])
71
+ end
72
+ end
@@ -0,0 +1,336 @@
1
+ require_relative '../spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe ImageVise::RenderEngine do
5
+ include Rack::Test::Methods
6
+
7
+ let(:app) { ImageVise::RenderEngine.new }
8
+
9
+ context 'when the subclass is configured to raise exceptions' do
10
+ after :each do
11
+ ImageVise.reset_allowed_hosts!
12
+ ImageVise.reset_secret_keys!
13
+ end
14
+
15
+ it 'raises an exception instead of returning an error response' do
16
+ class << app
17
+ def raise_exceptions?
18
+ true
19
+ end
20
+ end
21
+
22
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
23
+ image_request = ImageVise::ImageRequest.new(src_url: 'http://unknown.com/image.jpg', pipeline: p)
24
+ expect(app).to receive(:handle_generic_error).and_call_original
25
+ expect {
26
+ get image_request.to_path_params('l33tness')
27
+ }.to raise_error(/No keys set/)
28
+ end
29
+ end
30
+
31
+ context 'when requesting an image' do
32
+ before :each do
33
+ parsed_url = Addressable::URI.parse(public_url)
34
+ ImageVise.add_allowed_host!(parsed_url.host)
35
+ end
36
+
37
+ after :each do
38
+ ImageVise.reset_allowed_hosts!
39
+ ImageVise.reset_secret_keys!
40
+ end
41
+
42
+ it 'halts with 400 when the requested image cannot be opened by ImageMagick' do
43
+ uri = Addressable::URI.parse(public_url)
44
+ ImageVise.add_allowed_host!(uri.host)
45
+ ImageVise.add_secret_key!('l33tness')
46
+ uri.path = '/___nonexistent_image.jpg'
47
+
48
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
49
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
50
+
51
+ expect_any_instance_of(Patron::Session).to receive(:get_file) {|_self, url, path|
52
+ File.open(path, 'wb') {|f| f << 'totally not an image' }
53
+ double(status: 200)
54
+ }
55
+ expect(app).to receive(:handle_request_error).and_call_original
56
+
57
+ get image_request.to_path_params('l33tness')
58
+ expect(last_response.status).to eq(400)
59
+ expect(last_response['Cache-Control']).to match(/public/)
60
+ expect(last_response.body).to include('Unsupported/unknown')
61
+ end
62
+
63
+ it 'halts with 400 when a file:// URL is given and filesystem access is not enabled' do
64
+ uri = 'file://' + test_image_path
65
+ ImageVise.deny_filesystem_sources!
66
+ ImageVise.add_secret_key!('l33tness')
67
+
68
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
69
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
70
+
71
+ get image_request.to_path_params('l33tness')
72
+ expect(last_response.status).to eq(403)
73
+ expect(last_response.body).to include('filesystem access is disabled')
74
+ end
75
+
76
+ it 'responds with 403 when upstream returns it, and includes the URL in the error message' do
77
+ uri = Addressable::URI.parse(public_url)
78
+ ImageVise.add_allowed_host!(uri.host)
79
+ ImageVise.add_secret_key!('l33tness')
80
+ uri.path = '/forbidden'
81
+
82
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
83
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
84
+
85
+ get image_request.to_path_params('l33tness')
86
+ expect(last_response.status).to eq(403)
87
+ expect(last_response.headers['Content-Type']).to eq('application/json')
88
+ parsed = JSON.load(last_response.body)
89
+ expect(parsed['errors'].to_s).to include("Unfortunate upstream response")
90
+ expect(parsed['errors'].to_s).to include(uri.to_s)
91
+ end
92
+
93
+ it 'replays upstream error response codes that are selected to be replayed to the requester' do
94
+ uri = Addressable::URI.parse(public_url)
95
+ ImageVise.add_allowed_host!(uri.host)
96
+ ImageVise.add_secret_key!('l33tness')
97
+
98
+ [404, 403, 503, 504, 500].each do | error_code |
99
+ allow_any_instance_of(Patron::Session).to receive(:get_file).and_return(double(status: error_code))
100
+
101
+ p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
102
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
103
+
104
+ get image_request.to_path_params('l33tness')
105
+
106
+ expect(last_response.status).to eq(error_code)
107
+ expect(last_response.headers).to have_key('Cache-Control')
108
+ expect(last_response.headers['Cache-Control']).to match(/public/)
109
+
110
+ expect(last_response.headers['Content-Type']).to eq('application/json')
111
+ parsed = JSON.load(last_response.body)
112
+ expect(parsed['errors'].to_s).to include("Unfortunate upstream response")
113
+ end
114
+ end
115
+
116
+ it 'sets very far caching headers and an ETag, and returns a 304 if any ETag is set' do
117
+ uri = Addressable::URI.parse(public_url)
118
+ ImageVise.add_allowed_host!(uri.host)
119
+ ImageVise.add_secret_key!('l33tness')
120
+
121
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 35, gravity: 'c')
122
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
123
+
124
+ req_path = image_request.to_path_params('l33tness')
125
+
126
+ get req_path, {}
127
+ expect(last_response).to be_ok
128
+ expect(last_response['ETag']).not_to be_nil
129
+ expect(last_response['Cache-Control']).to match(/public/)
130
+
131
+ get req_path, {}, {'HTTP_IF_NONE_MATCH' => last_response['ETag']}
132
+ expect(last_response.status).to eq(304)
133
+
134
+ # Should consider _any_ ETag a request to rerender something
135
+ # that already exists in an upstream cache
136
+ get req_path, {}, {'HTTP_IF_NONE_MATCH' => SecureRandom.hex(4)}
137
+ expect(last_response.status).to eq(304)
138
+ end
139
+
140
+ it 'responds with an image that passes through all the processing steps' do
141
+ uri = Addressable::URI.parse(public_url)
142
+ ImageVise.add_allowed_host!(uri.host)
143
+ ImageVise.add_secret_key!('l33tness')
144
+
145
+ p = ImageVise::Pipeline.new.geom(geometry_string: '512x335').fit_crop(width: 10, height: 10, gravity: 'c')
146
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
147
+
148
+ get image_request.to_path_params('l33tness')
149
+ expect(last_response.status).to eq(200)
150
+
151
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
152
+ expect(last_response.headers).to have_key('Content-Length')
153
+ parsed_image = Magick::Image.from_blob(last_response.body)[0]
154
+ expect(parsed_image.columns).to eq(10)
155
+ end
156
+
157
+ it 'properly decodes the image request if its Base64 representation contains masked slashes and plus characters' do
158
+ ImageVise.add_secret_key!("this is fab")
159
+ sig = '64759d9ea610d75d9138bfa3ea01595d343ca8994261ae06fca8e6490222f140'
160
+ q = 'eyJwaXBlbGluZSI6W1sic2hhcnBlbiIseyJyYWRpdXMiO' +
161
+ 'jAuNSwic2lnbWEiOjAuNX1dXSwic3JjX3VybCI6InNoYWRl' +
162
+ 'cmljb246L0NQR1BfRmlyZWJhbGw-Yz1kOWM4ZTMzO'+
163
+ 'TZmNjMwYzM1MjM0MTYwMmM2YzJhYmQyZjAzNTcxMTF'+
164
+ 'jIn0'
165
+ params = {q: q, sig: sig}
166
+ req = ImageVise::ImageRequest.from_params(qs_params: params, secrets: ['this is fab'])
167
+
168
+ # We do a check based on the raised exception - the request will fail
169
+ # at the fetcher lookup stage. That stage however takes place _after_ the
170
+ # signature has been validated, which means that the slash within the
171
+ # Base64 payload has been taken into account
172
+ expect(app).to receive(:raise_exceptions?).and_return(true)
173
+ expect {
174
+ get req.to_path_params('this is fab')
175
+ }.to raise_error(/No fetcher registered for shadericon/)
176
+ end
177
+
178
+ it 'calls all of the internal methods during execution' do
179
+ uri = Addressable::URI.parse(public_url)
180
+ ImageVise.add_allowed_host!(uri.host)
181
+ ImageVise.add_secret_key!('l33tness')
182
+
183
+ p = ImageVise::Pipeline.new.geom(geometry_string: '512x335').fit_crop(width: 10, height: 10, gravity: 'c')
184
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
185
+ params = image_request.to_query_string_params('l33tness')
186
+
187
+ expect(app).to receive(:parse_env_into_request).and_call_original
188
+ expect(app).to receive(:process_image_request).and_call_original
189
+ expect(app).to receive(:extract_params_from_request).and_call_original
190
+ expect(app).to receive(:image_rack_response).and_call_original
191
+ expect(app).to receive(:source_file_type_permitted?).and_call_original
192
+
193
+ get image_request.to_path_params('l33tness')
194
+ expect(last_response.status).to eq(200)
195
+ end
196
+
197
+ it 'picks the image from the filesystem if that is permitted' do
198
+ uri = 'file://' + test_image_path
199
+ ImageVise.allow_filesystem_source!(File.dirname(test_image_path) + '/*.*')
200
+ ImageVise.add_secret_key!('l33tness')
201
+
202
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
203
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
204
+
205
+ get image_request.to_path_params('l33tness')
206
+ expect(last_response.status).to eq(200)
207
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
208
+ end
209
+
210
+ it 'URI-decodes the path in a file:// URL for a file with a Unicode path' do
211
+ utf8_file_path = File.dirname(test_image_path) + '/картинка.jpg'
212
+ FileUtils.cp_r(test_image_path, utf8_file_path)
213
+ uri = 'file://' + URI.encode(utf8_file_path)
214
+
215
+ ImageVise.allow_filesystem_source!(File.dirname(test_image_path) + '/*.*')
216
+ ImageVise.add_secret_key!('l33tness')
217
+
218
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
219
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
220
+
221
+ get image_request.to_path_params('l33tness')
222
+ File.unlink(utf8_file_path)
223
+ expect(last_response.status).to eq(200)
224
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
225
+ end
226
+
227
+ it 'forbids a request with an extra GET param' do
228
+ uri = 'file://' + URI.encode(test_image_path)
229
+
230
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
231
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
232
+
233
+ get image_request.to_path_params('l33tness'), {'extra' => '123'}
234
+
235
+ expect(last_response.status).to eq(400)
236
+ end
237
+
238
+ it 'returns the processed JPEG image as a PNG if it had to get an alpha channel during processing' do
239
+ uri = Addressable::URI.parse(public_url)
240
+ ImageVise.add_allowed_host!(uri.host)
241
+ ImageVise.add_secret_key!('l33tness')
242
+
243
+ p = ImageVise::Pipeline.new.geom(geometry_string: '220x220').ellipse_stencil
244
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
245
+
246
+ get image_request.to_path_params('l33tness')
247
+ expect(last_response.status).to eq(200)
248
+
249
+ expect(last_response.headers['Content-Type']).to eq('image/png')
250
+ expect(last_response.headers).to have_key('Content-Length')
251
+
252
+ examine_image_from_string(last_response.body)
253
+ end
254
+
255
+ it 'permits a PSD file by default' do
256
+ uri = Addressable::URI.parse(public_url_psd)
257
+ ImageVise.add_allowed_host!(uri.host)
258
+ ImageVise.add_secret_key!('l33tness')
259
+
260
+ p = ImageVise::Pipeline.new.geom(geometry_string: '220x220').ellipse_stencil
261
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
262
+
263
+ get image_request.to_path_params('l33tness')
264
+ expect(last_response.status).to eq(200)
265
+ end
266
+
267
+ it 'destroys all the loaded PSD layers' do
268
+ uri = Addressable::URI.parse(public_url_psd_multilayer)
269
+ ImageVise.add_allowed_host!(uri.host)
270
+ ImageVise.add_secret_key!('l33tness')
271
+
272
+ p = ImageVise::Pipeline.new.geom(geometry_string: '220x220')
273
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
274
+
275
+ class << app
276
+ def raise_exceptions?; true; end
277
+ end
278
+
279
+ # For each layer loaded into the ImageList
280
+ expect(ImageVise).to receive(:destroy).and_call_original.exactly(5).times
281
+
282
+ get image_request.to_path_params('l33tness')
283
+
284
+ expect(last_response.status).to eq(200)
285
+ end
286
+
287
+ it 'outputs a converted TIFF file as a PNG' do
288
+ uri = Addressable::URI.parse(public_url_tif)
289
+ ImageVise.add_allowed_host!(uri.host)
290
+ ImageVise.add_secret_key!('l33tness')
291
+
292
+ p = ImageVise::Pipeline.new.geom(geometry_string: '220x220')
293
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
294
+
295
+ class << app
296
+ def source_file_type_permitted?(type); true; end
297
+ end
298
+
299
+ get image_request.to_path_params('l33tness')
300
+ expect(last_response.status).to eq(200)
301
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
302
+ end
303
+
304
+ it 'processes a 1.5mb PSD with a forced conversion to JPEG' do
305
+ uri = Addressable::URI.parse(public_url_psd)
306
+ ImageVise.add_allowed_host!(uri.host)
307
+ ImageVise.add_secret_key!('1337ness')
308
+
309
+ p = ImageVise::Pipeline.new.geom(geometry_string: 'x220').force_jpg_out(quality: 85)
310
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
311
+
312
+ get image_request.to_path_params('1337ness')
313
+
314
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
315
+ expect(last_response.status).to eq(200)
316
+
317
+ examine_image_from_string(last_response.body)
318
+ end
319
+
320
+ it 'converts a PNG into a JPG applying a background fill' do
321
+ uri = Addressable::URI.parse(public_url_png_transparency)
322
+ ImageVise.add_allowed_host!(uri.host)
323
+ ImageVise.add_secret_key!('h00ray')
324
+
325
+ p = ImageVise::Pipeline.new.background_fill(color: 'white').geom(geometry_string: 'x220').force_jpg_out(quality: 5)
326
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
327
+
328
+ get image_request.to_path_params('h00ray')
329
+
330
+ expect(last_response.status).to eq(200)
331
+ expect(last_response.headers['Content-Type']).to eq('image/jpeg')
332
+
333
+ examine_image_from_string(last_response.body)
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageVise::Sharpen do
4
+ it 'refuses invalid parameters' do
5
+ expect { described_class.new(sigma: 0, radius: -1) }.to raise_error(ArgumentError)
6
+ end
7
+
8
+ it 'applies the crop with different gravities' do
9
+ [[1, 1], [4, 2], [0.75, 0.5]].each do |(r, s)|
10
+ image = Magick::Image.read(test_image_path)[0]
11
+ expect(ImageVise).to receive(:destroy).with(instance_of(Magick::Image)).and_call_original
12
+ sharpen = described_class.new(radius: r, sigma: s)
13
+ sharpen.apply!(image)
14
+ examine_image(image, "sharpen-rad_%02f-sigma_%02f" % [r, s])
15
+ end
16
+ end
17
+ end