mugshot 0.3.1 → 0.4.0

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