spraycan 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +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
|