mugshot 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/features/convert_image_format.feature +22 -0
  2. data/features/crop_image.feature +5 -5
  3. data/features/define_image_quality.feature +8 -0
  4. data/features/resize_image.feature +22 -0
  5. data/features/retrieve_image_with_any_name.feature +9 -0
  6. data/features/set_background.feature +7 -0
  7. data/features/step_definitions/all_steps.rb +65 -14
  8. data/features/support/env.rb +24 -7
  9. data/features/support/files/test-with_75%_of_compression.jpg +0 -0
  10. data/features/support/files/test.gif +0 -0
  11. data/features/support/files/test.png +0 -0
  12. data/features/support/files/with_alpha_channel-with_a_red_background.jpg +0 -0
  13. data/features/support/files/with_alpha_channel.png +0 -0
  14. data/features/upload_image.feature +21 -0
  15. data/lib/mugshot/application.rb +30 -24
  16. data/lib/mugshot/fs_storage.rb +1 -0
  17. data/lib/mugshot/http_storage.rb +25 -0
  18. data/lib/mugshot/image.rb +28 -12
  19. data/lib/mugshot/magick_factory.rb +12 -0
  20. data/lib/mugshot/storage.rb +1 -0
  21. data/lib/mugshot.rb +7 -11
  22. data/spec/files/firefox_png.png +0 -0
  23. data/spec/files/test_png.png +0 -0
  24. data/spec/mugshot/application_spec.rb +59 -42
  25. data/spec/mugshot/http_storage_spec.rb +33 -0
  26. data/spec/mugshot/image_spec.rb +31 -38
  27. data/spec/mugshot/magick_factory_spec.rb +13 -0
  28. data/spec/spec.opts +1 -4
  29. data/spec/spec_helper.rb +18 -6
  30. metadata +126 -48
  31. data/features/retrieve_resized_image.feature +0 -10
  32. data/features/retrieve_resized_image_keeping_aspect_ratio.feature +0 -22
  33. data/lib/mugshot/proxy.rb +0 -32
  34. data/spec/mugshot/proxy_spec.rb +0 -67
  35. /data/features/support/files/{test.crop.300x200.jpg → test-cropped_to_300x200.jpg} +0 -0
  36. /data/features/support/files/{test.200x.jpg → test-resized_to_200x.jpg} +0 -0
  37. /data/features/support/files/{test.200x200.jpg → test-resized_to_200x200.jpg} +0 -0
  38. /data/features/support/files/{test.x200.jpg → test-resized_to_x200.jpg} +0 -0
@@ -0,0 +1,22 @@
1
+ Feature: Convert Image Format
2
+
3
+ Scenario: Convert image to gif
4
+ Given an image
5
+
6
+ When I ask for it with format gif
7
+
8
+ Then I should get it with format gif
9
+
10
+ Scenario: Convert image to jpg
11
+ Given an image
12
+
13
+ When I ask for it with format jpg
14
+
15
+ Then I should get it with format jpg
16
+
17
+ Scenario: Convert image to png
18
+ Given an image
19
+
20
+ When I ask for it with format png
21
+
22
+ Then I should get it with format png
@@ -1,8 +1,8 @@
1
1
  Feature: Crop image
2
2
 
3
- Scenario: Crop image with known aspect ratio
4
- When I upload an image
3
+ Scenario: Crop image
4
+ Given an image
5
+
6
+ When I ask for it cropped to 300x200
5
7
 
6
- And I ask for the 300x200 cropped image
7
-
8
- Then I should get the 300x200 cropped image
8
+ Then I should get it cropped to 300x200
@@ -0,0 +1,8 @@
1
+ Feature: Define image quality
2
+
3
+ Scenario: Define compression ratio
4
+ Given an image
5
+
6
+ When I ask for it with 75% of compression
7
+
8
+ Then I should get it with 75% of compression
@@ -0,0 +1,22 @@
1
+ Feature: Resize image
2
+
3
+ Scenario: Resize image
4
+ Given an image
5
+
6
+ When I ask for it resized to 200x200
7
+
8
+ Then I should get it resized to 200x200
9
+
10
+ Scenario: Resize image giving only width
11
+ Given an image
12
+
13
+ When I ask for it resized to 200x
14
+
15
+ Then I should get it resized to 200x
16
+
17
+ Scenario: Resized image giving only height
18
+ Given an image
19
+
20
+ When I ask for it resized to x200
21
+
22
+ Then I should get it resized to x200
@@ -0,0 +1,9 @@
1
+ Feature: Retrieve image with any name
2
+
3
+ Scenario: Retrieve image with any name
4
+ Given an image
5
+
6
+ When I ask for it with the name "any_name"
7
+ And I ask for it with the name "42"
8
+
9
+ Then all of them should be the same
@@ -0,0 +1,7 @@
1
+ Feature: Set Background
2
+
3
+ Scenario: Set background for a png image with alpha channel
4
+ Given a png image with alpha channel
5
+ When I ask for it with a red background
6
+ Then I should get it with a red background
7
+
@@ -1,35 +1,86 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ Given /^(a|an) (.*)Storage$/ do |_, storage_name|
3
+ case storage_name
4
+ when 'FS' then CucumberWorld.storage = Mugshot::FSStorage.new('/tmp/mugshot/cucumber')
5
+ when 'HTTP' then CucumberWorld.storage = Mugshot::HTTPStorage.new
6
+ end
7
+ end
8
+
9
+ Given /^an image$/ do
10
+ Given "a jpg image test"
11
+ end
12
+
13
+ Given /^a (.*) image (.*)$/ do |ext, name|
14
+ @image = {
15
+ :name => name,
16
+ :ext => ext,
17
+ :filename => "#{name.gsub(' ', '_')}.#{ext}"}
18
+
19
+ @image[:id] = write_image(@image[:filename])
20
+ end
21
+
2
22
  When /^I upload an image$/ do
3
23
  post '/', "file" => Rack::Test::UploadedFile.new("features/support/files/test.jpg", "image/jpeg")
4
24
  @image_id = last_response.body
5
25
  end
6
26
 
7
- When /^I ask for the (.*) resized image$/ do |size|
8
- get "/resize/#{size}/#{@image_id}.jpg"
9
- @retrieved_image = last_response.body
27
+ When /^I ask for it$/ do
28
+ get "/#{@image_id}/any_name.jpg"
29
+ @retrieved_image = Magick::Image.from_blob(last_response.body).first
30
+ end
31
+
32
+ When /^I ask for an image that doesn't exist$/ do
33
+ get "/nonexistant_id/any_name.jpg"
34
+ end
35
+
36
+ When /^I ask for it resized to (.*)$/ do |size|
37
+ get "/resize/#{size}/#{@image[:id]}/any_name.jpg"
38
+ @retrieved_image = Magick::Image.from_blob(last_response.body).first
39
+ end
40
+
41
+ When /^I ask for it cropped to (.*)$/ do |size|
42
+ get "/crop/#{size}/#{@image[:id]}/any_name.jpg"
43
+ @retrieved_image = Magick::Image.from_blob(last_response.body).first
44
+ end
45
+
46
+ When /^I ask for it with (\d*)% of compression$/ do |compression|
47
+ get "/quality/#{compression}/#{@image[:id]}/any_name.jpg"
48
+ @retrieved_image = Magick::Image.from_blob(last_response.body).first
49
+ end
50
+
51
+ When /^I ask for it with format (.*)$/ do |format|
52
+ get "/#{@image[:id]}/any_name.#{format}"
53
+ @retrieved_image = Magick::Image.from_blob(last_response.body).first
54
+ end
55
+
56
+ When /^I ask for it with the name "(.*)"$/ do |name|
57
+ get "/#{@image[:id]}/#{name}.jpg"
58
+ @images_by_name[name] = Magick::Image.from_blob(last_response.body).first
10
59
  end
11
60
 
12
- When /^I ask for a (.*) resized image that doesn't exist$/ do |size|
13
- get "/resize/#{size}/nonexistant.jpg"
61
+ When /^I ask for it with a (.*) background$/ do |color|
62
+ get "/background/#{color}/#{@image[:id]}/img.jpg"
63
+ @retrieved_image = Magick::Image.from_blob(last_response.body).first
14
64
  end
15
65
 
16
- When /^I ask for the (.*) cropped image$/ do |size|
17
- get "/crop/#{size}/#{@image_id}.jpg"
18
- @retrieved_image = last_response.body
66
+ Then /^I should get it$/ do
67
+ @retrieved_image.should be_same_image_as("test.jpg")
19
68
  end
20
69
 
21
70
  Then /^I should get a (\d+) response$/ do |response_code|
22
71
  last_response.status.should == response_code.to_i
23
72
  end
24
73
 
25
- Then /^I should get the (.*) resized image$/ do |size|
26
- @retrieved_image.should be_same_image_as("test.#{size}.jpg")
74
+ Then /^I should get it with format (.*)$/ do |expected_format|
75
+ @retrieved_image.should be_same_image_as("test.#{expected_format}")
27
76
  end
28
77
 
29
- Then /^I should get the (.*) resized image keeping the aspect ratio$/ do |size|
30
- @retrieved_image.should be_same_image_as("test.#{size}.jpg")
78
+ Then /^I should get it (.*)$/ do |name|
79
+ @retrieved_image.should be_same_image_as("#{@image[:name].gsub(' ', '_')}-#{name.underscore.gsub(' ', '_')}.jpg")
31
80
  end
32
81
 
33
- Then /^I should get the (.*) cropped image$/ do |size|
34
- @retrieved_image.should be_same_image_as("test.crop.#{size}.jpg")
82
+ Then /^all of them should be the same$/ do
83
+ @images_by_name.values.each do |img|
84
+ img.should be_same_image_as("test.jpg")
85
+ end
35
86
  end
@@ -1,32 +1,49 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ require "rubygems"
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
2
5
  require 'mugshot'
3
6
 
4
7
  require 'rack/test'
5
- require 'spec/expectations'
6
-
8
+ require 'rspec/expectations'
7
9
  require 'pp'
8
10
 
11
+ require 'RMagick'
12
+
9
13
  module CucumberWorld
14
+ include Rspec::Matchers
10
15
  include Rack::Test::Methods
16
+
17
+ mattr_accessor :storage
11
18
 
12
19
  def app
13
- Mugshot::Application.new(Mugshot::FSStorage.new('/tmp/mugshot/cucumber'))
20
+ Mugshot::Application.new(storage)
21
+ end
22
+
23
+ def write_image(filename)
24
+ Mugshot::FSStorage.new('/tmp/mugshot/cucumber').
25
+ write(IO.read(File.expand_path(filename, "features/support/files/")))
14
26
  end
15
27
  end
16
28
  World(CucumberWorld)
17
29
 
30
+ Before do
31
+ @images_by_name = HashWithIndifferentAccess.new
32
+ CucumberWorld.storage = Mugshot::FSStorage.new('/tmp/mugshot/cucumber')
33
+ end
34
+
18
35
  After do
19
36
  require 'fileutils'
20
37
  FileUtils.rm_rf("/tmp/mugshot/cucumber")
21
38
  end
22
39
 
23
- Spec::Matchers.define :be_same_image_as do |expected_filename|
24
- match do |actual_blob|
25
- actual = Magick::Image.from_blob(actual_blob).first
26
- expected = Magick::Image.read(File.expand_path(__FILE__ + "/../files/#{expected_filename}")).first
40
+ Rspec::Matchers.define :be_same_image_as do |expected|
41
+ match do |actual|
42
+ expected = Magick::Image.read(File.expand_path(__FILE__ + "/../files/#{expected}")).first if expected.is_a?(String)
27
43
 
28
44
  actual.columns == expected.columns &&
29
45
  actual.rows == expected.rows &&
30
46
  actual.difference(expected)[1] < 0.01
31
47
  end
32
48
  end
49
+
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ Feature: Upload Image
2
+
3
+ Scenario: Filesystem storage
4
+ Given an FSStorage
5
+
6
+ When I upload an image
7
+ And I ask for it
8
+
9
+ Then I should get it
10
+
11
+ Scenario: HTTP storage
12
+ Given an HTTPStorage
13
+
14
+ When I upload an image
15
+
16
+ Then I should get a 405 response
17
+
18
+ Scenario: Image wasn't uploaded
19
+ When I ask for an image that doesn't exist
20
+
21
+ Then I should get a 404 response
@@ -3,6 +3,8 @@ require 'sinatra/base'
3
3
 
4
4
  class Mugshot::Application < Sinatra::Base
5
5
 
6
+ VALID_OPERATIONS = %w[background crop resize quality]
7
+
6
8
  set :static, true
7
9
  set :public, ::File.expand_path(::File.join(::File.dirname(__FILE__), "public"))
8
10
 
@@ -15,28 +17,18 @@ class Mugshot::Application < Sinatra::Base
15
17
  end
16
18
 
17
19
  post '/?' do
18
- @storage.write(params['file'][:tempfile].read)
19
- end
20
-
21
- get '/:size/:id.:format' do |size, id, format|
22
- image = @storage.read(id)
23
- halt 404 if image.blank?
24
-
25
- image.resize!(size)
26
-
27
- begin
28
- send_image(image, format.to_sym)
29
- ensure
30
- image.destroy!
31
- end
20
+ id = @storage.write(params['file'][:tempfile].read)
21
+ halt 405 if id.blank?
22
+ id
32
23
  end
33
24
 
34
- get '/*/:id.:format' do |splat, id, format|
25
+ get '/*/?:id/:name.:format' do |splat, id, _, format|
35
26
  image = @storage.read(id)
36
27
  halt 404 if image.blank?
37
28
 
38
29
  begin
39
- process_operations(image, splat)
30
+ process_operations(image, @default_operations)
31
+ process_operations(image, operations_from_splat(splat))
40
32
  send_image(image, format.to_sym)
41
33
  ensure
42
34
  image.destroy!
@@ -45,23 +37,37 @@ class Mugshot::Application < Sinatra::Base
45
37
 
46
38
  protected
47
39
 
48
- def initialize(storage)
49
- @storage = storage
40
+ def initialize(opts)
41
+ opts = {:storage => opts} if opts.kind_of?(Mugshot::Storage)
42
+ opts.to_options!
43
+
44
+ @storage = opts.delete(:storage)
45
+
46
+ @default_operations = opts
50
47
  end
51
48
 
52
49
  private
53
50
 
54
- def process_operations(image, splat)
55
- operations = Hash[*splat.split('/')]
56
- operations.assert_valid_keys('crop', 'resize') rescue halt 404
57
- operations.each do |op, op_params|
58
- image.send("#{op}!", op_params)
51
+ def operations_from_splat(splat)
52
+ operations = []
53
+ begin
54
+ operations = Hash[*splat.split('/')]
55
+ operations.assert_valid_keys(VALID_OPERATIONS)
56
+ rescue
57
+ halt 404
59
58
  end
59
+ operations
60
60
  end
61
61
 
62
+ def process_operations(image, operations)
63
+ operations.each do |op, op_params|
64
+ image.send("#{op}!", op_params.to_s)
65
+ end
66
+ end
67
+
62
68
  def send_image(image, format)
63
69
  content_type format
64
70
  response['Content-Disposition'] = 'inline'
65
- image.to_blob
71
+ image.to_blob(:format => format)
66
72
  end
67
73
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ require 'fileutils'
2
3
  class Mugshot::FSStorage < Mugshot::Storage
3
4
  def write(bin)
4
5
  returning asset_id do |id|
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'fileutils'
4
+ require 'net/http'
5
+
6
+ class Mugshot::HTTPStorage < Mugshot::Storage
7
+ def write(bin)
8
+ nil
9
+ end
10
+
11
+ def read(id)
12
+ url = URI.parse(@url_resolver.call(id))
13
+ res = Net::HTTP.start(url.host, url.port) {|http| http.get(url.path)}
14
+
15
+ return nil unless res.code.to_i == 200
16
+
17
+ Mugshot::Image.new(res.body)
18
+ end
19
+
20
+ protected
21
+
22
+ def initialize(&block)
23
+ @url_resolver = block
24
+ end
25
+ end
data/lib/mugshot/image.rb CHANGED
@@ -1,17 +1,13 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ require 'RMagick'
2
3
  class Mugshot::Image
3
4
 
4
- def width
5
- @image.columns
6
- end
7
-
8
- def height
9
- @image.rows
5
+ def background!(color)
6
+ @background_color = color
10
7
  end
11
8
 
12
9
  def resize!(size)
13
10
  w, h = parse_size(size)
14
-
15
11
  if [w, h].include?(nil)
16
12
  @image.resize_to_fit! w, h
17
13
  else
@@ -27,28 +23,48 @@ class Mugshot::Image
27
23
  self
28
24
  end
29
25
 
26
+ def quality!(quality)
27
+ @quality = quality.to_i
28
+ self
29
+ end
30
+
30
31
  def destroy!
31
32
  @image.destroy!
32
33
  self
33
34
  end
34
35
 
35
36
  def to_blob(opts = {})
37
+ opts.merge!(:quality => @quality)
38
+
39
+ set_background(@background_color) if !!@background_color
40
+
36
41
  @image.strip!
37
42
  @image.to_blob do
38
- self.format = opts[:format].to_s if opts[:format].present?
43
+ self.format = opts[:format].to_s if opts.include?(:format)
39
44
  self.quality = opts[:quality] if opts[:quality].present?
40
45
  end
41
46
  end
42
47
 
43
48
  protected
44
-
45
- def initialize(file)
46
- @image = Magick::Image.read(file).first
49
+ def initialize(file_or_blob)
50
+ if file_or_blob.is_a?(File)
51
+ @image = Magick::Image.read(file_or_blob).first
52
+ else
53
+ @image = Magick::Image.from_blob(file_or_blob).first
54
+ end
55
+
56
+ # initialize attrs
57
+ @background_color = @quality = nil
47
58
  end
48
59
 
49
60
  private
50
-
51
61
  def parse_size(size)
52
62
  size.to_s.split("x").map{|i| i.blank? ? nil : i.to_i}
53
63
  end
64
+
65
+ def set_background(color)
66
+ @image = Mugshot::MagickFactory.
67
+ create_canvas(@image.columns, @image.rows, color).
68
+ composite(@image, Magick::NorthWestGravity, Magick::OverCompositeOp)
69
+ end
54
70
  end
@@ -0,0 +1,12 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'RMagick'
3
+
4
+ class Mugshot::MagickFactory
5
+ class << self
6
+
7
+ def create_canvas(columns, rows, color)
8
+ Magick::Image.new(columns, rows){ self.background_color = color }
9
+ end
10
+
11
+ end
12
+ end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ require 'uuid'
2
3
  class Mugshot::Storage
3
4
  protected
4
5
  def asset_id
data/lib/mugshot.rb CHANGED
@@ -1,15 +1,11 @@
1
1
  # -*- encoding: utf-8 -*-
2
- require 'rubygems'
3
- require 'fileutils'
4
- require 'uuid'
5
- require 'active_support'
6
- require 'RMagick'
2
+ require "active_support/core_ext"
7
3
 
8
4
  module Mugshot
5
+ autoload :Image, "mugshot/image"
6
+ autoload :Storage, "mugshot/storage"
7
+ autoload :FSStorage, "mugshot/fs_storage"
8
+ autoload :Application, "mugshot/application"
9
+ autoload :HTTPStorage, "mugshot/http_storage"
10
+ autoload :MagickFactory, "mugshot/magick_factory"
9
11
  end
10
-
11
- require 'mugshot/image'
12
- require 'mugshot/storage'
13
- require 'mugshot/fs_storage'
14
- require 'mugshot/application'
15
- require 'mugshot/proxy'
Binary file
Binary file
@@ -3,17 +3,42 @@ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
3
3
 
4
4
  describe Mugshot::Application do
5
5
  before :each do
6
- @storage = mock(Mugshot::Storage)
6
+ @storage = stub(Mugshot::Storage, :null_object => true)
7
+ @image = stub(Mugshot::Image, :null_object => true)
8
+ @storage.stub!(:read).with("image_id").and_return(@image)
9
+
7
10
  def app
8
11
  Mugshot::Application.new(@storage)
9
12
  end
10
13
  end
11
14
 
15
+ describe "default values" do
16
+ it "should accept default value for quality" do
17
+ def app
18
+ Mugshot::Application.new(:storage => @storage, :quality => 42)
19
+ end
20
+
21
+ @image.should_receive(:quality!).with("42")
22
+
23
+ get "/image_id/any_name.jpg"
24
+ end
25
+
26
+ it "should accept default value for background" do
27
+ def app
28
+ Mugshot::Application.new(:storage => @storage, :background => :blue)
29
+ end
30
+
31
+ @image.should_receive(:background!).with("blue")
32
+
33
+ get "/image_id/any_name.jpg"
34
+ end
35
+ end
36
+
12
37
  describe "POST /" do
13
38
  it "should create image" do
14
39
  file_read = nil
15
40
  File.open("spec/files/test.jpg") {|f| file_read = f.read}
16
- @storage.should_receive(:write).with(file_read)
41
+ @storage.should_receive(:write).with(file_read).and_return("batata")
17
42
 
18
43
  post "/", "file" => Rack::Test::UploadedFile.new("spec/files/test.jpg", "image/jpeg")
19
44
 
@@ -27,71 +52,63 @@ describe Mugshot::Application do
27
52
 
28
53
  last_response.body.should == "batata"
29
54
  end
30
- end
31
55
 
32
- shared_examples_for 'any GET image' do
33
- before :each do
34
- @image = mock(Mugshot::Image, :null_object => true)
35
- @storage.stub!(:read).with("image_id").and_return(@image)
36
- end
56
+ it "should halt 405 when storage doesn't allow writing to" do
57
+ @storage.stub!(:write).and_return(nil)
37
58
 
38
- it "should destroy image" do
39
- @image.should_receive(:destroy!)
59
+ post "/", "file" => Rack::Test::UploadedFile.new("spec/files/test.jpg", "image/jpeg")
60
+
61
+ last_response.status.should == 405
62
+ end
63
+ end
64
+
65
+ describe "GET /:ops/:ops_params/:id/:name.:format" do
66
+ it "should perform operations on image" do
67
+ @image.should_receive(:resize!).with("140x140")
68
+ @image.should_receive(:crop!).with("140x105")
69
+ @image.should_receive(:quality!).with("70")
70
+ @image.should_receive(:background!).with("red")
40
71
 
41
- perform_get
72
+ get "/background/red/resize/140x140/crop/140x105/quality/70/image_id/any_name.jpg"
42
73
  end
43
74
 
44
75
  it "should return image" do
45
76
  @image.stub!(:to_blob).and_return("image data")
46
77
 
47
- perform_get
78
+ get "/crop/140x105/image_id/any_name.jpg"
48
79
 
49
80
  last_response.should be_ok
50
81
  last_response.body.should == "image data"
51
82
  end
52
83
 
84
+ it "should destroy image" do
85
+ @image.should_receive(:destroy!)
86
+ get "/image_id/any_name.jpg"
87
+ end
88
+
53
89
  it "should halt 404 when image doesn't exist" do
54
90
  @storage.stub!(:read).with("image_id").and_return(nil)
55
91
 
56
- perform_get
92
+ get "/crop/140x105/image_id/any_name.jpg"
57
93
 
58
94
  last_response.should be_not_found
59
95
  last_response.body.should be_empty
60
96
  end
61
- end
62
-
63
- describe "GET /:size/:id.:format" do
64
- def perform_get
65
- get "/200x200/image_id.jpg"
66
- end
67
-
68
- it_should_behave_like 'any GET image'
69
-
70
- it "should resize image" do
71
- @image.should_receive(:resize!).with("200x200")
72
-
73
- perform_get
74
- end
75
- end
76
-
77
- describe "GET /:ops/:ops_params/:id.:format" do
78
- def perform_get
79
- get "/crop/140x105/image_id.jpg"
80
- end
81
97
 
82
- it_should_behave_like 'any GET image'
98
+ it "should halt 404 on operations that are not allowed" do
99
+ @image.should_not_receive(:operation!)
83
100
 
84
- it "should perform operations on image" do
85
- @image.should_receive(:resize!).with("140x140")
86
- @image.should_receive(:crop!).with("140x105")
101
+ get "/operation/140x105/image_id/any_name.jpg"
87
102
 
88
- get "/resize/140x140/crop/140x105/image_id.jpg"
103
+ last_response.should be_not_found
104
+ last_response.body.should be_empty
89
105
  end
106
+
107
+ it "should halt 404 on URL with invalid operation/param pair" do
108
+ get "/140x105/image_id/any_name.jpg"
90
109
 
91
- it "should halt 404 on operations that are not allowed" do
92
- @image.should_not_receive(:operation!)
93
-
94
- get "/operation/140x105/image_id.jpg"
110
+ last_response.should be_not_found
111
+ last_response.body.should be_empty
95
112
  end
96
113
  end
97
114
 
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
4
+ require 'fakeweb'
5
+
6
+ describe Mugshot::HTTPStorage do
7
+ before :each do
8
+ @storage = Mugshot::HTTPStorage.new{|id| "http://foo.com:123/any/#{id}/abc.jpg"}
9
+ end
10
+
11
+ describe 'reading' do
12
+ before :each do
13
+ FakeWeb.register_uri(:get, 'http://foo.com:123/any/image_id/abc.jpg', :body => "blob")
14
+ FakeWeb.register_uri(:get, 'http://foo.com:123/any/does_not_exist/abc.jpg', :status => 404)
15
+ end
16
+
17
+ it "should return image" do
18
+ Mugshot::Image.should_receive(:new).with("blob").and_return("image")
19
+
20
+ @storage.read("image_id").should == "image"
21
+ end
22
+
23
+ it "should return nil if image does not exist in the other mugshot" do
24
+ @storage.read("does_not_exist").should be_nil
25
+ end
26
+ end
27
+
28
+ describe 'writing' do
29
+ it "should return nil" do
30
+ @storage.write('').should be_nil
31
+ end
32
+ end
33
+ end
@@ -3,59 +3,51 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
3
 
4
4
  describe Mugshot::Image do
5
5
  before :each do
6
- @magick_image = mock(Magick::Image)
7
- Magick::Image.stub!(:read).and_return([@magick_image])
6
+ @magick_image = mock(Magick::Image, :null_object => true, :columns => 100, :rows => 100)
7
+ @magick_image.instance_eval do # TODO: Explain it
8
+ def to_blob(&block)
9
+ block.call if block.present?
10
+ return 'blob_data'
11
+ end
12
+ end
8
13
 
14
+ Magick::Image.stub!(:read).and_return([@magick_image])
15
+
9
16
  @image = Mugshot::Image.new(File.open("spec/files/test.jpg"))
10
17
  end
11
-
12
- it 'should return image width and height' do
13
- @magick_image.stub!(:columns).and_return(100)
14
- @magick_image.stub!(:rows).and_return(200)
15
-
16
- @image.width.should == 100
17
- @image.height.should == 200
18
+
19
+ it 'can be initialized with blob' do
20
+ Magick::Image.should_receive(:from_blob).and_return([@magick_image])
21
+ Mugshot::Image.new("blob")
18
22
  end
19
23
 
20
24
  describe "to blob" do
21
- before(:each) do
22
- @magick_image.stub!(:strip!)
23
- @magick_image.instance_eval do
24
- def to_blob(&block)
25
- block.call if block.present?
26
- return 'blob_data'
27
- end
28
- end
29
- end
30
-
31
25
  it 'should return image as a blob using default options' do
32
26
  @image.to_blob.should == 'blob_data'
33
27
  end
34
28
 
35
- it 'should return image as a blob using quality' do
36
- @image.should_receive(:quality=).with(75)
37
- @magick_image.instance_eval do
38
- def to_blob(&block)
39
- yield
40
- return 'blob_data'
41
- end
42
- end
43
-
44
- @image.to_blob(:quality => 75).should == 'blob_data'
45
- end
46
-
47
29
  it 'should return image as a blob using format' do
48
30
  @image.should_receive(:format=).with('png')
49
- @magick_image.instance_eval do
50
- def to_blob(&block)
51
- yield
52
- return 'blob_data'
53
- end
54
- end
55
-
56
31
  @image.to_blob(:format => :png).should == 'blob_data'
57
32
  end
33
+ end
34
+
35
+ it 'should return image as a blob using quality' do
36
+ @image.should_receive(:quality=).with(75)
58
37
 
38
+ @image.quality!("75")
39
+ @image.to_blob
40
+ end
41
+
42
+ it 'should set background for transparent images' do
43
+ canvas = mock(Magick::Image)
44
+ Mugshot::MagickFactory.stub!(:create_canvas).and_return(canvas)
45
+ canvas.should_receive(:composite).
46
+ with(@magick_image, Magick::NorthWestGravity, Magick::OverCompositeOp).
47
+ and_return(@magick_image)
48
+
49
+ @image.background!("gray")
50
+ @image.to_blob
59
51
  end
60
52
 
61
53
  it "should resize image to given width and height" do
@@ -84,3 +76,4 @@ describe Mugshot::Image do
84
76
  end
85
77
 
86
78
  end
79
+
@@ -0,0 +1,13 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
+
4
+ describe Mugshot::MagickFactory do
5
+
6
+ it "should create a canvas given columns, rows and background color" do
7
+ canvas = Mugshot::MagickFactory.create_canvas(100, 200, 'red')
8
+ canvas.columns.should == 100
9
+ canvas.rows.should == 200
10
+ canvas.background_color == 'red'
11
+ end
12
+
13
+ end
data/spec/spec.opts CHANGED
@@ -1,5 +1,2 @@
1
1
  --color
2
- --format nested
3
- --loadby mtime
4
- --reverse
5
- --backtrace
2
+ --format progress
data/spec/spec_helper.rb CHANGED
@@ -1,17 +1,29 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ require "rubygems"
2
3
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
4
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
-
5
- require 'rubygems'
6
5
  require 'mugshot'
7
6
 
8
- require 'spec'
9
- require 'spec/autorun'
7
+ require 'rspec'
8
+ require 'rspec/autorun'
10
9
  require 'rack/test'
11
10
  require 'pp'
12
11
 
13
- Spec::Runner::QuietBacktraceTweaker::IGNORE_PATTERNS << /\/Library\//
12
+ def in_editor?
13
+ ENV.has_key?('TM_MODE') || ENV.has_key?('EMACS') || ENV.has_key?('VIM')
14
+ end
15
+
16
+ # Requires supporting files with custom matchers and macros, etc,
17
+ # in ./support/ and its subdirectories.
18
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
14
19
 
15
- Spec::Runner.configure do |config|
20
+ Rspec.configure do |config|
21
+ require 'rspec/expectations'
22
+ config.include Rspec::Matchers
16
23
  config.include Rack::Test::Methods
24
+
25
+ config.mock_framework = :rspec
26
+ config.filter_run :focused => true
27
+ config.run_all_when_everything_filtered = true
28
+ config.color_enabled = !in_editor?
17
29
  end
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mugshot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 4
8
+ - 0
9
+ version: 0.4.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - "Cain\xC3\xA3 Nunes"
@@ -12,79 +17,137 @@ autorequire:
12
17
  bindir: bin
13
18
  cert_chain: []
14
19
 
15
- date: 2010-02-04 00:00:00 -02:00
20
+ date: 2010-03-01 00:00:00 -03:00
16
21
  default_executable:
17
22
  dependencies:
18
23
  - !ruby/object:Gem::Dependency
19
24
  name: activesupport
20
- type: :runtime
21
- version_requirement:
22
- version_requirements: !ruby/object:Gem::Requirement
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
23
27
  requirements:
24
- - - ">="
28
+ - - ~>
25
29
  - !ruby/object:Gem::Version
26
- version: "0"
27
- version:
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 5
34
+ version: 2.3.5
35
+ type: :runtime
36
+ version_requirements: *id001
28
37
  - !ruby/object:Gem::Dependency
29
38
  name: sinatra
30
- type: :runtime
31
- version_requirement:
32
- version_requirements: !ruby/object:Gem::Requirement
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
33
41
  requirements:
34
42
  - - ">="
35
43
  - !ruby/object:Gem::Version
44
+ segments:
45
+ - 0
46
+ - 9
47
+ - 4
36
48
  version: 0.9.4
37
- version:
49
+ type: :runtime
50
+ version_requirements: *id002
38
51
  - !ruby/object:Gem::Dependency
39
52
  name: rmagick
40
- type: :runtime
41
- version_requirement:
42
- version_requirements: !ruby/object:Gem::Requirement
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
43
55
  requirements:
44
56
  - - ">="
45
57
  - !ruby/object:Gem::Version
58
+ segments:
59
+ - 2
60
+ - 12
61
+ - 2
46
62
  version: 2.12.2
47
- version:
63
+ type: :runtime
64
+ version_requirements: *id003
48
65
  - !ruby/object:Gem::Dependency
49
66
  name: uuid
50
- type: :runtime
51
- version_requirement:
52
- version_requirements: !ruby/object:Gem::Requirement
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
53
69
  requirements:
54
70
  - - ">="
55
71
  - !ruby/object:Gem::Version
72
+ segments:
73
+ - 2
74
+ - 0
75
+ - 2
56
76
  version: 2.0.2
57
- version:
77
+ type: :runtime
78
+ version_requirements: *id004
79
+ - !ruby/object:Gem::Dependency
80
+ name: blankslate
81
+ prerelease: false
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ segments:
87
+ - 2
88
+ - 1
89
+ - 2
90
+ - 3
91
+ version: 2.1.2.3
92
+ type: :runtime
93
+ version_requirements: *id005
58
94
  - !ruby/object:Gem::Dependency
59
95
  name: rspec
96
+ prerelease: false
97
+ requirement: &id006 !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ segments:
102
+ - 2
103
+ - 0
104
+ - 0
105
+ - a8
106
+ version: 2.0.0.a8
60
107
  type: :development
61
- version_requirement:
62
- version_requirements: !ruby/object:Gem::Requirement
108
+ version_requirements: *id006
109
+ - !ruby/object:Gem::Dependency
110
+ name: rcov
111
+ prerelease: false
112
+ requirement: &id007 !ruby/object:Gem::Requirement
63
113
  requirements:
64
114
  - - ">="
65
115
  - !ruby/object:Gem::Version
66
- version: 1.3.0
67
- version:
116
+ segments:
117
+ - 0
118
+ - 9
119
+ - 8
120
+ version: 0.9.8
121
+ type: :development
122
+ version_requirements: *id007
68
123
  - !ruby/object:Gem::Dependency
69
124
  name: cucumber
70
- type: :development
71
- version_requirement:
72
- version_requirements: !ruby/object:Gem::Requirement
125
+ prerelease: false
126
+ requirement: &id008 !ruby/object:Gem::Requirement
73
127
  requirements:
74
128
  - - ">="
75
129
  - !ruby/object:Gem::Version
130
+ segments:
131
+ - 0
132
+ - 6
133
+ - 2
76
134
  version: 0.6.2
77
- version:
135
+ type: :development
136
+ version_requirements: *id008
78
137
  - !ruby/object:Gem::Dependency
79
138
  name: rack-test
80
- type: :development
81
- version_requirement:
82
- version_requirements: !ruby/object:Gem::Requirement
139
+ prerelease: false
140
+ requirement: &id009 !ruby/object:Gem::Requirement
83
141
  requirements:
84
142
  - - ">="
85
143
  - !ruby/object:Gem::Version
144
+ segments:
145
+ - 0
146
+ - 5
147
+ - 1
86
148
  version: 0.5.1
87
- version:
149
+ type: :development
150
+ version_requirements: *id009
88
151
  description: Dead simple image server
89
152
  email:
90
153
  - cainanunes@gmail.com
@@ -102,8 +165,9 @@ files:
102
165
  - lib/mugshot.rb
103
166
  - lib/mugshot/application.rb
104
167
  - lib/mugshot/fs_storage.rb
168
+ - lib/mugshot/http_storage.rb
105
169
  - lib/mugshot/image.rb
106
- - lib/mugshot/proxy.rb
170
+ - lib/mugshot/magick_factory.rb
107
171
  - lib/mugshot/public/crossdomain.xml
108
172
  - lib/mugshot/storage.rb
109
173
  - LICENSE
@@ -121,37 +185,51 @@ required_ruby_version: !ruby/object:Gem::Requirement
121
185
  requirements:
122
186
  - - ">="
123
187
  - !ruby/object:Gem::Version
188
+ segments:
189
+ - 0
124
190
  version: "0"
125
- version:
126
191
  required_rubygems_version: !ruby/object:Gem::Requirement
127
192
  requirements:
128
193
  - - ">="
129
194
  - !ruby/object:Gem::Version
195
+ segments:
196
+ - 0
130
197
  version: "0"
131
- version:
132
198
  requirements: []
133
199
 
134
200
  rubyforge_project:
135
- rubygems_version: 1.3.5
201
+ rubygems_version: 1.3.6
136
202
  signing_key:
137
203
  specification_version: 3
138
204
  summary: Dead simple image server
139
205
  test_files:
140
- - spec/test.html
206
+ - spec/files/firefox_png.png
207
+ - spec/files/test.jpg
208
+ - spec/files/test_png.png
209
+ - spec/mugshot/application_spec.rb
141
210
  - spec/mugshot/fs_storage_spec.rb
142
- - spec/mugshot/proxy_spec.rb
211
+ - spec/mugshot/http_storage_spec.rb
143
212
  - spec/mugshot/image_spec.rb
144
- - spec/mugshot/application_spec.rb
145
- - spec/spec_helper.rb
146
- - spec/files/test.jpg
213
+ - spec/mugshot/magick_factory_spec.rb
147
214
  - spec/spec.opts
148
- - features/retrieve_resized_image.feature
215
+ - spec/spec_helper.rb
216
+ - spec/test.html
217
+ - features/convert_image_format.feature
149
218
  - features/crop_image.feature
150
- - features/retrieve_resized_image_keeping_aspect_ratio.feature
219
+ - features/define_image_quality.feature
220
+ - features/resize_image.feature
221
+ - features/retrieve_image_with_any_name.feature
222
+ - features/set_background.feature
223
+ - features/step_definitions/all_steps.rb
151
224
  - features/support/env.rb
152
- - features/support/files/test.200x200.jpg
153
- - features/support/files/test.crop.300x200.jpg
154
- - features/support/files/test.x200.jpg
225
+ - features/support/files/test-cropped_to_300x200.jpg
226
+ - features/support/files/test-resized_to_200x.jpg
227
+ - features/support/files/test-resized_to_200x200.jpg
228
+ - features/support/files/test-resized_to_x200.jpg
229
+ - features/support/files/test-with_75%_of_compression.jpg
230
+ - features/support/files/test.gif
155
231
  - features/support/files/test.jpg
156
- - features/support/files/test.200x.jpg
157
- - features/step_definitions/all_steps.rb
232
+ - features/support/files/test.png
233
+ - features/support/files/with_alpha_channel-with_a_red_background.jpg
234
+ - features/support/files/with_alpha_channel.png
235
+ - features/upload_image.feature
@@ -1,10 +0,0 @@
1
- Feature: Retrieve resized image
2
-
3
- Scenario: Successful retrieval of resized image
4
- When I upload an image
5
- And I ask for the 200x200 resized image
6
- Then I should get the 200x200 resized image
7
-
8
- Scenario: Image doesn't exist
9
- When I ask for a 200x200 resized image that doesn't exist
10
- Then I should get a 404 response
@@ -1,22 +0,0 @@
1
- Feature: Retrieve resized image keeping aspect ratio
2
-
3
- Scenario: Successful retrieval of resized image from given width
4
- When I upload an image
5
- And I ask for the 200x resized image
6
-
7
- Then I should get the 200x resized image keeping the aspect ratio
8
-
9
- Scenario: Successful retrieval of resized image from given height
10
- When I upload an image
11
- And I ask for the x200 resized image
12
-
13
- Then I should get the x200 resized image keeping the aspect ratio
14
-
15
- Scenario: Image doesn't exist
16
- When I ask for a 200x resized image that doesn't exist
17
-
18
- Then I should get a 404 response
19
-
20
- When I ask for a x200 resized image that doesn't exist
21
-
22
- Then I should get a 404 response
data/lib/mugshot/proxy.rb DELETED
@@ -1,32 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- require 'sinatra/base'
3
- require 'net/http'
4
-
5
- class Mugshot::Proxy < Sinatra::Base
6
- get '/*' do
7
- url = URI.parse "#{@host}/#{params[:splat]}"
8
- res = Net::HTTP.start(url.host, url.port) {|http|
9
- http.get(url.path)
10
- }
11
-
12
- status res.code.to_i
13
-
14
- headers_hash = {}
15
- res.each_header do |k,v|
16
- headers_hash[k] = v unless k.to_s =~ /status/i
17
- end
18
- headers headers_hash
19
-
20
- res.body
21
- end
22
-
23
- before do
24
- halt 405 unless request.get?
25
- end
26
-
27
- protected
28
-
29
- def initialize(host)
30
- @host = host
31
- end
32
- end
@@ -1,67 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
3
-
4
- describe Mugshot::Proxy do
5
- before :each do
6
- def app
7
- Mugshot::Proxy.new("http://localhost:9292")
8
- end
9
- end
10
-
11
- describe 'GET /*' do
12
- before :each do
13
- @http = stub(Net::HTTP, :null_object => true)
14
- @res = stub(Net::HTTPResponse, :null_object => true)
15
- @http.stub!(:get).with('/request/path').and_return(@res)
16
- Net::HTTP.stub!(:start).with("localhost", 9292).and_yield(@http)
17
- end
18
-
19
- it 'should proxy the request to configured host' do
20
- @res.stub!(:body).and_return('body')
21
-
22
- get '/request/path'
23
-
24
- last_response.body.should == 'body'
25
- end
26
-
27
- it 'should return original response headers' do
28
- @res.stub!(:each_header)\
29
- .and_yield('content-type', 'image/jpg')\
30
- .and_yield('cache-control', 'public, max-age=31557600')
31
-
32
- get '/request/path'
33
-
34
- last_response.headers['content-type'].should == 'image/jpg'
35
- last_response.headers['cache-control'].should == 'public, max-age=31557600'
36
- end
37
-
38
- it 'should return original status' do
39
- @res.stub!(:code).and_return('404')
40
-
41
- get '/request/path'
42
-
43
- last_response.status.should == 404
44
- end
45
-
46
- it 'should not return status header' do
47
- @res.stub!(:each_header).and_yield('status', '200')
48
-
49
- get '/request/path'
50
-
51
- last_response.headers.should_not include('status')
52
- end
53
- end
54
-
55
- describe 'other methods' do
56
- it 'should not be allowed' do
57
- post '/request/path'
58
- last_response.status.should == 405
59
-
60
- put '/request/path'
61
- last_response.status.should == 405
62
-
63
- delete '/request/path'
64
- last_response.status.should == 405
65
- end
66
- end
67
- end