spraycan 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +5 -0
- data/Gemfile +6 -0
- data/README.md +20 -0
- data/Rakefile +10 -0
- data/bin/spraycan +48 -0
- data/draw_test.rb +17 -0
- data/lib/core_ext/magick.rb +27 -0
- data/lib/core_ext/profiles/cmyk-profile.icc +0 -0
- data/lib/core_ext/profiles/srgb-profile.icc +0 -0
- data/lib/publisher_s3.rb +8 -0
- data/lib/safe_call.rb +13 -0
- data/lib/spraycan.rb +11 -0
- data/lib/spraycan/base.rb +11 -0
- data/lib/spraycan/rmagick.rb +100 -0
- data/lib/spraycan/version.rb +3 -0
- data/lib/spraycan/workers/image.rb +11 -0
- data/lib/spraycan/workers/manipulate.rb +40 -0
- data/lib/spraycan/workers/previews.rb +47 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/spraycan/rmagick_spec.rb +217 -0
- data/spec/spraycan/workers/manipulate_spec.rb +53 -0
- data/spec/spraycan/workers/previews_spec.rb +78 -0
- data/spec/support/bottle_helper.rb +14 -0
- data/spraycan.gemspec +28 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0c87eb1997c18476e5f7e5f3995863cff6e91173
|
4
|
+
data.tar.gz: f4f3137f02a414b31e554d4ddf8c97503f7202dc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1b37960630ea84edabeddcbbcfa51d9de2a4c62d348cc6891e42f47bf2d9bed3433ad724c1e957f362bd6d9fe977ac7c356d16ceea02cbaac88190dc7ba995ee
|
7
|
+
data.tar.gz: 9a8c8beab9b20160b59f963f9b8a8b1f569a2237330996d00c065e1c4c9e6365a62756d964f9b12124ffd416999de4ab6241a59055ff563fb7d1140964265ce9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Spraycan
|
2
|
+
|
3
|
+
## Development
|
4
|
+
|
5
|
+
### Setup
|
6
|
+
|
7
|
+
Spraycan relies on a few installed libraries to run:
|
8
|
+
|
9
|
+
- imagemagick
|
10
|
+
- ghostscript
|
11
|
+
|
12
|
+
On OS X, installing them via `brew` is easiest:
|
13
|
+
|
14
|
+
> brew install imagemagick --build-from-source
|
15
|
+
> brew install ghostscript
|
16
|
+
|
17
|
+
### Ruby version compatibility
|
18
|
+
|
19
|
+
- Ruby v1.9 for Spraycan versions < v1.1
|
20
|
+
- Ruby v2.1 for Spraycan versions >= v1.1
|
data/Rakefile
ADDED
data/bin/spraycan
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'spraycan'
|
4
|
+
require 'optparse'
|
5
|
+
STDOUT.sync = true
|
6
|
+
|
7
|
+
options = {
|
8
|
+
background: false,
|
9
|
+
broker: '127.0.0.1',
|
10
|
+
env: 'development',
|
11
|
+
queue_name: 'blocks.spraycan'
|
12
|
+
}
|
13
|
+
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = <<-BANNER
|
16
|
+
Spraycan handles images resizing & processing requests
|
17
|
+
|
18
|
+
Usage: #{__FILE__} [options]
|
19
|
+
|
20
|
+
Options are:
|
21
|
+
BANNER
|
22
|
+
|
23
|
+
opts.separator ''
|
24
|
+
|
25
|
+
opts.on('-b', '--broker=HOST', String,
|
26
|
+
'The address of the amqp broker to connect to',
|
27
|
+
"Default: #{options[:broker]}") { |b| options[:broker] = b }
|
28
|
+
|
29
|
+
opts.on('-q', '--queue=queue_name', String,
|
30
|
+
'Specify which queue to listen to',
|
31
|
+
"Default: #{options[:queue_name]}") { |q| options[:queue_name] = q }
|
32
|
+
|
33
|
+
opts.on('-e', '--env=environment', String,
|
34
|
+
'Set the environment to run in',
|
35
|
+
"Default: #{options[:env]}") { |e| options[:env] = e }
|
36
|
+
|
37
|
+
opts.on('-D', '--background',
|
38
|
+
'Run campaigner in the background.') { |b| options[:background] = b }
|
39
|
+
|
40
|
+
opts.on('-h', '--help', 'Show this help message.') { puts opts; exit }
|
41
|
+
end.parse!(ARGV)
|
42
|
+
|
43
|
+
if options[:background]
|
44
|
+
require 'daemons'
|
45
|
+
Daemons.daemonize(app_name: 'spraycan', dir_mode: :normal, dir: '/var/run/campaigner')
|
46
|
+
end
|
47
|
+
|
48
|
+
Spraycan.run(options)
|
data/draw_test.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rmagick"
|
2
|
+
include Magick
|
3
|
+
|
4
|
+
img = Image.new(1024, 1024) do
|
5
|
+
self.background_color = 'blue'
|
6
|
+
end
|
7
|
+
|
8
|
+
txt = Draw.new
|
9
|
+
img.annotate(txt, 0, 0, 0, 0, "In ur Railz, annotatin ur picz.") do
|
10
|
+
txt.gravity = Magick::SouthGravity
|
11
|
+
txt.pointsize = 25
|
12
|
+
txt.stroke = "#000000"
|
13
|
+
txt.fill = "#ffffff"
|
14
|
+
txt.font_weight = Magick::BoldWeight
|
15
|
+
end
|
16
|
+
img.format = "png"
|
17
|
+
File.open("/tmp/img.png", "w") { |f| f << img.to_blob }
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Magick
|
2
|
+
class Image
|
3
|
+
SCT_CMYK_ICC = File.dirname(__FILE__) + '/profiles/cmyk-profile.icc'
|
4
|
+
SCT_SRGB_ICC = File.dirname(__FILE__) + '/profiles/srgb-profile.icc'
|
5
|
+
|
6
|
+
def ensure_rgb!
|
7
|
+
if rgb?
|
8
|
+
strip!
|
9
|
+
elsif cmyk?
|
10
|
+
add_profile SCT_CMYK_ICC if color_profile == nil
|
11
|
+
add_profile SCT_SRGB_ICC
|
12
|
+
else
|
13
|
+
puts 'unkown colorspace!'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def rgb?
|
20
|
+
colorspace == Magick::RGBColorspace || colorspace == Magick::SRGBColorspace
|
21
|
+
end
|
22
|
+
|
23
|
+
def cmyk?
|
24
|
+
colorspace == Magick::CMYKColorspace
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
Binary file
|
Binary file
|
data/lib/publisher_s3.rb
ADDED
data/lib/safe_call.rb
ADDED
data/lib/spraycan.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'aws/s3'
|
2
|
+
require 'bottle'
|
3
|
+
require 'core_ext/magick'
|
4
|
+
require 'publisher_s3'
|
5
|
+
require 'safe_call'
|
6
|
+
|
7
|
+
require 'spraycan/base'
|
8
|
+
require 'spraycan/rmagick'
|
9
|
+
require 'spraycan/version'
|
10
|
+
require 'spraycan/workers/manipulate'
|
11
|
+
require 'spraycan/workers/previews'
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'RMagick'
|
2
|
+
|
3
|
+
module Spraycan
|
4
|
+
|
5
|
+
class Rmagick
|
6
|
+
VALID_OUTPUT_FORMATS = %w(JPEG PNG JPG GIF)
|
7
|
+
|
8
|
+
def resize(image, width, height = nil)
|
9
|
+
width, height = calculate_dimensions(image, width) unless height
|
10
|
+
|
11
|
+
process(image) do
|
12
|
+
change_geometry("#{width}x#{height}") { |cols, rows, image| image.resize!(cols, rows) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def crop(image, tl_x, tl_y, br_x, br_y)
|
17
|
+
process image do
|
18
|
+
crop!(tl_x, tl_y, br_x, br_y)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def crop_resize(image, width, height = nil)
|
23
|
+
width, height = calculate_dimensions(image, width) unless height
|
24
|
+
|
25
|
+
process image do
|
26
|
+
crop_resized!(width, height)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# previews => { :very_small => { :dimensions => 40, :target => "/tmp/spraycan_spec/v_small.thumbnail.image.png" },
|
31
|
+
def previews(image, previews)
|
32
|
+
previews.each_value do |preview|
|
33
|
+
if preview.has_key?(:dimensions)
|
34
|
+
preview.merge!(crop_resize(image, preview.delete(:dimensions)))
|
35
|
+
else
|
36
|
+
preview.merge!(process(image))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
previews.tap do |obj|
|
41
|
+
obj[:original] = dimensions(image)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def exif_data(image)
|
46
|
+
data = Magick::Image.from_blob(image).first.get_exif_by_entry
|
47
|
+
data.inject({}) { |m, exif_entry| m[exif_entry[0]] = exif_entry[1]; m }
|
48
|
+
end
|
49
|
+
|
50
|
+
def resized_crop_mask(image, crop, size)
|
51
|
+
process(image) do
|
52
|
+
resize!(size[:width], size[:height]) if size[:width] > 0 && size[:height] > 0
|
53
|
+
crop!(crop[:x], crop[:y], crop[:width], crop[:height], true) # pass true to clear offset info
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def process(image, &block)
|
60
|
+
img = Magick::Image.from_blob(image).first
|
61
|
+
img.instance_eval &block if block_given?
|
62
|
+
img.ensure_rgb!
|
63
|
+
img.format = get_format(image) # ensure that resized output is a JPEG
|
64
|
+
|
65
|
+
{
|
66
|
+
image: img.to_blob,
|
67
|
+
width: img.columns,
|
68
|
+
height: img.rows,
|
69
|
+
format: img.format
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_format(image_data)
|
74
|
+
current_format = Magick::Image.from_blob(image_data).first.format
|
75
|
+
VALID_OUTPUT_FORMATS.include?(current_format) ? current_format : 'JPEG'
|
76
|
+
end
|
77
|
+
|
78
|
+
def dimensions(image_data)
|
79
|
+
image = Magick::Image.from_blob(image_data).first
|
80
|
+
{ width: image.columns, height: image.rows }
|
81
|
+
end
|
82
|
+
|
83
|
+
def calculate_dimensions(image_data, size)
|
84
|
+
image = Magick::Image.from_blob(image_data).first
|
85
|
+
return image.columns, image.rows unless clipping_required?(image, size)
|
86
|
+
|
87
|
+
ratio = image.columns.to_f / image.rows.to_f
|
88
|
+
|
89
|
+
return [size, size.to_f / ratio] if ratio > 1 # Landscape
|
90
|
+
return [ratio * size, size] if ratio < 1 # Portrait
|
91
|
+
|
92
|
+
[size, size] # Square
|
93
|
+
end
|
94
|
+
|
95
|
+
def clipping_required?(image, size)
|
96
|
+
(image.columns > size) || (image.rows > size)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Spraycan
|
2
|
+
class Image< Bottle::Foreman
|
3
|
+
def process(payload)
|
4
|
+
return failure("Image payload expected a hash, got #{payload.class.to_s}.") unless payload.is_a?(Hash)
|
5
|
+
success({})
|
6
|
+
rescue => e
|
7
|
+
Bugsnag.notify(e)
|
8
|
+
failure("Fatal error handling image: #{e.message}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Spraycan
|
2
|
+
module Workers
|
3
|
+
|
4
|
+
class Manipulate < Bottle::Foreman
|
5
|
+
def process(payload)
|
6
|
+
puts "Starting Manipulate#process. Payload - #{payload.inspect}\n"
|
7
|
+
|
8
|
+
crop = payload[:crop]
|
9
|
+
size = payload[:size]
|
10
|
+
s3_path = payload[:s3_path]
|
11
|
+
|
12
|
+
return failure('s3_path must be specified') unless s3_path
|
13
|
+
return failure('crop not set') unless crop
|
14
|
+
return failure('size not set') unless size
|
15
|
+
|
16
|
+
image = get_image(s3_path)
|
17
|
+
|
18
|
+
response = Rmagick.new.resized_crop_mask(image, crop, size)
|
19
|
+
|
20
|
+
save_to_cloud(response[:image], s3_path)
|
21
|
+
|
22
|
+
GC.start #free the memory
|
23
|
+
return success({})
|
24
|
+
rescue => e
|
25
|
+
return failure 'Fatal error handling image: ' + e.message
|
26
|
+
end
|
27
|
+
|
28
|
+
def save_to_cloud(image, s3_path)
|
29
|
+
Publisher::S3::Upload.upload(image, s3_path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_image(image_path)
|
33
|
+
Publisher::S3::Download.file_content(image_path)
|
34
|
+
rescue
|
35
|
+
raise "Failed to download #{image_path} from S3"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Spraycan
|
2
|
+
module Workers
|
3
|
+
|
4
|
+
class Previews < Bottle::Foreman
|
5
|
+
def process(payload)
|
6
|
+
puts "Starting Previews#process. Payload - #{payload.inspect}\n"
|
7
|
+
|
8
|
+
return failure("Image payload expected a hash, got #{payload.class.to_s}.") unless payload.is_a?(Hash)
|
9
|
+
|
10
|
+
s3_path = payload[:s3_path]
|
11
|
+
previews = payload[:previews]
|
12
|
+
|
13
|
+
return failure('s3_path must be specified') unless s3_path
|
14
|
+
return failure('previews is not a hash') unless previews.is_a?(Hash)
|
15
|
+
|
16
|
+
image = get_image(s3_path)
|
17
|
+
|
18
|
+
response = Rmagick.new.previews(image, previews)
|
19
|
+
|
20
|
+
sizes = {}
|
21
|
+
response.each do |k, v|
|
22
|
+
sizes[k] = [v[:width], v[:height]]
|
23
|
+
|
24
|
+
next if k == :original
|
25
|
+
save_to_cloud(response[k][:image], response[k][:s3_path])
|
26
|
+
v.delete :image
|
27
|
+
end
|
28
|
+
|
29
|
+
GC.start #free the memory
|
30
|
+
success({ :sizes => sizes })
|
31
|
+
rescue => e
|
32
|
+
failure("Fatal error handling image: #{e.message}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def save_to_cloud(image, s3_path)
|
36
|
+
Publisher::S3::Upload.upload(image, s3_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_image(image_path)
|
40
|
+
Publisher::S3::Download.file_content(image_path)
|
41
|
+
rescue
|
42
|
+
raise "Failed to download #{image_path} from S3"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :test)
|
3
|
+
|
4
|
+
require 'spraycan'
|
5
|
+
require 'tmpdir'
|
6
|
+
Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each { |f| require f }
|
7
|
+
|
8
|
+
def write_test_image(path, width=1000, height=1000, format='png')
|
9
|
+
img = Magick::Image.new(width, height) do
|
10
|
+
self.background_color = 'blue'
|
11
|
+
end
|
12
|
+
|
13
|
+
txt = Magick::Draw.new
|
14
|
+
img.annotate(txt, 0, 0, 0, 0, 'In ur Railz, annotatin ur picz.') do
|
15
|
+
txt.gravity = Magick::SouthGravity
|
16
|
+
txt.pointsize = 25
|
17
|
+
txt.stroke = '#000000'
|
18
|
+
txt.fill = '#ffffff'
|
19
|
+
txt.font_weight = Magick::BoldWeight
|
20
|
+
end
|
21
|
+
img.format = format
|
22
|
+
File.open(path, 'w') { |f| f << img.to_blob }
|
23
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
describe Spraycan::Rmagick do
|
2
|
+
|
3
|
+
before do
|
4
|
+
@image = 'image'
|
5
|
+
@columns = 1500; @rows = 1000
|
6
|
+
@blob = 'blob'
|
7
|
+
|
8
|
+
@rm_image = double(Object)
|
9
|
+
@rm_image.stub(:change_geometry).and_return
|
10
|
+
@rm_image.stub(:crop!).and_return
|
11
|
+
@rm_image.stub(:crop_resized!).and_return
|
12
|
+
@rm_image.stub(:resize!).and_return
|
13
|
+
@rm_image.stub(:ensure_rgb!).and_return
|
14
|
+
@rm_image.stub(:to_blob).and_return(@blob)
|
15
|
+
@rm_image.stub(:format).and_return('JPEG')
|
16
|
+
@rm_image.stub(:format=).and_return('JPEG')
|
17
|
+
@rm_image.stub(:columns).and_return(@columns)
|
18
|
+
@rm_image.stub(:rows).and_return(@rows)
|
19
|
+
Magick::Image.stub(:from_blob).and_return([@rm_image])
|
20
|
+
|
21
|
+
@processor = described_class.new
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'when resizing' do
|
25
|
+
|
26
|
+
it 'should auto calculate width/height when passed a single value' do
|
27
|
+
Magick::Image.should_receive(:from_blob).and_return([@rm_image])
|
28
|
+
@processor.should_receive(:calculate_dimensions).with(@image, 300).and_return([300,200])
|
29
|
+
|
30
|
+
@processor.resize @image, 300
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should preprocess images before resizing' do
|
34
|
+
@processor.resize @image, 300, 200
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should change the geometry of the image' do
|
38
|
+
@rm_image.should_receive(:change_geometry).and_return
|
39
|
+
@processor.resize @image, 300, 200
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should convert images to rgb if required' do
|
43
|
+
@rm_image.should_receive(:ensure_rgb!).and_return
|
44
|
+
@processor.resize @image, 300, 200
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should return the raw image data back to the caller' do
|
48
|
+
@rm_image.should_receive(:to_blob).and_return('blob')
|
49
|
+
@processor.resize(@image, 300, 200).should == { :format => 'JPEG', :image => 'blob', :height => 1000, :width => 1500 }
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'when cropping' do
|
55
|
+
|
56
|
+
it 'should preprocess images before cropping' do
|
57
|
+
@processor.crop @image, 10, 10, 100, 100
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should change the geometry of the image' do
|
61
|
+
@rm_image.should_receive(:crop!).and_return
|
62
|
+
@processor.crop @image, 10, 10, 100, 100
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should convert images to rgb if required' do
|
66
|
+
@rm_image.should_receive(:ensure_rgb!).and_return
|
67
|
+
@processor.resize @image, 300, 200
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should return the raw image data back to the caller' do
|
71
|
+
@rm_image.should_receive(:to_blob).and_return('blob')
|
72
|
+
@processor.crop(@image, 10, 10, 100, 100).should == { :format => 'JPEG', :image => 'blob', :height => 1000, :width => 1500 }
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'when performing a resized crop' do
|
78
|
+
|
79
|
+
it 'should preprocess images before resizing/cropping' do
|
80
|
+
@processor.crop_resize @image, 75, 75
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should change the geometry of the image' do
|
84
|
+
@rm_image.should_receive(:crop_resized!).and_return
|
85
|
+
@processor.crop_resize @image, 75, 75
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should convert images to rgb if required' do
|
89
|
+
@rm_image.should_receive(:ensure_rgb!).and_return
|
90
|
+
@processor.resize @image, 300, 200
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return the raw image data back to the caller' do
|
94
|
+
@rm_image.should_receive(:to_blob).and_return('blob')
|
95
|
+
@processor.crop_resize(@image, 75, 75).should == { :format => 'JPEG', :image => 'blob', :height => 1000, :width => 1500 }
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'when generating previews' do
|
101
|
+
|
102
|
+
it 'should change the geometry of the image' do
|
103
|
+
@rm_image.should_receive(:crop_resized!).twice.and_return
|
104
|
+
@processor.previews @image, { :small => {:dimensions => 200}, :large => {:dimensions => 500} }
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should return the raw image data back to the caller' do
|
108
|
+
@rm_image.should_receive(:to_blob).twice.and_return('blob')
|
109
|
+
@processor.previews(@image, { :small => {:dimensions => 1000}, :large => {:dimensions => 500} }).should == { :small=> { :format => 'JPEG', :height => 1000, :width => 1500, :image => 'blob' }, :large=> { :format => 'JPEG', :height => 1000, :width => 1500, :image => 'blob' }, :original => {:width => 1500, :height => 1000} }
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'crop_and_resize' do
|
115
|
+
|
116
|
+
it 'should return an image with the specified sizes' do
|
117
|
+
@processor.resized_crop_mask(@image, {:x => 0, :y => 0, :width => 800, :height => 800 }, { :width => 2890, :height => 1940 }).should == {
|
118
|
+
:format => 'JPEG', :image => 'blob', :width => 1500, :height => 1000}
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should return an image cropped to the crop marks' do
|
123
|
+
@rm_image.should_receive(:crop!).with(0,20,800,900, true).once
|
124
|
+
@processor.resized_crop_mask(@image, {:x => 0, :y => 20, :width => 800, :height => 900 }, { :width => 2890, :height => 1940 }).should == {
|
125
|
+
:format => 'JPEG', :image => 'blob', :width => 1500, :height => 1000}
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should return the raw image data back to the caller' do
|
129
|
+
@rm_image.should_receive(:resize!).with(2890,1940).once
|
130
|
+
@processor.resized_crop_mask(@image, {:x => 0, :y => 0, :width => 800, :height => 800 }, { :width => 2890, :height => 1940 }).should == {
|
131
|
+
:format => 'JPEG', :image => 'blob', :width => 1500, :height => 1000}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe 'exif_data' do
|
136
|
+
it 'should return a hash of exif data for the image' do
|
137
|
+
@rm_image.should_receive(:get_exif_by_entry).once.and_return([['Make', 'Canon'], ['ShutterSpeedValue', '189/32']])
|
138
|
+
@processor.exif_data(@image).should == { 'Make' => 'Canon', 'ShutterSpeedValue' => '189/32' }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'get_format' do
|
143
|
+
|
144
|
+
described_class::VALID_OUTPUT_FORMATS.each do |valid_format|
|
145
|
+
it "should not change the image format if it is a #{valid_format}" do
|
146
|
+
@rm_image.should_receive(:format).and_return(valid_format)
|
147
|
+
@processor.send(:get_format, @rm_image).should == valid_format
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should change the format to JPEG if a non valid return format is found' do
|
152
|
+
['TIF', 'TIFF', 'PDF'].each do |bad_format|
|
153
|
+
@rm_image.should_receive(:format).and_return(bad_format)
|
154
|
+
@processor.send(:get_format, @rm_image).should == 'JPEG'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
describe 'dimension calculation' do
|
161
|
+
|
162
|
+
it 'should automatically recognize images in portrait mode' do
|
163
|
+
@processor.send(:calculate_dimensions, @rm_image, 300).should == [300,200]
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should automatically recognize images in landscape mode' do
|
167
|
+
@rm_image.stub(:columns).and_return(1000)
|
168
|
+
@rm_image.stub(:rows).and_return(1500)
|
169
|
+
@processor.send(:calculate_dimensions, @rm_image, 300).should == [200,300]
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should automatically clip resizes if they are larger than the original' do
|
173
|
+
@processor.send(:calculate_dimensions, @rm_image, 2000).should == [1500,1000]
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
describe Magick::Image, 'when created' do
|
181
|
+
|
182
|
+
before do
|
183
|
+
@image = Magick::Image.new(200, 100)
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should be able to convert the profile to srgb when requested' do
|
187
|
+
@image.should respond_to(:ensure_rgb!)
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
describe Magick::Image, 'when converting images to sRGB' do
|
193
|
+
|
194
|
+
before do
|
195
|
+
@image = Magick::Image.new(200, 100)
|
196
|
+
@image.stub(:add_profile).and_return
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'should return if the image is already in sRGB colour space' do
|
200
|
+
@image.colorspace = Magick::RGBColorspace
|
201
|
+
@image.should_not_receive(:add_profile)
|
202
|
+
@image.ensure_rgb!
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should add a CMYK profile if the image does not have a profile and is in CMYK colour space' do
|
206
|
+
@image.colorspace = Magick::CMYKColorspace
|
207
|
+
@image.should_receive(:add_profile).twice.and_return
|
208
|
+
@image.ensure_rgb!
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'should add a sRGB profile if the image is in CMYK colour space' do
|
212
|
+
@image.colorspace = Magick::CMYKColorspace
|
213
|
+
@image.should_receive(:add_profile).with(Magick::Image::SCT_SRGB_ICC).and_return
|
214
|
+
@image.ensure_rgb!
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
include Spraycan::Workers
|
4
|
+
|
5
|
+
describe Spraycan::Workers::Manipulate do
|
6
|
+
before do
|
7
|
+
tmpdir = Dir.mktmpdir
|
8
|
+
image_path = File.join(tmpdir, 'image.png')
|
9
|
+
write_test_image(image_path, 1280, 800)
|
10
|
+
|
11
|
+
@payload = {
|
12
|
+
:crop => {:x=>497, :y=>228, :width=>217, :height=>168},
|
13
|
+
:size => {:width=>921, :height=>576},
|
14
|
+
:s3_path => 'some/key'
|
15
|
+
}
|
16
|
+
|
17
|
+
Publisher::S3::Upload.stub(:upload)
|
18
|
+
file_content = File.read(image_path)
|
19
|
+
Publisher::S3::Download.stub(:file_content).and_return(file_content)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should require s3_path' do
|
23
|
+
@payload.delete :s3_path
|
24
|
+
Manipulate.new.process(@payload).should fail_with 's3_path must be specified'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should require a valid image' do
|
28
|
+
Publisher::S3::Download.stub(:file_content).with(@payload[:s3_path]).and_raise('Error')
|
29
|
+
Manipulate.new.process(@payload).should fail_with 'Fatal error handling image: Failed to download some/key from S3'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should require crop' do
|
33
|
+
@payload.delete :crop
|
34
|
+
Manipulate.new.process(@payload).should fail_with 'crop not set'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should require size' do
|
38
|
+
@payload.delete :size
|
39
|
+
Manipulate.new.process(@payload).should fail_with 'size not set'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should fail correctly' do
|
43
|
+
manipulate = Manipulate.new
|
44
|
+
manipulate.stub(:get_image).and_raise('Some bad thing happened')
|
45
|
+
manipulate.process(@payload).should fail_with 'Fatal error handling image: Some bad thing happened'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should save manipulated image to the cloud' do
|
49
|
+
Publisher::S3::Upload.should_receive(:upload)
|
50
|
+
Manipulate.new.process(@payload)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
include Spraycan::Workers
|
2
|
+
|
3
|
+
describe Spraycan::Workers::Previews do
|
4
|
+
before do
|
5
|
+
tmpdir = Dir.mktmpdir
|
6
|
+
image_path = File.join(tmpdir, 'image.png')
|
7
|
+
write_test_image(image_path, 1280, 800)
|
8
|
+
@payload = {
|
9
|
+
:previews => {
|
10
|
+
:very_small=>
|
11
|
+
{:dimensions=>40,
|
12
|
+
:target=>File.join(tmpdir, 'v_small.thumbnail.image.png')},
|
13
|
+
:small=>
|
14
|
+
{:dimensions=>135,
|
15
|
+
:target=>File.join(tmpdir, 'small.thumbnail.image.png')},
|
16
|
+
:preview=>
|
17
|
+
{:dimensions=>450,
|
18
|
+
:target=>File.join(tmpdir, 'preview.thumbnail.image.png')},
|
19
|
+
:large_preview=>
|
20
|
+
{:dimensions=>1100,
|
21
|
+
:target=>File.join(tmpdir, 'large_preview.thumbnail.image.png')}
|
22
|
+
},
|
23
|
+
:s3_path => 'some/key'
|
24
|
+
}
|
25
|
+
Publisher::S3::Upload.stub(:upload)
|
26
|
+
file_content = File.read(image_path)
|
27
|
+
Publisher::S3::Download.stub(:file_content).and_return(file_content)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should fail correctly' do
|
31
|
+
preview = Previews.new
|
32
|
+
preview.stub(:get_image).and_raise('Some bad thing happened')
|
33
|
+
|
34
|
+
preview.process(@payload).should fail_with 'Fatal error handling image: Some bad thing happened'
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'payload' do
|
38
|
+
it 'should fail unless payload is a hash' do
|
39
|
+
Previews.new.process([]).should fail_with 'Image payload expected a hash, got Array.'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should require a s3 path' do
|
43
|
+
@payload.delete(:s3_path)
|
44
|
+
Previews.new.process(@payload).should fail_with 's3_path must be specified'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should require a valid image' do
|
48
|
+
Publisher::S3::Download.stub(:file_content).with(@payload[:s3_path]).and_raise('Error')
|
49
|
+
Previews.new.process(@payload).should fail_with "Fatal error handling image: Failed to download #{@payload[:s3_path]} from S3"
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should require an hash of previews' do
|
53
|
+
@payload.delete :previews
|
54
|
+
Previews.new.process(@payload).should fail_with 'previews is not a hash'
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'sizes response' do
|
60
|
+
it 'should return sizes correctly' do
|
61
|
+
Previews.new.process(@payload).should succeed_with({:sizes=>{:very_small=>[40, 25], :small=>[135, 84], :preview=>[450, 281], :large_preview=>[1100, 687], :original=>[1280, 800]}})
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should create images in payload' do
|
66
|
+
@payload[:previews].each do |size, value|
|
67
|
+
next if size == :original
|
68
|
+
Publisher::S3::Upload.should_receive(:upload)
|
69
|
+
end
|
70
|
+
Previews.new.process(@payload)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should save each new crop image to the cloud' do
|
74
|
+
Publisher::S3::Upload.should_receive(:upload)
|
75
|
+
Previews.new.process(@payload)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
RSpec::Matchers.define :fail_with do |expected|
|
2
|
+
match do |actual|
|
3
|
+
actual[:state] == 'error'
|
4
|
+
actual[:message] == expected
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
RSpec::Matchers.define :succeed_with do |expected|
|
9
|
+
match do |actual|
|
10
|
+
actual[:state] == 'success'
|
11
|
+
actual.delete(:state)
|
12
|
+
actual == expected
|
13
|
+
end
|
14
|
+
end
|
data/spraycan.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib/', __FILE__)
|
3
|
+
require 'spraycan/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'spraycan'
|
7
|
+
s.version = Spraycan::VERSION
|
8
|
+
s.authors = ['Nick Marfleet', 'Alan Harper']
|
9
|
+
s.email = ['nick@sct.com.au']
|
10
|
+
s.homepage = 'http://www.blocksglobal.com'
|
11
|
+
s.summary = %q{Image processor for blocks}
|
12
|
+
s.description = s.summary + ' using the bottle messaging framework'
|
13
|
+
|
14
|
+
s.rubyforge_project = 'spraycan'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
|
21
|
+
s.required_ruby_version = '~> 2.1.8'
|
22
|
+
|
23
|
+
s.add_development_dependency 'rspec', '~> 2.14.1'
|
24
|
+
|
25
|
+
s.add_runtime_dependency 'aws-s3'
|
26
|
+
s.add_runtime_dependency 'daemons'
|
27
|
+
s.add_runtime_dependency 'rmagick'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spraycan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Marfleet
|
8
|
+
- Alan Harper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-03-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 2.14.1
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 2.14.1
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: aws-s3
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: daemons
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rmagick
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: Image processor for blocks using the bottle messaging framework
|
71
|
+
email:
|
72
|
+
- nick@sct.com.au
|
73
|
+
executables:
|
74
|
+
- spraycan
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- ".rspec"
|
80
|
+
- Gemfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/spraycan
|
84
|
+
- draw_test.rb
|
85
|
+
- lib/core_ext/magick.rb
|
86
|
+
- lib/core_ext/profiles/cmyk-profile.icc
|
87
|
+
- lib/core_ext/profiles/srgb-profile.icc
|
88
|
+
- lib/publisher_s3.rb
|
89
|
+
- lib/safe_call.rb
|
90
|
+
- lib/spraycan.rb
|
91
|
+
- lib/spraycan/base.rb
|
92
|
+
- lib/spraycan/rmagick.rb
|
93
|
+
- lib/spraycan/version.rb
|
94
|
+
- lib/spraycan/workers/image.rb
|
95
|
+
- lib/spraycan/workers/manipulate.rb
|
96
|
+
- lib/spraycan/workers/previews.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
- spec/spraycan/rmagick_spec.rb
|
99
|
+
- spec/spraycan/workers/manipulate_spec.rb
|
100
|
+
- spec/spraycan/workers/previews_spec.rb
|
101
|
+
- spec/support/bottle_helper.rb
|
102
|
+
- spraycan.gemspec
|
103
|
+
homepage: http://www.blocksglobal.com
|
104
|
+
licenses: []
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 2.1.8
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project: spraycan
|
122
|
+
rubygems_version: 2.4.5.1
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Image processor for blocks
|
126
|
+
test_files:
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
- spec/spraycan/rmagick_spec.rb
|
129
|
+
- spec/spraycan/workers/manipulate_spec.rb
|
130
|
+
- spec/spraycan/workers/previews_spec.rb
|
131
|
+
- spec/support/bottle_helper.rb
|