spraycan 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --color
2
+ --format progress
3
+ --order random
4
+ --profile 5
5
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'bottle', :git => 'git://github.com/square-circle-triangle/bottle.git', :ref => 'master'
5
+ gem 'publisher-s3', :git => 'git@bitbucket.org:squarecircletriangle/publisher-s3.git', :ref => 'master'
6
+ gemspec
@@ -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
@@ -0,0 +1,10 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
7
+
8
+ namespace :ci do
9
+ task :tests => :spec
10
+ end
@@ -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)
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'publisher/s3'
2
+
3
+ Publisher::S3.configure do |config|
4
+ config.access_key = ENV['S3_ACCESS_KEY']
5
+ config.access_secret = ENV['S3_SECRET_KEY']
6
+ config.bucket_name = ENV['S3_ASSETS_BUCKET_NAME']
7
+ config.default_host = ENV['S3_DEFAULT_HOST']
8
+ end
@@ -0,0 +1,13 @@
1
+ module SafeCall
2
+
3
+ def safe_call
4
+ begin
5
+ yield
6
+ rescue Errno::ECONNRESET
7
+ i ||= 0
8
+ i += 1
9
+ retry if i < 3
10
+ end
11
+ end
12
+
13
+ end
@@ -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,11 @@
1
+ module Spraycan
2
+ def self.run(opts={})
3
+ start(opts[:env])
4
+ server = Bottle::Server.new(opts[:queue_name], { :host => opts[:broker] })
5
+ server.poll
6
+ end
7
+
8
+ def self.start(env)
9
+ @@env = env.to_sym
10
+ end
11
+ end
@@ -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,3 @@
1
+ module Spraycan
2
+ VERSION = '1.1.3'
3
+ 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
@@ -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
@@ -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