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.
@@ -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