├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── config.ru ├── cucumber.yml ├── features ├── convert_image_format.feature ├── crop_image.feature ├── define_image_quality.feature ├── resize_image.feature ├── retrieve_image_with_any_name.feature ├── set_background.feature ├── step_definitions │ └── all_steps.rb ├── support │ ├── env.rb │ └── files │ │ ├── test-cropped_to_300x200.jpg │ │ ├── test-resized_to_200x.jpg │ │ ├── test-resized_to_200x200.jpg │ │ ├── test-resized_to_x200.jpg │ │ ├── test-with_75%_of_compression.jpg │ │ ├── test.gif │ │ ├── test.jpg │ │ ├── test.png │ │ ├── with_alpha_channel-with_a_red_background.jpg │ │ └── with_alpha_channel.png └── upload_image.feature ├── lib ├── mugshot.rb └── mugshot │ ├── application.rb │ ├── fs_storage.rb │ ├── http_storage.rb │ ├── image.rb │ ├── magick_factory.rb │ ├── public │ └── crossdomain.xml │ ├── s3_storage.rb │ ├── storage.rb │ └── version.rb ├── mugshot.gemspec └── spec ├── files ├── firefox_png.png ├── test-upload.jpg ├── test.jpg └── test_png.png ├── mugshot ├── application_spec.rb ├── fs_storage_spec.rb ├── http_storage_spec.rb ├── image_spec.rb ├── magick_factory_spec.rb ├── s3_storage_spec.rb └── storage_spec.rb ├── spec_helper.rb └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | nbproject 21 | .bundle 22 | .rvmrc 23 | 24 | ## PROJECT::SPECIFIC 25 | .document 26 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - 1.9.2 4 | - ree 5 | - rbx 6 | - jruby -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in foo.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mugshot (1.0.0.rc1) 5 | activesupport (>= 2.3.5) 6 | blankslate (>= 2.1.2.3) 7 | fog (>= 0.7.2) 8 | i18n (>= 0.5.0) 9 | rmagick (>= 2.12.2) 10 | sinatra (>= 0.9.4) 11 | uuid (>= 2.0.2) 12 | 13 | GEM 14 | remote: http://rubygems.org/ 15 | specs: 16 | activesupport (3.0.9) 17 | blankslate (2.1.2.4) 18 | builder (3.0.0) 19 | cucumber (0.10.2) 20 | builder (>= 2.1.2) 21 | diff-lcs (>= 1.1.2) 22 | gherkin (>= 2.3.5) 23 | json (>= 1.4.6) 24 | term-ansicolor (>= 1.0.5) 25 | diff-lcs (1.1.2) 26 | excon (0.6.5) 27 | fakeweb (1.3.0) 28 | fog (0.10.0) 29 | builder 30 | excon (~> 0.6.5) 31 | formatador (~> 0.2.0) 32 | mime-types 33 | multi_json (~> 1.0.3) 34 | net-scp (~> 1.0.4) 35 | net-ssh (~> 2.1.4) 36 | nokogiri (~> 1.5.0) 37 | ruby-hmac 38 | formatador (0.2.0) 39 | gherkin (2.3.6) 40 | json (>= 1.4.6) 41 | i18n (0.6.0) 42 | json (1.5.1) 43 | macaddr (1.0.0) 44 | mime-types (1.16) 45 | multi_json (1.0.3) 46 | net-scp (1.0.4) 47 | net-ssh (>= 1.99.1) 48 | net-ssh (2.1.4) 49 | nokogiri (1.5.0) 50 | rack (1.2.2) 51 | rack-test (0.5.7) 52 | rack (>= 1.0) 53 | rmagick (2.13.1) 54 | rspec (2.5.0) 55 | rspec-core (~> 2.5.0) 56 | rspec-expectations (~> 2.5.0) 57 | rspec-mocks (~> 2.5.0) 58 | rspec-core (2.5.1) 59 | rspec-expectations (2.5.0) 60 | diff-lcs (~> 1.1.2) 61 | rspec-mocks (2.5.0) 62 | ruby-hmac (0.4.0) 63 | sinatra (1.2.6) 64 | rack (~> 1.1) 65 | tilt (>= 1.2.2, < 2.0) 66 | term-ansicolor (1.0.5) 67 | tilt (1.3.2) 68 | uuid (2.3.3) 69 | macaddr (~> 1.0) 70 | 71 | PLATFORMS 72 | ruby 73 | 74 | DEPENDENCIES 75 | cucumber (>= 0.6.2) 76 | fakeweb 77 | mugshot! 78 | rack-test (>= 0.5.1) 79 | rspec (>= 2.3.0) 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Globo.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deprecated 2 | ========== 3 | 4 | **Mugshot has been deprecated and is no longer maintained**. 5 | 6 | 7 | Mugshot 8 | ======= 9 | 10 | **Mugshot is a dead simple image server**. 11 | 12 | 13 | Overview 14 | -------- 15 | 16 | The basic idea of Mugshot is that you upload the largest/highest quality images possible. When retrieving the images you apply different operations to it such as: resizing, rounded corners, transparency and anything else we can think of! 17 | 18 | Only the original image is stored on the server. All operations are performed dynamically, which is why caching is so important (see below). 19 | 20 | 21 | Caching 22 | ------- 23 | 24 | Mugshot doesn't cache anything by itself but is designed to play nice with standard HTTP caching. 25 | 26 | For production use, don't even think about using Mugshot without something like Varnish or Squid sitting in front. 27 | 28 | 29 | Using 30 | ----- 31 | 32 | Mugshot provides you with a Sinatra application. You can create a **config.ru** file with these contents to start using Mugshot: 33 | 34 | 35 | Using activeresource (3.0.6) 36 | Then you can run it with: 37 | 38 | $ rackup config.ru 39 | 40 | And access in your browser: 41 | 42 | http://localhost:9292/myimg/some-name.jpg 43 | 44 | This would simply return the image located at /tmp/mugshot/myimg, converting it to a JPEG. Additionaly you can pass some operations to be performed over the image: 45 | 46 | http://localhost:9292/resize/100x100/myimg/some-name.jpg # resizing to 100x100 pixels 47 | http://localhost:9292/resize/100x/myimg/some-name.jpg # resizing to 100 pixels in width maintaining aspect ratio 48 | http://localhost:9292/resize/x100/myimg/some-name.jpg # resizing to 100 pixels in height maintaining aspect ratio 49 | http://localhost:9292/crop/200x150/myimg/some-name.jpg # resize and crop image to 200x150 50 | http://localhost:9292/quality/70/crop/200x150/myimg/some-name.jpg # convert it to JPEG with quality of 70% and resize and crop image to 200x150 51 | http://localhost:9292/background/red/crop/200x150/myimg/some-name.jpg # convert it to JPEG with red background and resize and crop image to 200x150 52 | 53 | Supported operations 54 | -------------------- 55 | 56 | ### Resize 57 | 58 | /resize/WIDTHxHEIGHT/id/name.jpg (ex: http://mugshot.ws/resize/200x100/myid/thumb.jpg) 59 | 60 | ### Resize keeping aspect ratio 61 | 62 | /resize/WIDTHx/id/name.jpg (ex: http://mugshot.ws/resize/200x/myid/thumb.jpg) 63 | 64 | /resize/xHEIGHT/id/name.jpg (ex: http://mugshot.ws/resize/x100/myid/thumb.jpg) 65 | 66 | ### Crop 67 | 68 | /crop/WIDTHxHEIGHT/id/name.jpg (ex: http://mugshot.ws/crop/200x100/myid/thumb.jpg) 69 | 70 | ### Quality 71 | 72 | /quality/QUALITY/id/name.jpg (ex: http://mugshot.ws/quality/70/myid/thumb.jpg) 73 | 74 | ### Background 75 | 76 | /background/COLOR/id/name.jpg (ex: http://mugshot.ws/background/red/myid/thumb.jpg) 77 | 78 | 79 | Configuration 80 | ------------- 81 | 82 | You can further configure your Mugshot::Application when creating it, like so: 83 | 84 | # -*- encoding: utf-8 -*- 85 | require "rubygems" 86 | require "mugshot" 87 | 88 | run Mugshot::Application.new( 89 | :storage => Mugshot::FSStorage.new("/tmp/mugshot"), 90 | :cache_duration => 7.days.to_i, # duration set in cache header (in seconds) 91 | :allowed_sizes => ['640x360', '480x360', '320x240'], # an array with valid sizes for resize and crop operations 92 | :allowed_formats => [:jpg, :png], # an array with the allowed formats 93 | :allowed_names => ['thumb', 'img'], # an array with the allowed names in the URL 94 | :quality_range => 1..100, # the range of allowed values for quality operations 95 | :valid_operations => [:crop, :resize, :quality] # an array with the valid operations 96 | ) 97 | 98 | When using the restrictive configurations any value other than the ones allowed will result in a 400 status code being returned. If no restriction is set then any value can be given, which can lead to DOS attacks. Be careful! 99 | 100 | Development 101 | ----------- 102 | 103 | Clone the repository and run: 104 | 105 | $ bundle 106 | 107 | This will install all dependencies for you. Then you can run the specs and features: 108 | 109 | $ rake spec 110 | $ rake cucumber 111 | 112 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require "rubygems" 3 | require "rake" 4 | require 'rspec/core/rake_task' 5 | require 'cucumber/rake/task' 6 | 7 | task :default => [:spec, :features] 8 | 9 | require 'bundler' 10 | Bundler::GemHelper.install_tasks 11 | 12 | task :default => [:spec, :cucumber] 13 | 14 | RSpec::Core::RakeTask.new :spec 15 | Cucumber::Rake::Task.new(:cucumber) 16 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.unshift(::File.expand_path(::File.join(::File.dirname(__FILE__), "lib"))) 3 | require "mugshot" 4 | 5 | system "mkdir -p /tmp/mugshot" 6 | %w[test.jpg with_alpha_channel.png].each do |filename| 7 | system "cp -f features/support/files/#{filename} /tmp/mugshot/#{filename.split('.').first}" 8 | end 9 | 10 | run Mugshot::Application.new( 11 | :storage => Mugshot::FSStorage.new("/tmp/mugshot") 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --guess -r features/support/env.rb -r features/step_definitions 2 | -------------------------------------------------------------------------------- /features/convert_image_format.feature: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /features/crop_image.feature: -------------------------------------------------------------------------------- 1 | Feature: Crop image 2 | 3 | Scenario: Crop image 4 | Given an image 5 | 6 | When I ask for it cropped to 300x200 7 | 8 | Then I should get it cropped to 300x200 9 | -------------------------------------------------------------------------------- /features/define_image_quality.feature: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /features/resize_image.feature: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /features/retrieve_image_with_any_name.feature: -------------------------------------------------------------------------------- 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 10 | -------------------------------------------------------------------------------- /features/set_background.feature: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /features/step_definitions/all_steps.rb: -------------------------------------------------------------------------------- 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 | 22 | When /^I upload an image$/ do 23 | post '/', "file" => Rack::Test::UploadedFile.new("features/support/files/test.jpg", "image/jpeg") 24 | @image_id = last_response.body 25 | end 26 | 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 59 | end 60 | 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 64 | end 65 | 66 | Then /^I should get it$/ do 67 | @retrieved_image.should be_same_image_as("test.jpg") 68 | end 69 | 70 | Then /^I should get a (\d+) response$/ do |response_code| 71 | last_response.status.should == response_code.to_i 72 | end 73 | 74 | Then /^I should get it with format (.*)$/ do |expected_format| 75 | @retrieved_image.should be_same_image_as("test.#{expected_format}") 76 | end 77 | 78 | Then /^I should get it (.*)$/ do |name| 79 | @retrieved_image.should be_same_image_as("#{@image[:name].gsub(' ', '_')}-#{name.underscore.gsub(' ', '_')}.jpg") 80 | end 81 | 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 86 | end 87 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 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')) 5 | require 'mugshot' 6 | 7 | require 'rack/test' 8 | require 'rspec/expectations' 9 | require 'pp' 10 | 11 | require 'RMagick' 12 | 13 | module CucumberWorld 14 | include RSpec::Matchers 15 | include Rack::Test::Methods 16 | 17 | mattr_accessor :storage 18 | 19 | def app 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/"))) 26 | end 27 | end 28 | World(CucumberWorld) 29 | 30 | Before do 31 | @images_by_name = HashWithIndifferentAccess.new 32 | CucumberWorld.storage = Mugshot::FSStorage.new('/tmp/mugshot/cucumber') 33 | end 34 | 35 | After do 36 | require 'fileutils' 37 | FileUtils.rm_rf("/tmp/mugshot/cucumber") 38 | end 39 | 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) 43 | 44 | actual.columns == expected.columns && 45 | actual.rows == expected.rows && 46 | actual.difference(expected)[1] < 0.01 47 | end 48 | end 49 | 50 | -------------------------------------------------------------------------------- /features/support/files/test-cropped_to_300x200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test-cropped_to_300x200.jpg -------------------------------------------------------------------------------- /features/support/files/test-resized_to_200x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test-resized_to_200x.jpg -------------------------------------------------------------------------------- /features/support/files/test-resized_to_200x200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test-resized_to_200x200.jpg -------------------------------------------------------------------------------- /features/support/files/test-resized_to_x200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test-resized_to_x200.jpg -------------------------------------------------------------------------------- /features/support/files/test-with_75%_of_compression.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test-with_75%_of_compression.jpg -------------------------------------------------------------------------------- /features/support/files/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test.gif -------------------------------------------------------------------------------- /features/support/files/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test.jpg -------------------------------------------------------------------------------- /features/support/files/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/test.png -------------------------------------------------------------------------------- /features/support/files/with_alpha_channel-with_a_red_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/with_alpha_channel-with_a_red_background.jpg -------------------------------------------------------------------------------- /features/support/files/with_alpha_channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/features/support/files/with_alpha_channel.png -------------------------------------------------------------------------------- /features/upload_image.feature: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /lib/mugshot.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require "active_support/core_ext" 3 | 4 | module Mugshot 5 | autoload :MagickFactory, "mugshot/magick_factory" 6 | autoload :Image, "mugshot/image" 7 | 8 | autoload :Storage, "mugshot/storage" 9 | autoload :FSStorage, "mugshot/fs_storage" 10 | autoload :S3Storage, "mugshot/s3_storage" 11 | autoload :HTTPStorage, "mugshot/http_storage" 12 | 13 | autoload :Application, "mugshot/application" 14 | end 15 | -------------------------------------------------------------------------------- /lib/mugshot/application.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'sinatra/base' 3 | 4 | class Mugshot::Application < Sinatra::Base 5 | 6 | DEFAULT_VALID_OPERATIONS = %w[background crop resize quality] 7 | 8 | set :static, true 9 | set :public, ::File.expand_path(::File.join(::File.dirname(__FILE__), "public")) 10 | 11 | get '/?' do 12 | 'ok' 13 | end 14 | 15 | post '/?' do 16 | id = @storage.write(params['file'][:tempfile].read) 17 | halt 405 if id.blank? 18 | id 19 | end 20 | 21 | before '/*/?:id/:name.:format' do |splat, id, name, format| 22 | @operations = operations_from_splat(splat) 23 | check_format(format) 24 | check_name(name) 25 | check_operations 26 | end 27 | 28 | get '/*/?:id/:name.:format' do |splat, id, _, format| 29 | image = @storage.read(id) 30 | 31 | if image.blank? 32 | response['Cache-Control'] = "public, max-age=#{1.minute}" 33 | halt 404 34 | end 35 | 36 | begin 37 | process_operations(image, @default_operations) 38 | process_operations(image, @operations) 39 | response['Cache-Control'] = "public, max-age=#{@cache_duration}" 40 | send_image(image, format.to_sym) 41 | ensure 42 | image.destroy! 43 | end 44 | end 45 | 46 | protected 47 | 48 | def initialize(opts) 49 | opts = {:storage => opts} if opts.kind_of?(Mugshot::Storage) 50 | opts.to_options! 51 | 52 | @storage = opts.delete(:storage) 53 | @cache_duration = opts.delete(:cache_duration) || 1.year.to_i 54 | @valid_operations = (opts.delete(:valid_operations) || DEFAULT_VALID_OPERATIONS).map(&:to_s) 55 | @quality_range = opts.delete(:quality_range) 56 | @allowed_sizes = opts.delete(:allowed_sizes) 57 | @allowed_formats = opts.delete(:allowed_formats) 58 | @allowed_names = opts.delete(:allowed_names) 59 | 60 | @default_operations = opts 61 | 62 | super(opts) 63 | end 64 | 65 | private 66 | 67 | def operations_from_splat(splat) 68 | operations = [] 69 | begin 70 | operations = Hash[*splat.split('/')] 71 | operations.assert_valid_keys(@valid_operations) 72 | rescue 73 | halt 400 74 | end 75 | operations 76 | end 77 | 78 | def check_format(format) 79 | halt 400 unless valid_format?(format) 80 | end 81 | 82 | def valid_format?(format) 83 | @allowed_formats.blank? || @allowed_formats.map(&:to_s).include?(format) 84 | end 85 | 86 | def check_name(name) 87 | halt 400 unless valid_name?(name) 88 | end 89 | 90 | def valid_name?(name) 91 | @allowed_names.blank? || @allowed_names.map(&:to_s).include?(name) 92 | end 93 | 94 | def check_operations 95 | halt 400 unless valid_quality_operation? && valid_resize_operation? && valid_crop_operation? 96 | end 97 | 98 | def valid_quality_operation? 99 | !@operations.has_key?("quality") || @quality_range.blank? || @quality_range.include?(@operations["quality"].to_i) 100 | end 101 | 102 | def valid_resize_operation? 103 | valid_operation?("resize", @allowed_sizes) 104 | end 105 | 106 | def valid_crop_operation? 107 | valid_operation?("crop", @allowed_sizes) 108 | end 109 | 110 | def valid_operation?(operation, range_or_array) 111 | !@operations.has_key?(operation) || range_or_array.blank? || range_or_array.include?(@operations[operation]) 112 | end 113 | 114 | def process_operations(image, operations) 115 | operations.each do |op, op_params| 116 | image.send("#{op}!", op_params.to_s) 117 | end 118 | end 119 | 120 | def send_image(image, format) 121 | content_type format 122 | response['Content-Disposition'] = 'inline' 123 | image.to_blob(:format => format) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/mugshot/fs_storage.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'fileutils' 3 | class Mugshot::FSStorage < Mugshot::Storage 4 | def write(bin) 5 | id = asset_id 6 | path = id_to_path(id) 7 | FileUtils.mkdir_p(File.dirname(File.join(@root_path, path))) 8 | File.open(File.join(@root_path, path), "w") do |fw| 9 | fw.write(bin) 10 | end 11 | id 12 | end 13 | 14 | def read(id) 15 | path = id_to_path(id) 16 | file = File.join(@root_path, path) 17 | return nil unless File.exist? file 18 | Mugshot::Image.new File.open(file) 19 | end 20 | 21 | protected 22 | 23 | def initialize(root_path) 24 | @root_path = root_path 25 | FileUtils.mkdir_p(root_path) 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/mugshot/http_storage.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'fileutils' 3 | require 'net/http' 4 | 5 | class Mugshot::HTTPStorage < Mugshot::Storage 6 | 7 | def read(id) 8 | url = URI.parse(@url_resolver.call(id)) 9 | res = Net::HTTP.start(url.host, url.port) {|http| http.get(url.path)} 10 | 11 | return nil unless res.code.to_i == 200 12 | 13 | Mugshot::Image.new(res.body) 14 | end 15 | 16 | protected 17 | 18 | def initialize(&block) 19 | @url_resolver = block 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/mugshot/image.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'RMagick' 3 | class Mugshot::Image 4 | 5 | def background!(color) 6 | @background_color = color 7 | end 8 | 9 | def resize!(size) 10 | w, h = parse_size(size) 11 | if [w, h].include?(nil) 12 | @image.resize_to_fit! w, h 13 | else 14 | @image.resize! w, h 15 | end 16 | 17 | self 18 | end 19 | 20 | def crop!(size) 21 | w, h = parse_size(size) 22 | @image.resize_to_fill! w, h 23 | self 24 | end 25 | 26 | def quality!(quality) 27 | @quality = quality.to_i 28 | self 29 | end 30 | 31 | def destroy! 32 | @image.destroy! 33 | self 34 | end 35 | 36 | def to_blob(opts = {}) 37 | opts.merge!(:quality => @quality) 38 | 39 | set_background(@background_color) if !!@background_color 40 | 41 | @image.strip! 42 | @image.to_blob do 43 | self.format = opts[:format].to_s if opts.include?(:format) 44 | self.quality = opts[:quality] if opts[:quality].present? 45 | end 46 | end 47 | 48 | protected 49 | 50 | def initialize(file_or_blob) 51 | if file_or_blob.is_a?(File) 52 | @image = Magick::Image.read(file_or_blob).first 53 | else 54 | @image = Magick::Image.from_blob(file_or_blob).first 55 | end 56 | 57 | # initialize attrs 58 | @background_color = @quality = nil 59 | end 60 | 61 | private 62 | 63 | def parse_size(size) 64 | size.to_s.split("x").map{|i| i.blank? ? nil : i.to_i} 65 | end 66 | 67 | def set_background(color) 68 | @image = Mugshot::MagickFactory. 69 | create_canvas(@image.columns, @image.rows, color). 70 | composite(@image, Magick::NorthWestGravity, Magick::OverCompositeOp) 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/mugshot/magick_factory.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'RMagick' 3 | 4 | class Mugshot::MagickFactory 5 | def self.create_canvas(columns, rows, color) 6 | Magick::Image.new(columns, rows){ self.background_color = color } 7 | end 8 | end -------------------------------------------------------------------------------- /lib/mugshot/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/mugshot/s3_storage.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'fog' 3 | 4 | class Mugshot::S3Storage < Mugshot::Storage 5 | 6 | HEADERS = {'x-amz-acl' => 'public-read'} 7 | 8 | def write(bin) 9 | id = asset_id 10 | path = id_to_path(id) 11 | @storage.put_object(@bucket_name, path, bin, HEADERS) 12 | id 13 | end 14 | 15 | def read(id) 16 | path = id_to_path(id) 17 | Mugshot::Image.new(@storage.get_object(@bucket_name, path).body) 18 | rescue Excon::Errors::NotFound 19 | nil 20 | end 21 | 22 | protected 23 | 24 | def initialize(opts) 25 | opts.to_options! 26 | opts.assert_valid_keys(:bucket_name, :access_key_id, :secret_access_key) 27 | 28 | @bucket_name = opts[:bucket_name] 29 | 30 | @storage = Fog::Storage.new(:provider => 'AWS', :aws_access_key_id => opts[:access_key_id], :aws_secret_access_key => opts[:secret_access_key]) 31 | @storage.put_bucket(@bucket_name) 32 | end 33 | 34 | end 35 | 36 | 37 | -------------------------------------------------------------------------------- /lib/mugshot/storage.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'uuid' 3 | class Mugshot::Storage 4 | 5 | def write(bin) 6 | nil 7 | end 8 | 9 | protected 10 | 11 | def asset_id 12 | UUID.generate :compact 13 | end 14 | 15 | def id_to_path(id) 16 | path = id.to_s.clone 17 | 14.times do |i| 18 | path = path.insert(2 + (i * 2) + i, '/') 19 | end 20 | path 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/mugshot/version.rb: -------------------------------------------------------------------------------- 1 | module Mugshot 2 | VERSION = "1.0.0.rc2" 3 | end 4 | 5 | -------------------------------------------------------------------------------- /mugshot.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "mugshot/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "mugshot" 7 | s.version = Mugshot::VERSION 8 | s.authors = ["Caina Nunes", "Fabricio Lopes", "Guilherme Cirne", "Jose Peleteiro", "Vicente Mundim", "Anselmo Alves"] 9 | s.email = ["cainanunes@gmail.com", "fabriciolopesvital@gmail.com", "gcirne@gmail.com", "jose@peleteiro.net", "vicente.mundim@gmail.com", "me@anselmoalves.com"] 10 | s.homepage = %q{http://mugshot.ws} 11 | s.summary = %q{Dead simple image server} 12 | s.description = %q{The basic idea of Mugshot is that you upload the largest/highest quality images possible. When retrieving the images you apply different operations to it such as: resizing, rounded corners, transparency and anything else we can think of!} 13 | s.date = %q{2011-04-25} 14 | 15 | s.rubyforge_project = "mugshot" 16 | 17 | s.rdoc_options = ["--charset=UTF-8"] 18 | s.extra_rdoc_files = ["LICENSE", "README.md"] 19 | 20 | s.files = `git ls-files -- lib/*`.split("\n") 21 | s.test_files = `git ls-files -- {spec,features}/*`.split("\n") 22 | s.require_paths = ["lib"] 23 | 24 | s.add_dependency(%q, [">= 2.3.5"]) 25 | s.add_dependency(%q, [">= 0.5.0"]) 26 | s.add_dependency(%q, [">= 2.12.2"]) 27 | s.add_dependency(%q, [">= 2.0.2"]) 28 | s.add_dependency(%q, [">= 2.1.2.3"]) 29 | s.add_dependency(%q, [">= 0.9.4"]) 30 | s.add_dependency(%q, [">= 0.7.2"]) 31 | 32 | s.add_development_dependency(%q, [">= 0"]) 33 | s.add_development_dependency(%q, [">= 2.3.0"]) 34 | s.add_development_dependency(%q, [">= 0.6.2"]) 35 | s.add_development_dependency(%q, [">= 0.5.1"]) 36 | end 37 | -------------------------------------------------------------------------------- /spec/files/firefox_png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/spec/files/firefox_png.png -------------------------------------------------------------------------------- /spec/files/test-upload.jpg: -------------------------------------------------------------------------------- 1 | image 2 | -------------------------------------------------------------------------------- /spec/files/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/spec/files/test.jpg -------------------------------------------------------------------------------- /spec/files/test_png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/mugshot/914192bac747273b436ca4c35fe445591e30c4e6/spec/files/test_png.png -------------------------------------------------------------------------------- /spec/mugshot/application_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path(File.dirname(__FILE__) + "/../spec_helper") 3 | 4 | describe Mugshot::Application do 5 | before :each do 6 | @storage = stub(Mugshot::Storage, :kind_of? => true).as_null_object 7 | @image = stub(Mugshot::Image, :blank? => false).as_null_object 8 | @storage.stub!(:read).with("image_id").and_return(@image) 9 | 10 | def app 11 | Mugshot::Application.new(@storage) 12 | end 13 | end 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 | 37 | describe "POST /" do 38 | it "should create image" do 39 | file_read = nil 40 | File.open("spec/files/test-upload.jpg") {|f| file_read = f.read} 41 | @storage.should_receive(:write).with(file_read).and_return("batata") 42 | post "/", "file" => Rack::Test::UploadedFile.new("spec/files/test-upload.jpg", "image/jpeg") 43 | 44 | last_response.status.should == 200 45 | end 46 | 47 | it "should return image id" do 48 | @storage.stub!(:write).and_return("batata") 49 | 50 | post "/", "file" => Rack::Test::UploadedFile.new("spec/files/test.jpg", "image/jpeg") 51 | 52 | last_response.body.should == "batata" 53 | end 54 | 55 | it "should halt 405 when storage doesn't allow writing to" do 56 | @storage.stub!(:write).and_return(nil) 57 | 58 | post "/", "file" => Rack::Test::UploadedFile.new("spec/files/test.jpg", "image/jpeg") 59 | 60 | last_response.status.should == 405 61 | end 62 | end 63 | 64 | describe "GET /:ops/:ops_params/:id/:name.:format" do 65 | it "should perform operations on image" do 66 | @image.should_receive(:resize!).with("140x140") 67 | @image.should_receive(:crop!).with("140x105") 68 | @image.should_receive(:quality!).with("70") 69 | @image.should_receive(:background!).with("red") 70 | 71 | get "/background/red/resize/140x140/crop/140x105/quality/70/image_id/any_name.jpg" 72 | end 73 | 74 | it "should return image" do 75 | @image.stub!(:to_blob).and_return("image data") 76 | 77 | get "/crop/140x105/image_id/any_name.jpg" 78 | 79 | last_response.should be_ok 80 | last_response.body.should == "image data" 81 | end 82 | 83 | it "should destroy image" do 84 | @image.should_receive(:destroy!) 85 | get "/image_id/any_name.jpg" 86 | end 87 | 88 | it "should halt 404 when image doesn't exist" do 89 | @storage.stub!(:read).with("image_id").and_return(nil) 90 | 91 | get "/crop/140x105/image_id/any_name.jpg" 92 | 93 | last_response.should be_not_found 94 | last_response.body.should be_empty 95 | end 96 | 97 | it "should halt 400 on operations that are not allowed" do 98 | @image.should_not_receive(:operation!) 99 | 100 | get "/operation/140x105/image_id/any_name.jpg" 101 | 102 | last_response.status.should == 400 103 | last_response.body.should be_empty 104 | end 105 | 106 | it "should halt 400 on URL with invalid operation/param pair" do 107 | get "/140x105/image_id/any_name.jpg" 108 | 109 | last_response.status.should == 400 110 | last_response.body.should be_empty 111 | end 112 | end 113 | 114 | describe "GET /" do 115 | it "should return ok as healthcheck" do 116 | get "/" 117 | 118 | last_response.should be_ok 119 | last_response.body.should == "ok" 120 | end 121 | end 122 | 123 | describe "cache" do 124 | it "should cache response with max age of 1 day" do 125 | @image.stub!(:to_blob).and_return("image data") 126 | get "/crop/140x105/image_id/any_name.jpg" 127 | last_response.headers["Cache-Control"].should == "public, max-age=31557600" 128 | end 129 | end 130 | 131 | describe "configuration" do 132 | describe "cache duration" do 133 | it "should use the configured cache duration" do 134 | def app 135 | Mugshot::Application.new(:storage => @storage, :cache_duration => 3.days.to_i) 136 | end 137 | 138 | @image.stub!(:to_blob).and_return("image data") 139 | get "/crop/140x105/image_id/any_name.jpg" 140 | last_response.headers["Cache-Control"].should == "public, max-age=#{3.days.to_i}" 141 | end 142 | end 143 | 144 | describe "valid operations" do 145 | it "should allow a valid operation" do 146 | def app 147 | Mugshot::Application.new(:storage => @storage, :valid_operations => ["crop", "resize"]) 148 | end 149 | 150 | @image.stub!(:to_blob).and_return("image data") 151 | 152 | get "/resize/200x100/image_id/any_name.jpg" 153 | last_response.should be_ok 154 | last_response.body.should == "image data" 155 | end 156 | 157 | it "should halt with a 400 (Bad Request) when an invalid operation is given" do 158 | def app 159 | Mugshot::Application.new(:storage => @storage, :valid_operations => ["crop", "resize"]) 160 | end 161 | 162 | get "/quality/50/image_id/any_name.jpg" 163 | last_response.status.should == 400 164 | end 165 | end 166 | 167 | describe "quality range" do 168 | it "should allow quality operations with values in the configured range" do 169 | def app 170 | Mugshot::Application.new(:storage => @storage, :quality_range => 1..200) 171 | end 172 | 173 | @image.stub!(:to_blob).and_return("image data") 174 | 175 | 1.upto(200) do |quality| 176 | get "/quality/#{quality}/image_id/any_name.jpg" 177 | last_response.should be_ok 178 | last_response.body.should == "image data" 179 | end 180 | end 181 | 182 | it "should allow quality operations when no range is configured" do 183 | @image.stub!(:to_blob).and_return("image data") 184 | 185 | 1.upto(300) do |quality| 186 | get "/quality/#{quality}/image_id/any_name.jpg" 187 | last_response.should be_ok 188 | last_response.body.should == "image data" 189 | end 190 | end 191 | 192 | it "should halt with a 400 (Bad Request) quality operations with values outside the configured range" do 193 | def app 194 | Mugshot::Application.new(:storage => @storage, :quality_range => 1..200) 195 | end 196 | 197 | get "/quality/0/image_id/any_name.jpg" 198 | last_response.status.should == 400 199 | 200 | get "/quality/201/image_id/any_name.jpg" 201 | last_response.status.should == 400 202 | end 203 | end 204 | 205 | describe "allowed sizes" do 206 | def allowed_sizes 207 | ['640x480', '640x360', '480x360', '320x240'] 208 | end 209 | 210 | %w{resize crop}.each do |operation| 211 | it "should allow #{operation} operations for configured values" do 212 | def app 213 | Mugshot::Application.new(:storage => @storage, :allowed_sizes => allowed_sizes) 214 | end 215 | 216 | @image.stub!(:to_blob).and_return("image data") 217 | 218 | allowed_sizes.each do |size| 219 | get "/#{operation}/#{size}/image_id/any_name.jpg" 220 | last_response.should be_ok 221 | last_response.body.should == "image data" 222 | end 223 | end 224 | 225 | it "should allow #{operation} operations when allowed sizes is not configured" do 226 | def app 227 | Mugshot::Application.new(:storage => @storage) 228 | end 229 | 230 | @image.stub!(:to_blob).and_return("image data") 231 | 232 | ['300x200', '400x250'].each do |size| 233 | get "/#{operation}/#{size}/image_id/any_name.jpg" 234 | last_response.should be_ok 235 | last_response.body.should == "image data" 236 | end 237 | end 238 | 239 | it "should halt with a 400 (Bad Request) #{operation} operations with a not allowed size" do 240 | def app 241 | Mugshot::Application.new(:storage => @storage, :allowed_sizes => allowed_sizes) 242 | end 243 | 244 | get "/#{operation}/300x200/image_id/any_name.jpg" 245 | last_response.status.should == 400 246 | 247 | get "/#{operation}/480x300/image_id/any_name.jpg" 248 | last_response.status.should == 400 249 | end 250 | end 251 | end 252 | 253 | describe "allowed formats" do 254 | it "should allow valid formats" do 255 | def app 256 | Mugshot::Application.new(:storage => @storage, :allowed_formats => ["jpg", "png"]) 257 | end 258 | 259 | @image.stub!(:to_blob).and_return("image data") 260 | 261 | ["jpg", "png"].each do |format| 262 | get "/image_id/any_name.#{format}" 263 | last_response.should be_ok 264 | last_response.body.should == "image data" 265 | end 266 | end 267 | 268 | it "should allow any format when no allowed format is configured" do 269 | @image.stub!(:to_blob).and_return("image data") 270 | 271 | get "/image_id/any_name.tiff" 272 | last_response.should be_ok 273 | last_response.body.should == "image data" 274 | end 275 | 276 | it "should halt with a 400 (Bad Request) when an invalid format is given" do 277 | def app 278 | Mugshot::Application.new(:storage => @storage, :allowed_formats => ["jpg", "png"]) 279 | end 280 | 281 | get "image_id/any_name.tiff" 282 | last_response.status.should == 400 283 | end 284 | end 285 | 286 | describe "allowed names" do 287 | it "should allow valid names" do 288 | def app 289 | Mugshot::Application.new(:storage => @storage, :allowed_names => ["some_name", "some_other_name"]) 290 | end 291 | 292 | @image.stub!(:to_blob).and_return("image data") 293 | 294 | ["some_name", "some_other_name"].each do |name| 295 | get "/image_id/#{name}.jpg" 296 | last_response.should be_ok 297 | last_response.body.should == "image data" 298 | end 299 | end 300 | 301 | it "should allow any name when no allowed name is configured" do 302 | @image.stub!(:to_blob).and_return("image data") 303 | 304 | get "/image_id/any_name.tiff" 305 | last_response.should be_ok 306 | last_response.body.should == "image data" 307 | end 308 | 309 | it "should halt with a 400 (Bad Request) when an invalid name is given" do 310 | def app 311 | Mugshot::Application.new(:storage => @storage, :allowed_names => ["some_name", "some_other_name"]) 312 | end 313 | 314 | get "image_id/any_name.tiff" 315 | last_response.status.should == 400 316 | end 317 | end 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /spec/mugshot/fs_storage_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 3 | 4 | describe Mugshot::FSStorage do 5 | before :each do 6 | @fs = Mugshot::FSStorage.new("/tmp/mugshot/spec") 7 | end 8 | 9 | it "should write an image to the filesystem and read it back" do 10 | bin = File.open("spec/files/test.jpg").read 11 | 12 | image = Mugshot::Image.new File.open("spec/files/test.jpg") 13 | Mugshot::Image.stub!(:new).and_return(image) 14 | 15 | id = @fs.write(bin) 16 | 17 | image2 = @fs.read(id) 18 | image2.should == image 19 | end 20 | 21 | it "should return nil when an image with the given id doesn't exist on the filesystem" do 22 | @fs.read('nonexistant-id').should be_nil 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/mugshot/http_storage_spec.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/mugshot/image_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 3 | 4 | describe Mugshot::Image do 5 | before :each do 6 | @magick_image = mock(Magick::Image, :columns => 100, :rows => 100).as_null_object 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 13 | 14 | Magick::Image.stub!(:read).and_return([@magick_image]) 15 | 16 | @image = Mugshot::Image.new(File.open("spec/files/test.jpg")) 17 | end 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") 22 | end 23 | 24 | describe "to blob" do 25 | it 'should return image as a blob using default options' do 26 | @image.to_blob.should == 'blob_data' 27 | end 28 | 29 | it 'should return image as a blob using format' do 30 | @image.should_receive(:format=).with('png') 31 | @image.to_blob(:format => :png).should == 'blob_data' 32 | end 33 | end 34 | 35 | it 'should return image as a blob using quality' do 36 | @image.should_receive(:quality=).with(75) 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 51 | end 52 | 53 | it "should resize image to given width and height" do 54 | @magick_image.should_receive(:resize!).with(300, 200) 55 | @image.resize! "300x200" 56 | end 57 | 58 | it 'should resize image to fit given width' do 59 | @magick_image.should_receive(:resize_to_fit!).with(300, nil) 60 | @image.resize! "300x" 61 | end 62 | 63 | it 'should resize image to fit given height' do 64 | @magick_image.should_receive(:resize_to_fit!).with(nil, 200) 65 | @image.resize! "x200" 66 | end 67 | 68 | it "should crop image to given width and height" do 69 | @magick_image.should_receive(:resize_to_fill!).with(140, 105) 70 | @image.crop! "140x105" 71 | end 72 | 73 | it 'should destroy image' do 74 | @magick_image.should_receive(:'destroy!') 75 | @image.destroy! 76 | end 77 | 78 | end 79 | 80 | -------------------------------------------------------------------------------- /spec/mugshot/magick_factory_spec.rb: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /spec/mugshot/s3_storage_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 3 | require 'fog' 4 | 5 | describe Mugshot::S3Storage do 6 | before :each do 7 | Fog.mock! 8 | @fs = Mugshot::S3Storage.new(:bucket_name => 'bucket_name', :access_key_id => 'access_key_id', :secret_access_key => 'secret_access_key') 9 | @bin = File.open("spec/files/test.jpg").read 10 | end 11 | 12 | it "should write an image to the filesystem and read it back" do 13 | image = Mugshot::Image.new(File.open("spec/files/test.jpg")) 14 | Mugshot::Image.stub!(:new).and_return(image) 15 | 16 | id = @fs.write(@bin) 17 | 18 | image2 = @fs.read(id) 19 | image2.should == image 20 | end 21 | 22 | it "should return nil when an image with the given id doesn't exist on the filesystem" do 23 | @fs.read('nonexistant-id').should be_nil 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/mugshot/storage_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 3 | 4 | describe Mugshot::Storage do 5 | it "should convert an id to path with 6 levels of directories" do 6 | @fs = Mugshot::Storage.new 7 | @fs.send(:id_to_path, "a9657a30c7df012c736512313b021ce1").should == "a9/65/7a/30/c7/df/012c736512313b021ce1" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require "rubygems" 3 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 4 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require 'mugshot' 6 | 7 | require 'rspec' 8 | require 'rspec/autorun' 9 | require 'rack/test' 10 | require 'pp' 11 | 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} 19 | 20 | Rspec.configure do |config| 21 | require 'rspec/expectations' 22 | config.include Rspec::Matchers 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? 29 | end 30 | -------------------------------------------------------------------------------- /spec/test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
--------------------------------------------------------------------------------