anisoptera 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in anisoptera.gemspec
4
+ gemspec
@@ -0,0 +1,116 @@
1
+ # Configurable Rack endpoint for Async image resizing.
2
+
3
+ In progress.
4
+
5
+ Borrows heavily from Dragonfly ([https://github.com/markevans/dragonfly](https://github.com/markevans/dragonfly)).
6
+
7
+ Async mode relies on Thin's async.callback env variable and EventMachine, and ImageMagick for image processing.
8
+
9
+ See [http_router](/ismasan/anisoptera/blob/master/examples/http_router.ru) example for an intro.
10
+
11
+ ## Usage
12
+
13
+ You need a rack router of some sort.
14
+
15
+ ```ruby
16
+ # image_resizer.ru
17
+
18
+ require 'http_router'
19
+ require 'anisoptera'
20
+
21
+ Anisoptera[:media].configure do |config|
22
+ # This is where your original image files are
23
+ config.base_path = './'
24
+ # In case of error, resize and serve up this image
25
+ config.error_image = './Error.gif'
26
+ # Run this block in case of error
27
+ config.on_error do |exception, params|
28
+ Airbrake.notify(
29
+ :error_class => exception.class.name,
30
+ :error_message => exception.message,
31
+ :parameters => params
32
+ )
33
+ end
34
+ end
35
+
36
+ # Create an app with defined routes
37
+
38
+ app = HttpRouter.new do
39
+
40
+ add('/media/:geometry/:color_mode/:filename').to Anisoptera[:media].endpoint {|image, params|
41
+ image.file(params[:filename]).thumb(params[:geometry])
42
+ image.greyscale if params[:color_mode] == 'grey'
43
+ image.encode('png')
44
+ }
45
+ end
46
+
47
+ # Run the app
48
+ run app
49
+ ```
50
+
51
+ Run with Thin
52
+
53
+ $ thin start -R image_resizer.ru -e production -p 5000
54
+
55
+ Now you get on-the fly image resizes
56
+
57
+ http://some.host/media/100x100/grey/logo.png
58
+
59
+ Anisoptera returns all the right HTTP headers so if you put this behind a caching proxy such as Varnish it should just work.
60
+
61
+ ## Custom headers
62
+
63
+ You can pass optional headers, or override default ones
64
+
65
+ ```ruby
66
+ Anisoptera[:media].configure do |config|
67
+ config.headers = {
68
+ 'Cache-Control' => '1234567890',
69
+ 'X-Custom' => 'Hello there'
70
+ }
71
+ end
72
+ ```
73
+
74
+ ## DoS protection
75
+
76
+ Obviously it's a bad idea to allow people to freely resize images on the fly as it might bring your servers down. You can hash the parameters in the URL with a shared key and secret, something like:
77
+
78
+ ```ruby
79
+ get('/media/:hash').to Anisoptera[:media].endpoint {|image, params|
80
+ verify_sha! params[:hash], params[:k]
81
+
82
+ args = Anisoptera::Serializer.marshal_decode(params[:hash])
83
+ image_path = args[:file_name]
84
+ image.file(image_path).thumb(args[:geometry])
85
+ image.greyscale if args[:grey]
86
+ image.encode('jpg')
87
+ }
88
+ ```
89
+
90
+ Then you request images with passing a hash of parameters encoded with the shared secret, and a public key to decode it back.
91
+
92
+ http://some.host/media/BAh7CToGZiIVMjUzNjctaGVsbWV0LmpwZzoJZ3JleUY6DHNob3BfaWRpAeA6BmciDDIwMHgyMDA?k=6a58f9458425f73
93
+
94
+ Anisoptera::Serializer's encode and decode methods can help you Base64-encode a hash of parameters. This is also good because some ImageMagick geometry strings are not valid URL components.
95
+
96
+ # LICENSE
97
+
98
+ Copyright (C) 2011 Ismael Celis
99
+
100
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
101
+ this software and associated documentation files (the "Software"), to deal in
102
+ the Software without restriction, including without limitation the rights to
103
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
104
+ of the Software, and to permit persons to whom the Software is furnished to do
105
+ so, subject to the following conditions:
106
+
107
+ The above copyright notice and this permission notice shall be included in all
108
+ copies or substantial portions of the Software.
109
+
110
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
111
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
112
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
113
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
114
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
115
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
116
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ # If you want to make this the default task
8
+ task :default => :spec
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "anisoptera/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "anisoptera"
7
+ s.version = Anisoptera::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ismael Celis"]
10
+ s.email = ["ismaelct@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Async Rack app for image thumbnailing}
13
+ s.description = %q{You'll need an Eventmachine server such as Thin to run this. See README.'}
14
+
15
+ s.rubyforge_project = "anisoptera"
16
+
17
+ s.add_dependency 'eventmachine', ">= 0.12.10"
18
+ s.add_dependency 'thin'
19
+ s.add_dependency 'thin_async'
20
+ s.add_dependency 'rack', ">= 1.2.2"
21
+
22
+ s.add_development_dependency "bundler", ">= 1.0.0"
23
+ s.add_development_dependency "rack-test"
24
+ s.add_development_dependency "rspec"
25
+ s.add_development_dependency "thin-async-test"
26
+ s.add_development_dependency "http_router"
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
30
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
31
+ s.require_paths = ["lib"]
32
+ end
@@ -0,0 +1,36 @@
1
+ # thin start -R http_router -p 3000
2
+ #
3
+ # http://localhost:3000/media/170x240+210+210/m/pic1.jpg
4
+ # http://localhost:3000/media/170x240/m/pic1.jpg
5
+ # http://localhost:3000/media/100x100-ne/m/pic1.jpg
6
+ # http://localhost:3000/media/100x100-c/m/pic1.jpg
7
+ # http://localhost:3000/media/100x100-c/greyscale/pic1.jpg
8
+
9
+ require 'rubygems'
10
+ # require 'bundler'
11
+ # Bundler.setup
12
+
13
+ require 'http_router'
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
16
+
17
+ require 'anisoptera'
18
+
19
+ Anisoptera[:media].configure do |config|
20
+ config.base_path = './'
21
+ end
22
+
23
+ # Anisoptera.prefer_async = false # uncomment this to test synchronous endpoint with any Rack server
24
+
25
+
26
+ routes = HttpRouter.new do
27
+
28
+ add('/media/:geometry/:color_mode/:filename').to Anisoptera[:media].endpoint {|image, params|
29
+ image.file(params[:filename]).thumb(params[:geometry])
30
+ image.greyscale if params[:color_mode] == 'grey'
31
+ image.encode('png')
32
+ }
33
+
34
+ end
35
+
36
+ run routes
Binary file
@@ -0,0 +1,32 @@
1
+ require 'ostruct'
2
+ require 'rack/mime'
3
+ require 'anisoptera/commander'
4
+ require 'anisoptera/serializer'
5
+ require 'anisoptera/version'
6
+ require 'anisoptera/app'
7
+ require 'anisoptera/endpoint'
8
+
9
+ module Anisoptera
10
+
11
+ HEADERS = {
12
+ 'Cache-Control' => 'public, max-age=3153600'
13
+ }.freeze
14
+
15
+ @apps = {}
16
+ @prefer_async = true
17
+
18
+ def prefer_async=(bool)
19
+ @prefer_async = bool
20
+ end
21
+
22
+ def prefer_async
23
+ !!@prefer_async
24
+ end
25
+
26
+ def [](app_name)
27
+ @apps[app_name] ||= Anisoptera::App.new
28
+ end
29
+
30
+ extend self
31
+
32
+ end
@@ -0,0 +1,29 @@
1
+ module Anisoptera
2
+
3
+ class Config < OpenStruct
4
+
5
+ def on_error(&block)
6
+ @on_error = block if block_given?
7
+ @on_error
8
+ end
9
+
10
+ end
11
+
12
+ class App
13
+
14
+ attr_reader :config
15
+
16
+ def initialize
17
+ @config = Config.new
18
+ end
19
+
20
+ def configure(&block)
21
+ block.call @config
22
+ end
23
+
24
+ def endpoint(&block)
25
+ Anisoptera::Endpoint.factory(@config, block)
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,73 @@
1
+ require 'eventmachine'
2
+ require 'thin/async'
3
+
4
+ module Anisoptera
5
+
6
+ class AsyncEndpoint
7
+
8
+ include Anisoptera::Endpoint
9
+
10
+ # This is a template async response.
11
+ AsyncResponse = [-1, {}, []].freeze
12
+
13
+ STATUSES = {
14
+ 0 => 200,
15
+ 1 => 500,
16
+ 255 => 200 # exit status represented differently when running tests in editor
17
+ }
18
+
19
+ def call(env)
20
+ response = Thin::AsyncResponse.new(env)
21
+
22
+ params = routing_params(env)
23
+
24
+ begin
25
+ job = Anisoptera::Commander.new( @config.base_path, @config.convert_command )
26
+ convert = @handler.call(job, params)
27
+ response.headers.update(update_headers(convert))
28
+
29
+ if !job.check_file
30
+ response.headers['X-Error'] = 'Image not found'
31
+ handle_error error_status(404), response, convert
32
+ else
33
+ handle_success response, convert
34
+ end
35
+ rescue => boom
36
+ response.headers['X-Error'] = boom.message
37
+ response.headers.update(update_headers)
38
+ @config.on_error.call(boom, params, env) if @config.on_error
39
+ handle_error(error_status(500), response)
40
+ end
41
+
42
+ response.finish
43
+ end
44
+
45
+ protected
46
+
47
+ def handle_success(response, convert)
48
+ EM.system( convert.command ){ |output, status|
49
+ http_status = STATUSES[status.exitstatus]
50
+ response.status = http_status
51
+ r = http_status == 200 ? output : 'SERVER ERROR'
52
+ response << r
53
+ response.done
54
+ }
55
+ end
56
+
57
+ def handle_error(status, response, convert = nil)
58
+ response.status = status
59
+ response.headers['Content-Type'] = Rack::Mime.mime_type(::File.extname(error_image))
60
+ if convert # pass error image through original IM command
61
+ EM.system( convert.command(error_image) ){ |output, status|
62
+ response << output
63
+ response.done
64
+ }
65
+ else # just blocking read because user handler blew up
66
+ response << ::File.read(error_image)
67
+ response.done
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,148 @@
1
+ module Anisoptera
2
+
3
+ # Simple interface to ImageMagick main commands
4
+ # Most of it borrowed from DragonFly
5
+ # https://github.com/markevans/dragonfly/blob/master/lib/dragonfly/image_magick/processor.rb
6
+ #
7
+ class Commander
8
+
9
+ GRAVITIES = {
10
+ 'nw' => 'NorthWest',
11
+ 'n' => 'North',
12
+ 'ne' => 'NorthEast',
13
+ 'w' => 'West',
14
+ 'c' => 'Center',
15
+ 'e' => 'East',
16
+ 'sw' => 'SouthWest',
17
+ 's' => 'South',
18
+ 'se' => 'SouthEast'
19
+ }
20
+
21
+ # Geometry string patterns
22
+ RESIZE_GEOMETRY = /^\d*x\d*[><%^!]?$|^\d+@$/ # e.g. '300x200!'
23
+ CROPPED_RESIZE_GEOMETRY = /^(\d+)x(\d+)[#|-](\w{1,2})?$/ # e.g. '20x50#ne'
24
+ CROP_GEOMETRY = /^(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?$/ # e.g. '30x30+10+10'
25
+
26
+ def initialize(base_path, convert_command = nil)
27
+ @convert_command = convert_command || 'convert'
28
+ @base_path = base_path
29
+ @original = nil
30
+ @geometry = nil
31
+ end
32
+
33
+ def file(path)
34
+ @original = path
35
+ self
36
+ end
37
+
38
+ def encode(format)
39
+ @encode = format
40
+ self
41
+ end
42
+
43
+ def square(size)
44
+ size = size.to_i
45
+ d = size * 2
46
+ @square = "-thumbnail x#{d} -resize '#{d}x<' -resize 50% -gravity center -crop #{size}x#{size}+0+0"
47
+ self
48
+ end
49
+
50
+ def greyscale
51
+ @greyscale = '-colorspace Gray'
52
+ self
53
+ end
54
+
55
+ def thumb(geometry)
56
+ @geometry = case geometry
57
+ when RESIZE_GEOMETRY
58
+ resize(geometry)
59
+ when CROPPED_RESIZE_GEOMETRY
60
+ resize_and_crop(:width => $1, :height => $2, :gravity => $3)
61
+ when CROP_GEOMETRY
62
+ crop(
63
+ :width => $1,
64
+ :height => $2,
65
+ :x => $3,
66
+ :y => $4,
67
+ :gravity => $5
68
+ )
69
+ else raise ArgumentError, "Didn't recognise the geometry string #{geometry}."
70
+ end
71
+ self
72
+ end
73
+
74
+ def mime_type
75
+ ext = @encode ? ".#{@encode}" : File.extname(@original)
76
+ Rack::Mime.mime_type ext
77
+ end
78
+
79
+ # Utility method to test that all commands are working
80
+ #
81
+ def to_file(path)
82
+ File.open(path, 'w+') do |f|
83
+ f.write(`#{command}`)
84
+ end
85
+ end
86
+
87
+ def command(file_path = nil)
88
+ raise ArgumentError, "no original file provided. Do commander.file('some_file.jpg')" unless @original
89
+ file_path ||= full_file_path
90
+ cmd = []
91
+ cmd << @geometry if @geometry
92
+ cmd << @greyscale if @greyscale
93
+ cmd << @square if @square # should clear command
94
+ if @encode
95
+ cmd << "#{@encode}:-" # to stdout
96
+ else
97
+ cmd << '-'
98
+ end
99
+
100
+ "#{@convert_command} #{file_path} " + cmd.join(' ')
101
+ end
102
+
103
+ def check_file
104
+ ::File.exists?(full_file_path)
105
+ end
106
+
107
+ private
108
+
109
+ def full_file_path
110
+ File.join(@base_path, @original)
111
+ end
112
+
113
+ def resize(geometry)
114
+ "-resize \"#{geometry}\""
115
+ end
116
+
117
+ def crop(opts={})
118
+ width = opts[:width]
119
+ height = opts[:height]
120
+ gravity = GRAVITIES[opts[:gravity]]
121
+ x = "#{opts[:x] || 0}"
122
+ x = '+' + x unless x[/^[+-]/]
123
+ y = "#{opts[:y] || 0}"
124
+ y = '+' + y unless y[/^[+-]/]
125
+ repage = opts[:repage] == false ? '' : '+repage'
126
+ resize = opts[:resize]
127
+
128
+ "#{"-resize #{resize} " if resize}#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} #{repage}"
129
+ end
130
+
131
+ def resize_and_crop(opts={})
132
+ if !opts[:width] && !opts[:height]
133
+ return self
134
+ elsif !opts[:width] || !opts[:height]
135
+ attrs = identify(temp_object)
136
+ opts[:width] ||= attrs[:width]
137
+ opts[:height] ||= attrs[:height]
138
+ end
139
+
140
+ opts[:gravity] ||= 'c'
141
+
142
+ opts[:resize] = "#{opts[:width]}x#{opts[:height]}^^"
143
+ crop(opts)
144
+ end
145
+
146
+ end
147
+
148
+ end
@@ -0,0 +1,60 @@
1
+ module Anisoptera
2
+
3
+ module Endpoint
4
+
5
+ def self.factory(config, block)
6
+ if defined?(EventMachine) && Anisoptera.prefer_async
7
+ require 'anisoptera/async_endpoint'
8
+ AsyncEndpoint.new(config, &block)
9
+ else
10
+ require 'anisoptera/sync_endpoint'
11
+ SyncEndpoint.new(config, &block)
12
+ end
13
+
14
+ end
15
+
16
+ def initialize(config, &block)
17
+ @config = config
18
+ @handler = block
19
+ end
20
+
21
+ private
22
+
23
+ # Borrowed from Dragonfly
24
+ #
25
+ def routing_params(env)
26
+ pars = env['rack.routing_args'] ||
27
+ env['action_dispatch.request.path_parameters'] ||
28
+ env['router.params'] ||
29
+ env['usher.params'] ||
30
+ raise(ArgumentError, "couldn't find any routing parameters in env #{env.inspect}")
31
+
32
+ # http_router doesn't parse querystring! Let's make sure we do
33
+ query = Rack::Utils.parse_nested_query(env["QUERY_STRING"]).inject({}) do |mem, (k,v)|
34
+ mem[k.to_sym] = v
35
+ mem
36
+ end
37
+ pars.update(query)
38
+ end
39
+
40
+ def update_headers(commander = nil)
41
+ heads = Anisoptera::HEADERS.dup.update(
42
+ 'X-Generator' => self.class.name,
43
+ 'Last-Modified' => Time.now.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")
44
+ )
45
+ heads.update(@config.headers) if @config.headers && @config.headers.is_a?(Hash)
46
+ heads['Content-Type'] = commander.mime_type if commander
47
+ heads
48
+ end
49
+
50
+ def error_image
51
+ @config.error_image || ::File.join(File.dirname(__FILE__), 'error.png')
52
+ end
53
+
54
+ def error_status(status)
55
+ @config.error_status || status
56
+ end
57
+
58
+ end
59
+
60
+ end
Binary file
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ require 'base64'
3
+
4
+ # Stolen fr... I mean 'borrowed' from DragonFly
5
+ # https://github.com/markevans/dragonfly/blob/master/lib/dragonfly/serializer.rb
6
+ #
7
+ module Anisoptera
8
+ module Serializer
9
+
10
+ # Exceptions
11
+ class BadString < RuntimeError; end
12
+
13
+ extend self # So we can do Serializer.b64_encode, etc.
14
+
15
+ def b64_encode(string)
16
+ Base64.encode64(string).tr("\n=",'')
17
+ end
18
+
19
+ def b64_decode(string)
20
+ padding_length = string.length % 4
21
+ Base64.decode64(string + '=' * padding_length)
22
+ end
23
+
24
+ def marshal_encode(object)
25
+ b64_encode(Marshal.dump(object))
26
+ end
27
+
28
+ def marshal_decode(string)
29
+ Marshal.load(b64_decode(string))
30
+ rescue TypeError, ArgumentError => e
31
+ raise BadString, "couldn't decode #{string} - got #{e}"
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ module Anisoptera
2
+
3
+ class SyncEndpoint
4
+
5
+ include Anisoptera::Endpoint
6
+
7
+ def call(env)
8
+ params = routing_params(env)
9
+ job = Anisoptera::Commander.new( @config.base_path )
10
+ convert = @handler.call(job, params)
11
+
12
+ result = `#{convert.command}`
13
+
14
+ headers = update_headers(convert)
15
+
16
+ [200, headers, [result]]
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,3 @@
1
+ module Anisoptera
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ describe Anisoptera do
4
+
5
+ describe "defaults" do
6
+ it "must prefer async by default" do
7
+ Anisoptera.prefer_async.should be_true
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,258 @@
1
+ # encoding: utf-8
2
+ require File.dirname(__FILE__) + '/test_helper'
3
+
4
+ require 'thin'
5
+ require 'thin/async'
6
+ require 'thin/async/test'
7
+ require 'rack/test'
8
+ require 'http_router'
9
+ require "base64"
10
+
11
+ $HERE = File.dirname(__FILE__)
12
+
13
+ $CONVERT = "/usr/local/bin/convert"
14
+
15
+ describe 'Anisoptera::AsyncEndpoint' do
16
+ include Rack::Test::Methods
17
+
18
+ def app
19
+ Thin::Async::Test.new(@app)
20
+ end
21
+
22
+ before do
23
+ # redefine config each time to reset app
24
+ Anisoptera[:media].configure do |config|
25
+ config.base_path = File.join($HERE, 'files')
26
+ config.error_status = nil
27
+ config.error_image = nil
28
+ config.convert_command = $CONVERT
29
+ end
30
+
31
+ @app = HttpRouter.new do
32
+ add('/:g/:file').to Anisoptera[:media].endpoint {|image, params|
33
+ image_path = params[:file]
34
+ image.file(image_path).thumb(params[:g])
35
+ image.greyscale if params[:grey]
36
+ image.encode('jpg')
37
+ }
38
+ end
39
+ end
40
+
41
+ shared_examples_for "a cached image" do |status, content_type|
42
+
43
+ it 'should have status 200' do
44
+ last_response.status.should == status
45
+ end
46
+
47
+ it 'should have long-lived headers' do
48
+ last_response.headers["Cache-Control"].should == "public, max-age=3153600"
49
+ end
50
+
51
+ it 'should return Last-Modified header' do
52
+ last_response.headers['Last-Modified'].should == @the_time
53
+ end
54
+
55
+ it 'should return encoded content-type' do
56
+ last_response.headers['Content-Type'].should == content_type
57
+ end
58
+ end
59
+
60
+ shared_examples_for 'resized image data' do |test_image, geometry, content_type|
61
+ it 'should return resized image data' do
62
+ tempimg = File.join($HERE, 'files', "temp.#{content_type}")
63
+ Base64.encode64(`#{$CONVERT} #{test_image} -resize \"#{geometry}\" #{tempimg}`)
64
+ Base64.encode64(last_response.body).should == Base64.encode64(File.read(tempimg))
65
+ File.unlink(tempimg)
66
+ end
67
+ end
68
+
69
+ describe 'success' do
70
+ before do
71
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
72
+ get '/20x20/test.gif'
73
+ end
74
+
75
+ it_behaves_like 'a cached image', 200, 'image/jpeg'
76
+
77
+ it_behaves_like 'resized image data', File.join($HERE,'files','test.gif'), '20x20', 'jpg'
78
+
79
+ end
80
+
81
+ describe 'with querystring' do
82
+ before do
83
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
84
+ get '/20x20/missing.gif?file=test.gif'
85
+ end
86
+
87
+ it_behaves_like 'a cached image', 200, 'image/jpeg'
88
+
89
+ end
90
+
91
+ describe 'with custom headers' do
92
+ before do
93
+ # redefine config each time to reset app
94
+ Anisoptera[:custom].configure do |config|
95
+ config.base_path = File.join($HERE, 'files')
96
+ config.headers = {
97
+ 'Cache-Control' => '1234567890',
98
+ 'X-Custom' => 'custom-head'
99
+ }
100
+ end
101
+
102
+ @app = HttpRouter.new do
103
+ add('/:g/:file').to Anisoptera[:custom].endpoint {|image, params|
104
+ image_path = params[:file]
105
+ image.file(image_path).thumb(params[:g])
106
+ image.encode('jpg')
107
+ }
108
+ end
109
+
110
+ it 'should return encoded content-type' do
111
+ last_response.headers['Content-Type'].should == 'image/jpg'
112
+ end
113
+
114
+ it 'should overwrite passed headers' do
115
+ last_response.headers['Cache-Control'].should == '1234567890'
116
+ end
117
+
118
+ it 'should add passed new headers' do
119
+ last_response.headers['X-Custom'].should == 'custom-head'
120
+ end
121
+
122
+ end
123
+ end
124
+
125
+ describe 'missing image' do
126
+ before do
127
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
128
+ get '/20x20/testfoo.gif'
129
+ end
130
+
131
+ it_behaves_like 'a cached image', 404, 'image/png'
132
+
133
+ it_behaves_like 'resized image data', File.join($HERE,'..','lib','anisoptera', 'error.png'), '20x20', 'jpg'
134
+ end
135
+
136
+ describe 'missing image with error image configured' do
137
+ before do
138
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
139
+ Anisoptera[:media].config.error_image = File.join($HERE, 'files', 'Chile.gif')
140
+ get '/20x20/testfoo.gif'
141
+ end
142
+
143
+ it_behaves_like 'a cached image', 404, 'image/gif'
144
+
145
+ it_behaves_like 'resized image data', File.join($HERE,'files', 'Chile.gif'), '20x20', 'jpg'
146
+ end
147
+
148
+ describe 'with malformed geometry' do
149
+ before do
150
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
151
+ get '/20x20wtf/test.gif'
152
+ end
153
+
154
+ it_behaves_like 'a cached image', 500, 'image/png'
155
+
156
+ it 'should set X-Error header' do
157
+ last_response.headers['X-Error'].should == "Didn't recognise the geometry string 20x20wtf."
158
+ end
159
+ end
160
+
161
+ describe 'with exceptions in custom block' do
162
+
163
+ before do
164
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
165
+
166
+ @app = HttpRouter.new do
167
+ add('/:g/:file').to Anisoptera[:media].endpoint {|image, params|
168
+ raise 'Oops!'
169
+ }
170
+ end
171
+
172
+ get '/20x20/test.gif'
173
+ end
174
+
175
+ it_behaves_like 'a cached image', 500, 'image/png'
176
+
177
+ it 'should set X-Error header' do
178
+ last_response.headers['X-Error'].should == 'Oops!'
179
+ end
180
+
181
+ end
182
+
183
+ describe 'with exceptions and custom config.error_status' do
184
+ before do
185
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
186
+ Anisoptera[:media].config.error_status = 200
187
+
188
+ get '/20x20/testfoo.gif'
189
+ end
190
+
191
+ it_behaves_like 'a cached image', 200, 'image/png'
192
+
193
+ it 'should set X-Error header' do
194
+ last_response.headers['X-Error'].should == 'Image not found'
195
+ end
196
+
197
+ end
198
+
199
+ describe 'with exceptions in custom block and custom error_status' do
200
+
201
+ before do
202
+ Anisoptera[:media].config.error_status = 200
203
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
204
+
205
+ @app = HttpRouter.new do
206
+ add('/:g/:file').to Anisoptera[:media].endpoint {|image, params|
207
+ raise 'Oops!'
208
+ }
209
+ end
210
+
211
+ get '/20x20/test.gif'
212
+ end
213
+
214
+ it_behaves_like 'a cached image', 200, 'image/png'
215
+
216
+ it 'should set X-Error header' do
217
+ last_response.headers['X-Error'].should == 'Oops!'
218
+ end
219
+
220
+ end
221
+
222
+ describe 'with exceptions and config.on_error block' do
223
+
224
+ before do
225
+ @the_time = mock_time("Sun, 20 Nov 2011 01:21:21 GMT")
226
+
227
+ @error_message = ''
228
+ @error_params = nil
229
+ @env = nil
230
+ Anisoptera[:media].config.on_error do |exception, params, env|
231
+ @error_message = 'Oops!'
232
+ @error_params = params
233
+ @env = env
234
+ end
235
+
236
+ @app = HttpRouter.new do
237
+ add('/:gg/:file').to Anisoptera[:media].endpoint {|image, params|
238
+ raise 'Oops!'
239
+ }
240
+ end
241
+
242
+ get '/20x20/test.gif'
243
+ end
244
+
245
+ it 'should have called error block' do
246
+ @error_message.should == 'Oops!'
247
+ @error_params.should == {:gg => '20x20', :file => 'test.gif'}
248
+ @env['SCRIPT_NAME'].should == '/20x20/test.gif'
249
+ end
250
+
251
+ it_behaves_like 'a cached image', 500, 'image/png'
252
+
253
+ it 'should set X-Error header' do
254
+ last_response.headers['X-Error'].should == 'Oops!'
255
+ end
256
+
257
+ end
258
+ end
@@ -0,0 +1,104 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ describe Anisoptera::Commander do
4
+
5
+ describe 'building file path' do
6
+
7
+ before do
8
+ @commander = Anisoptera::Commander.new('/data')
9
+ end
10
+
11
+ it 'must raise error if no file name given' do
12
+ proc {
13
+ @commander.command
14
+ }.should raise_error(ArgumentError)
15
+ end
16
+
17
+ it 'must correct missing slashes' do
18
+ @commander.file('foo.jpg').command.should match(/^convert \/data\/foo\.jpg/)
19
+ @commander.file('/foo.jpg').command.should match(/^convert \/data\/foo\.jpg/)
20
+ end
21
+ end
22
+
23
+ describe 'commands' do
24
+ before do
25
+ @commander = Anisoptera::Commander.new('/data').file('foo.jpg')
26
+ end
27
+
28
+ describe 'resize' do
29
+ it 'should add resize command' do
30
+ @commander.thumb('10x10').command.should == "convert /data/foo.jpg -resize \"10x10\" -"
31
+ end
32
+ end
33
+
34
+ describe 'crop' do
35
+ it 'should add resize command' do
36
+ @commander.thumb('120x120+10+5').command.should == "convert /data/foo.jpg -crop 120x120+10+5 +repage -"
37
+ end
38
+ end
39
+
40
+ describe 'scale' do
41
+ it 'should add scale resize' do
42
+ @commander.thumb('50x50%').command.should == "convert /data/foo.jpg -resize \"50x50%\" -"
43
+ end
44
+ end
45
+
46
+ describe 'bigger / smaller than' do
47
+ it 'should add < or >' do
48
+ @commander.thumb('400x300<').command.should == "convert /data/foo.jpg -resize \"400x300<\" -"
49
+ @commander.thumb('400x300>').command.should == "convert /data/foo.jpg -resize \"400x300>\" -"
50
+ end
51
+ end
52
+
53
+ describe 'positioning (gravity)' do
54
+ it 'should resize with gravity' do
55
+ @commander.thumb('400x300#ne').command.should == "convert /data/foo.jpg -resize 400x300^^ -gravity NorthEast -crop 400x300+0+0 +repage -"
56
+ end
57
+ end
58
+
59
+ describe 'encode' do
60
+ it 'should add format arguments' do
61
+ @commander.encode('gif').command.should == 'convert /data/foo.jpg gif:-'
62
+ end
63
+ end
64
+
65
+ describe 'greyscale' do
66
+ it 'should add colorspace argument' do
67
+ @commander.greyscale.command.should == 'convert /data/foo.jpg -colorspace Gray -'
68
+ end
69
+ end
70
+
71
+ describe 'combined' do
72
+
73
+ it 'should combine commands' do
74
+ @commander.
75
+ thumb('100x100#ne').
76
+ encode('png').
77
+ command.should == "convert /data/foo.jpg -resize 100x100^^ -gravity NorthEast -crop 100x100+0+0 +repage png:-"
78
+ end
79
+
80
+ it 'should accept - as gravity separator because the hash breaks browsers' do
81
+ @commander.
82
+ thumb('100x100-ne').
83
+ command.should == "convert /data/foo.jpg -resize 100x100^^ -gravity NorthEast -crop 100x100+0+0 +repage -"
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ describe 'mime_type' do
90
+ before do
91
+ @commander = Anisoptera::Commander.new('/data').file('foo.png')
92
+ end
93
+
94
+ it 'should get default from filename' do
95
+ @commander.mime_type.should == 'image/png'
96
+ end
97
+
98
+ it 'should use the one passed to #encode if provided' do
99
+ @commander.encode('gif').mime_type.should == 'image/gif'
100
+ end
101
+
102
+ end
103
+
104
+ end
Binary file
Binary file
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ describe Anisoptera::Serializer do
4
+ before do
5
+ @data = {:a => 'a', :b => 'b', :c => 11}
6
+ @encoded = Anisoptera::Serializer.marshal_encode(@data)
7
+ end
8
+
9
+ describe "encoding" do
10
+ it "must convert object into a string" do
11
+ @encoded.class.should == String
12
+ end
13
+ end
14
+
15
+ describe 'decoding' do
16
+ it 'should recover original object' do
17
+ decoded = Anisoptera::Serializer.marshal_decode(@encoded)
18
+ decoded.should == @data
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+
8
+ require 'anisoptera'
9
+
10
+
11
+ require 'rspec'
12
+
13
+ def mock_time(atime)
14
+ t = stub('Now')
15
+ Time.stub!(:now).and_return t
16
+ t.stub!(:gmtime).and_return t
17
+ t.should_receive(:strftime).with("%a, %d %b %Y %H:%M:%S GMT").and_return atime
18
+ atime
19
+ end
metadata ADDED
@@ -0,0 +1,182 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anisoptera
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.2
6
+ platform: ruby
7
+ authors:
8
+ - Ismael Celis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-12-04 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.12.10
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: thin
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: thin_async
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: rack
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 1.2.2
57
+ type: :runtime
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: bundler
61
+ prerelease: false
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 1.0.0
68
+ type: :development
69
+ version_requirements: *id005
70
+ - !ruby/object:Gem::Dependency
71
+ name: rack-test
72
+ prerelease: false
73
+ requirement: &id006 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ type: :development
80
+ version_requirements: *id006
81
+ - !ruby/object:Gem::Dependency
82
+ name: rspec
83
+ prerelease: false
84
+ requirement: &id007 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "0"
90
+ type: :development
91
+ version_requirements: *id007
92
+ - !ruby/object:Gem::Dependency
93
+ name: thin-async-test
94
+ prerelease: false
95
+ requirement: &id008 !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: "0"
101
+ type: :development
102
+ version_requirements: *id008
103
+ - !ruby/object:Gem::Dependency
104
+ name: http_router
105
+ prerelease: false
106
+ requirement: &id009 !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: "0"
112
+ type: :development
113
+ version_requirements: *id009
114
+ description: You'll need an Eventmachine server such as Thin to run this. See README.'
115
+ email:
116
+ - ismaelct@gmail.com
117
+ executables: []
118
+
119
+ extensions: []
120
+
121
+ extra_rdoc_files: []
122
+
123
+ files:
124
+ - .gitignore
125
+ - .rspec
126
+ - Gemfile
127
+ - README.md
128
+ - Rakefile
129
+ - anisoptera.gemspec
130
+ - examples/http_router.ru
131
+ - examples/pic1.jpg
132
+ - lib/anisoptera.rb
133
+ - lib/anisoptera/app.rb
134
+ - lib/anisoptera/async_endpoint.rb
135
+ - lib/anisoptera/commander.rb
136
+ - lib/anisoptera/endpoint.rb
137
+ - lib/anisoptera/error.png
138
+ - lib/anisoptera/serializer.rb
139
+ - lib/anisoptera/sync_endpoint.rb
140
+ - lib/anisoptera/version.rb
141
+ - spec/anisoptera_spec.rb
142
+ - spec/async_endpoint_spec.rb
143
+ - spec/commander_spec.rb
144
+ - spec/files/Chile.gif
145
+ - spec/files/test.gif
146
+ - spec/serializer_spec.rb
147
+ - spec/test_helper.rb
148
+ homepage: ""
149
+ licenses: []
150
+
151
+ post_install_message:
152
+ rdoc_options: []
153
+
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: "0"
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ none: false
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: "0"
168
+ requirements: []
169
+
170
+ rubyforge_project: anisoptera
171
+ rubygems_version: 1.8.17
172
+ signing_key:
173
+ specification_version: 3
174
+ summary: Async Rack app for image thumbnailing
175
+ test_files:
176
+ - spec/anisoptera_spec.rb
177
+ - spec/async_endpoint_spec.rb
178
+ - spec/commander_spec.rb
179
+ - spec/files/Chile.gif
180
+ - spec/files/test.gif
181
+ - spec/serializer_spec.rb
182
+ - spec/test_helper.rb