httpthumbnailer 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "sinatra", ">= 1.2.6"
4
+ gem "mongrel", ">= 1.1.5"
5
+ gem "rmagick", "~> 2"
6
+ gem "haml", "~> 3"
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "rspec", "~> 2.3.0"
12
+ gem "cucumber", ">= 0"
13
+ gem "bundler", "~> 1.0.0"
14
+ gem "jeweler", "~> 1.6.4"
15
+ gem "rcov", ">= 0"
16
+ gem "daemon", "~> 1"
17
+ gem "httpclient", "~> 2.2"
18
+ gem "rdoc", "~> 3.9"
19
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,69 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.0.0)
5
+ cgi_multipart_eof_fix (2.5.0)
6
+ cucumber (1.1.2)
7
+ builder (>= 2.1.2)
8
+ diff-lcs (>= 1.1.2)
9
+ gherkin (~> 2.6.2)
10
+ json (>= 1.4.6)
11
+ term-ansicolor (>= 1.0.6)
12
+ daemon (1.1.0)
13
+ daemons (1.1.4)
14
+ diff-lcs (1.1.3)
15
+ fastthread (1.0.7)
16
+ gem_plugin (0.2.3)
17
+ gherkin (2.6.5)
18
+ json (>= 1.4.6)
19
+ git (1.2.5)
20
+ haml (3.1.3)
21
+ httpclient (2.2.3)
22
+ jeweler (1.6.4)
23
+ bundler (~> 1.0)
24
+ git (>= 1.2.5)
25
+ rake
26
+ json (1.6.1)
27
+ mongrel (1.1.5)
28
+ cgi_multipart_eof_fix (>= 2.4)
29
+ daemons (>= 1.0.3)
30
+ fastthread (>= 1.0.1)
31
+ gem_plugin (>= 0.2.3)
32
+ rack (1.3.5)
33
+ rack-protection (1.1.4)
34
+ rack
35
+ rake (0.9.2.2)
36
+ rcov (0.9.11)
37
+ rdoc (3.9.4)
38
+ rmagick (2.13.1)
39
+ rspec (2.3.0)
40
+ rspec-core (~> 2.3.0)
41
+ rspec-expectations (~> 2.3.0)
42
+ rspec-mocks (~> 2.3.0)
43
+ rspec-core (2.3.1)
44
+ rspec-expectations (2.3.0)
45
+ diff-lcs (~> 1.1.2)
46
+ rspec-mocks (2.3.0)
47
+ sinatra (1.3.1)
48
+ rack (~> 1.3, >= 1.3.4)
49
+ rack-protection (~> 1.1, >= 1.1.2)
50
+ tilt (~> 1.3, >= 1.3.3)
51
+ term-ansicolor (1.0.7)
52
+ tilt (1.3.3)
53
+
54
+ PLATFORMS
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ bundler (~> 1.0.0)
59
+ cucumber
60
+ daemon (~> 1)
61
+ haml (~> 3)
62
+ httpclient (~> 2.2)
63
+ jeweler (~> 1.6.4)
64
+ mongrel (>= 1.1.5)
65
+ rcov
66
+ rdoc (~> 3.9)
67
+ rmagick (~> 2)
68
+ rspec (~> 2.3.0)
69
+ sinatra (>= 1.2.6)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jakub Pastuszek
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.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = httpthumbnailer
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to httpthumbnailer
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Jakub Pastuszek. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "httpthumbnailer"
18
+ gem.homepage = "http://github.com/jpastuszek/httpthumbnailer"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{HTTP thumbnailing server}
21
+ gem.description = %Q{Provides HTTP API for thumbnailing images}
22
+ gem.email = "jpastuszek@gmail.com"
23
+ gem.authors = ["Jakub Pastuszek"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ require 'cucumber/rake/task'
40
+ Cucumber::Rake::Task.new(:features)
41
+
42
+ task :default => :spec
43
+
44
+ #require 'rake/rdoctask'
45
+ #Rake::RDocTask.new do |rdoc|
46
+ require 'rdoc/task'
47
+ RDoc::Task.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "httpthumbnailer #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'sinatra/base'
4
+ require 'haml'
5
+ require 'RMagick'
6
+
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
+ require 'httpthumbnailer/thumbnailer'
9
+ require 'httpthumbnailer/thumbnail_specs'
10
+
11
+ sinatra = Sinatra.new
12
+
13
+ sinatra.set :port, 3100
14
+ sinatra.set :bind, 'localhost'
15
+ sinatra.set :environment, 'production'
16
+ sinatra.set :server, ['mongrel']
17
+ sinatra.set :lock, true
18
+ sinatra.set :boundary, "thumnail image data"
19
+ sinatra.set :logging, true
20
+
21
+ sinatra.before do
22
+ unless @thumbnailer
23
+ @thumbnailer = Thumbnailer.new(:logger => logger)
24
+ @thumbnailer.method('crop') do |image, spec|
25
+ image.resize_to_fill(spec.width, spec.height)
26
+ end
27
+ end
28
+ end
29
+
30
+ sinatra.helpers do
31
+ def plain_exception(exception)
32
+ headers "Content-Type" => "text/plain"
33
+ body "Error: #{exception.class.name}: #{exception}\n"
34
+ end
35
+ end
36
+
37
+ sinatra.get '/' do
38
+ logger.info 'hello'
39
+ end
40
+
41
+ sinatra.put %r{/thumbnail/(.*)} do |specs|
42
+ image = begin
43
+ @thumbnailer.load('current', request.body)
44
+ rescue => e
45
+ plain_exception(e)
46
+ halt 415
47
+ end
48
+
49
+ thumbnail_specs = ThumbnailSpecs.from_uri(specs)
50
+
51
+ status 200
52
+ headers "Content-Type" => "multipart/mixed; boundary=\"#{settings.boundary}\""
53
+ stream do |out|
54
+ thumbnail_specs.each do |ts|
55
+ logger.info "Thumbnailing: #{ts}"
56
+ out << "--#{settings.boundary}\r\n"
57
+
58
+ begin
59
+ thumbnail = @thumbnailer.thumbnail('current', ts)
60
+ thumbnail_data = thumbnail.to_blob do |inf|
61
+ inf.format = ts.format
62
+ end
63
+
64
+ out << "Content-Type: #{ts.mime}\r\n\r\n"
65
+ out << thumbnail_data
66
+
67
+ thumbnail_data = nil
68
+ thumbnail.destroy!
69
+ rescue => e
70
+ out << "Content-Type: text/plain\r\n\r\n"
71
+ out << "Error: #{e.class.name}: #{e}\r\n"
72
+ ensure
73
+ out << "\r\n"
74
+ end
75
+ end
76
+ out << "--#{settings.boundary}--"
77
+ end
78
+ end
79
+
80
+ sinatra.error do
81
+ plain_exception(env['sinatra.error'])
82
+ end
83
+
84
+ begin
85
+ sinatra.run!
86
+ rescue Errno::EACCES, Errno::EADDRNOTAVAIL, SocketError => e
87
+ puts "Error: failed to bind HTTP server socket (#{options[:bind]}): #{e}"
88
+ exit 5
89
+ end
90
+
@@ -0,0 +1,23 @@
1
+ Feature: Generating set of thumbnails with single PUT request
2
+ In order to generate a set of image thumbnails
3
+ A user must PUT original image to URL in format
4
+ /thumbnail[/<thumbnail type>,<width>,<height>,<format>[,<option key>:<option value>]+]+
5
+
6
+ Scenario: Single thumbnail
7
+ Given test.jpg file content as request body
8
+ When I do PUT request /thumbnail/crop,16,16,PNG
9
+ Then I will get multipart response
10
+ Then first part mime type will be image/png
11
+ And first part will contain PNG image of size 16x16
12
+
13
+ Scenario: Multiple thumbnails
14
+ Given test.jpg file content as request body
15
+ When I do PUT request /thumbnail/crop,16,16,PNG/crop,4,8,JPG/crop,16,32,JPEG
16
+ Then I will get multipart response
17
+ Then first part mime type will be image/png
18
+ And first part will contain PNG image of size 16x16
19
+ Then second part mime type will be image/jpeg
20
+ And second part will contain JPEG image of size 4x8
21
+ Then third part mime type will be image/jpeg
22
+ And third part will contain JPEG image of size 16x32
23
+
@@ -0,0 +1,41 @@
1
+ Before do
2
+ server_start
3
+ @request_body = nil
4
+ @response = nil
5
+ @response_multipart = nil
6
+ end
7
+
8
+ After do
9
+ server_stop
10
+ end
11
+
12
+ Given /(.*) file content as request body/ do |file|
13
+ @request_body = File.open(support_dir + file){|f| f.read }
14
+ end
15
+
16
+ When /I do (.*) request (.*)/ do |method, uri|
17
+ @response = server_request(method, uri, nil, @request_body)
18
+ end
19
+
20
+ Then /I will get multipart response/ do
21
+ @response.header['Content-Type'].first.should match /^multipart/
22
+ @response_multipart = MultipartResponse.new(@response.header['Content-Type'].last, @response.body)
23
+ end
24
+
25
+ Then /(.*) part mime type will be (.*)/ do |part, mime|
26
+ @response_multipart.part[part_no(part)].header['Content-Type'].should == mime
27
+ end
28
+
29
+ Then /(.*) part will contain (.*) image of size (.*)/ do |part, image_type, image_size|
30
+ data = @response_multipart.part[part_no(part)].body
31
+
32
+ Open3.popen3('identify -') do |stdin, stdout, stderr|
33
+ stdin.write data
34
+ stdin.close
35
+ path, type, size, *rest = *stdout.read.split(' ')
36
+ type.should == image_type
37
+ size.should == image_size
38
+ end
39
+ end
40
+
41
+
@@ -0,0 +1,85 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+
12
+ require 'rspec/expectations'
13
+ require 'daemon'
14
+ require 'timeout'
15
+ require 'httpclient'
16
+ require 'httpthumbnailer/multipart_response'
17
+ require "open3"
18
+ require "thread"
19
+
20
+ def gem_dir
21
+ Pathname.new(__FILE__).dirname + '..' + '..'
22
+ end
23
+
24
+ def features_dir
25
+ gem_dir + 'features'
26
+ end
27
+
28
+ def support_dir
29
+ features_dir + 'support'
30
+ end
31
+
32
+ def script(file)
33
+ gem_dir + 'bin' + file
34
+ end
35
+
36
+ def server_get(uri)
37
+ HTTPClient.new.get_content("http://localhost:3100#{uri}")
38
+ end
39
+
40
+ def server_request(method, uri, query = nil, body = nil)
41
+ HTTPClient.new.request(method, "http://localhost:3100#{uri}", query, body)
42
+ end
43
+
44
+ def server_start
45
+ File.exist?("/tmp/httpthumbnailer.pid") and server_stop
46
+ fork do
47
+ Daemon.daemonize("/tmp/httpthumbnailer.pid", support_dir + 'server.log')
48
+ exec("bundle exec #{script('httpthumbnailer')} -p 3100")
49
+ end
50
+
51
+ Timeout.timeout(10) do
52
+ begin
53
+ server_get '/'
54
+ rescue Errno::ECONNREFUSED
55
+ sleep 0.1
56
+ retry
57
+ end
58
+ end
59
+ end
60
+
61
+ def server_stop
62
+ File.open("/tmp/httpthumbnailer.pid") do |pidf|
63
+ pid = pidf.read
64
+
65
+ Timeout.timeout(10) do
66
+ begin
67
+ loop do
68
+ ret = Process.kill("TERM", pid.strip.to_i)
69
+ sleep 0.1
70
+ end
71
+ rescue Errno::ESRCH
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def part_no(part)
78
+ case part
79
+ when 'first' then 0
80
+ when 'second' then 1
81
+ when 'third' then 2
82
+ else fail "add more parts?"
83
+ end
84
+ end
85
+
Binary file
@@ -0,0 +1,94 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "httpthumbnailer"
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jakub Pastuszek"]
12
+ s.date = "2011-11-23"
13
+ s.description = "Provides HTTP API for thumbnailing images"
14
+ s.email = "jpastuszek@gmail.com"
15
+ s.executables = ["httpthumbnailer"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".rspec",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE.txt",
26
+ "README.rdoc",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/httpthumbnailer",
30
+ "features/httpthumbnailer.feature",
31
+ "features/step_definitions/httpthumbnailer_steps.rb",
32
+ "features/support/env.rb",
33
+ "features/support/test.jpg",
34
+ "httpthumbnailer.gemspec",
35
+ "lib/httpthumbnailer/multipart_response.rb",
36
+ "lib/httpthumbnailer/thumbnail_specs.rb",
37
+ "lib/httpthumbnailer/thumbnailer.rb",
38
+ "spec/multipart_response_spec.rb",
39
+ "spec/spec_helper.rb",
40
+ "spec/thumbnail_specs_spec.rb",
41
+ "spec/thumbnailer_spec.rb"
42
+ ]
43
+ s.homepage = "http://github.com/jpastuszek/httpthumbnailer"
44
+ s.licenses = ["MIT"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = "1.8.10"
47
+ s.summary = "HTTP thumbnailing server"
48
+
49
+ if s.respond_to? :specification_version then
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<sinatra>, [">= 1.2.6"])
54
+ s.add_runtime_dependency(%q<mongrel>, [">= 1.1.5"])
55
+ s.add_runtime_dependency(%q<rmagick>, ["~> 2"])
56
+ s.add_runtime_dependency(%q<haml>, ["~> 3"])
57
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
58
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
59
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
60
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
61
+ s.add_development_dependency(%q<rcov>, [">= 0"])
62
+ s.add_development_dependency(%q<daemon>, ["~> 1"])
63
+ s.add_development_dependency(%q<httpclient>, ["~> 2.2"])
64
+ s.add_development_dependency(%q<rdoc>, ["~> 3.9"])
65
+ else
66
+ s.add_dependency(%q<sinatra>, [">= 1.2.6"])
67
+ s.add_dependency(%q<mongrel>, [">= 1.1.5"])
68
+ s.add_dependency(%q<rmagick>, ["~> 2"])
69
+ s.add_dependency(%q<haml>, ["~> 3"])
70
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
71
+ s.add_dependency(%q<cucumber>, [">= 0"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
74
+ s.add_dependency(%q<rcov>, [">= 0"])
75
+ s.add_dependency(%q<daemon>, ["~> 1"])
76
+ s.add_dependency(%q<httpclient>, ["~> 2.2"])
77
+ s.add_dependency(%q<rdoc>, ["~> 3.9"])
78
+ end
79
+ else
80
+ s.add_dependency(%q<sinatra>, [">= 1.2.6"])
81
+ s.add_dependency(%q<mongrel>, [">= 1.1.5"])
82
+ s.add_dependency(%q<rmagick>, ["~> 2"])
83
+ s.add_dependency(%q<haml>, ["~> 3"])
84
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
85
+ s.add_dependency(%q<cucumber>, [">= 0"])
86
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
87
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
88
+ s.add_dependency(%q<rcov>, [">= 0"])
89
+ s.add_dependency(%q<daemon>, ["~> 1"])
90
+ s.add_dependency(%q<httpclient>, ["~> 2.2"])
91
+ s.add_dependency(%q<rdoc>, ["~> 3.9"])
92
+ end
93
+ end
94
+
@@ -0,0 +1,45 @@
1
+ class MultipartResponse
2
+ class NoBoundaryFoundInContentTypeError < ArgumentError
3
+ def initialize(content_type_header, exception)
4
+ super("Content-Type header of '#{content_type_header}' has no boundary defined: #{exception.class.name}: #{exception}")
5
+ end
6
+ end
7
+
8
+ class Part
9
+ def initialize(data)
10
+ if data.include?("\r\n\r\n")
11
+ headers, *body = *data.split("\r\n\r\n")
12
+ @headers = Hash[headers.split("\r\n").map{|h| h.split(/: ?/)}]
13
+ @body = body.join("\r\n\r\n")
14
+ else
15
+ @headers = {'Content-Type' => 'text/plain'}
16
+ @body = data
17
+ end
18
+ end
19
+
20
+ attr_reader :headers, :body
21
+ alias :header :headers
22
+ end
23
+
24
+ def initialize(content_type_header, body)
25
+ @boundary = begin
26
+ content_type_header.split(';').map{|e| e.strip}.select{|e| e =~ /^boundary=/}.first.match(/^boundary="(.*)"/)[1]
27
+ rescue => e
28
+ raise NoBoundaryFoundInContentTypeError.new(content_type_header, e)
29
+ end
30
+
31
+ body, epilogue = *body.split("--#{@boundary}--")
32
+ preamble, *parts = *body.split("--#{@boundary}")
33
+
34
+ @preamble = preamble.sub(/\r\n$/m, '')
35
+ @preamble = nil if @preamble.empty?
36
+
37
+ @epilogue = epilogue.sub(/^\r\n/m, '') if epilogue
38
+
39
+ @parts = parts.map{|p| p.sub(/^\r\n/m, '').sub(/\r\n$/m, '')}.map{|p| Part.new(p)}
40
+ end
41
+
42
+ attr_reader :boundary, :preamble, :parts, :epilogue
43
+ alias :part :parts
44
+ end
45
+
@@ -0,0 +1,22 @@
1
+ require 'httpthumbnailer/thumbnailer'
2
+
3
+ class ThumbnailSpecs < Array
4
+ def self.from_uri(specs)
5
+ ts = ThumbnailSpecs.new
6
+ specs.split('/').each do |spec|
7
+ method, width, height, format, *options = *spec.split(',')
8
+ width = width.to_i
9
+ height = height.to_i
10
+
11
+ opts = {}
12
+ options.each do |option|
13
+ key, value = option.split(':')
14
+ opts[key] = value
15
+ end
16
+
17
+ ts << ThumbnailSpec.new(method, width, height, format, opts)
18
+ end
19
+ ts
20
+ end
21
+ end
22
+
@@ -0,0 +1,78 @@
1
+ require 'logger'
2
+
3
+ class ThumbnailSpec
4
+ def initialize(method, width, height, format, options = {})
5
+ @method = method
6
+ @width = width
7
+ @height = height
8
+ @format = format.upcase
9
+ @options = options
10
+ end
11
+
12
+ def mime
13
+ mime = case format
14
+ when 'JPG' then 'jpeg'
15
+ else format.downcase
16
+ end
17
+ "image/#{mime}"
18
+ end
19
+
20
+ attr_reader :method, :width, :height, :format, :options
21
+
22
+ def to_s
23
+ "#{method} #{width}x#{height} #{mime} (#{format}) #{options.inspect}"
24
+ end
25
+ end
26
+
27
+ class Thumbnailer
28
+ class UnsupportedMethodError < ArgumentError
29
+ def initialize(method)
30
+ super("Thumbnail method '#{method}' is not supported")
31
+ end
32
+ end
33
+
34
+ class ImageNotFound < ArgumentError
35
+ def initialize(id)
36
+ super("No image of ID '#{id}' found")
37
+ end
38
+ end
39
+
40
+ def initialize(options = {})
41
+ @images = {}
42
+ @methods = {}
43
+ @options = options
44
+ @options = options
45
+ @logger = (options[:logger] or Logger.new('/dev/null'))
46
+ end
47
+
48
+ def load(id, io)
49
+ @logger.info "Loading image #{id}"
50
+ @images[id] = Magick::Image.from_blob(io.read).first
51
+ @images[id].strip!
52
+
53
+ return @images[id] unless block_given?
54
+ begin
55
+ yield @images[id]
56
+ @logger.info "Done with image #{id}"
57
+ ensure
58
+ @logger.info "Destroying image #{id}"
59
+ @images[id].destroy!
60
+ @images.delete(id)
61
+ end
62
+ end
63
+
64
+ def method(method, &impl)
65
+ @methods[method] = impl
66
+ end
67
+
68
+ def thumbnail(id, spec)
69
+ image = @images[id] or raise ImageNotFound.new(id)
70
+ process_image(image, spec)
71
+ end
72
+
73
+ def process_image(image, spec)
74
+ impl = @methods[spec.method] or raise UnsupportedMethodError.new(spec.method)
75
+ impl.call(image, spec)
76
+ end
77
+ end
78
+
@@ -0,0 +1,95 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'httpthumbnailer/multipart_response'
3
+
4
+ describe MultipartResponse do
5
+ describe "parsing" do
6
+ it "should privide preamble, parts with headers and epilogue" do
7
+ content_type_header = 'multipart/mixed; boundary="cut here"'
8
+ body =
9
+ """hello
10
+ --cut here
11
+ Content-Type: text/plain
12
+
13
+ part 1
14
+ --cut here
15
+ Content-Type: text/html
16
+ Content-Transfer-Encoding: base64
17
+
18
+ part 2
19
+ --cut here
20
+ part 3
21
+ --cut here--
22
+ world""".gsub!("\n", "\r\n")
23
+
24
+ mr = MultipartResponse.new(content_type_header, body)
25
+ mr.preamble.should == "hello"
26
+
27
+ mr.parts[0].body.should == "part 1"
28
+ mr.parts[0].header['Content-Type'].should == "text/plain"
29
+
30
+ mr.parts[1].body.should == "part 2"
31
+ mr.parts[1].header['Content-Type'].should == "text/html"
32
+ mr.parts[1].header['Content-Transfer-Encoding'].should == "base64"
33
+
34
+ mr.parts[2].body.should == "part 3"
35
+
36
+ mr.epilogue.should == "world"
37
+ end
38
+
39
+ it "should privide nil preamble if no prologue sent" do
40
+ content_type_header = 'multipart/mixed; boundary="cut here"'
41
+ body =
42
+ """--cut here
43
+ part 1
44
+ --cut here--""".gsub!("\n", "\r\n")
45
+
46
+ mr = MultipartResponse.new(content_type_header, body)
47
+ mr.preamble.should be_nil
48
+ end
49
+
50
+ it "should privide nil epilogue if no epilogue sent" do
51
+ content_type_header = 'multipart/mixed; boundary="cut here"'
52
+ body =
53
+ """--cut here
54
+ part 1
55
+ --cut here--""".gsub!("\n", "\r\n")
56
+
57
+ mr = MultipartResponse.new(content_type_header, body)
58
+ mr.epilogue.should be_nil
59
+ end
60
+
61
+ it "should provide default mime type of text/plain if no Content-Type header specified" do
62
+ content_type_header = 'multipart/mixed; boundary="cut here"'
63
+ body =
64
+ """--cut here
65
+ part 1
66
+ --cut here--""".gsub!("\n", "\r\n")
67
+
68
+ mr = MultipartResponse.new(content_type_header, body)
69
+ mr.part[0].header['Content-Type'].should == 'text/plain'
70
+ end
71
+
72
+ it "should fail with MultipartResponse::NoBoundaryFoundInContentTypeError if no boundary specified in content type header" do
73
+ lambda {
74
+ MultipartResponse.new("fas", "")
75
+ }.should raise_error MultipartResponse::NoBoundaryFoundInContentTypeError
76
+ end
77
+ end
78
+
79
+ it "provides part alias" do
80
+ content_type_header = 'multipart/mixed; boundary="cut here"'
81
+ body =
82
+ """--cut here
83
+ part 1
84
+ --cut here
85
+ part 2
86
+ --cut here
87
+ part 3
88
+ --cut here--""".gsub!("\n", "\r\n")
89
+
90
+ mr = MultipartResponse.new(content_type_header, body)
91
+ mr.part[0].body.should == "part 1"
92
+ mr.part[1].body.should == "part 2"
93
+ mr.part[2].body.should == "part 3"
94
+ end
95
+ end
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+
5
+ # Requires supporting files with custom matchers and macros, etc,
6
+ # in ./support/ and its subdirectories.
7
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
8
+
9
+ RSpec.configure do |config|
10
+
11
+ end
@@ -0,0 +1,30 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'httpthumbnailer/thumbnail_specs'
3
+
4
+ describe ThumbnailSpecs do
5
+ it "can be crated from URI" do
6
+ ts = ThumbnailSpecs.from_uri('test,128,256,JPEG,magick:64,number:8/pad,128,128,PNG,background:0xFF00FF/crop,32,32,GIF')
7
+
8
+ ts[0].method.should == 'test'
9
+ ts[0].width.should == 128
10
+ ts[0].height.should == 256
11
+ ts[0].format.should == 'JPEG'
12
+ ts[0].mime.should == 'image/jpeg'
13
+ ts[0].options.should == { 'magick' => '64', 'number' => '8' }
14
+
15
+ ts[1].method.should == 'pad'
16
+ ts[1].width.should == 128
17
+ ts[1].height.should == 128
18
+ ts[1].format.should == 'PNG'
19
+ ts[1].mime.should == 'image/png'
20
+ ts[1].options.should == { 'background' => '0xFF00FF' }
21
+
22
+ ts[2].method.should == 'crop'
23
+ ts[2].width.should == 32
24
+ ts[2].height.should == 32
25
+ ts[2].format.should == 'GIF'
26
+ ts[2].mime.should == 'image/gif'
27
+ ts[2].options.should == {}
28
+ end
29
+ end
30
+
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'httpthumbnailer/thumbnailer'
3
+
4
+ describe ThumbnailSpec do
5
+ it "should store thumbanil attributes method, widht, height and options" do
6
+ t = ThumbnailSpec.new('pad', 128, 256, 'PNG', 'background' => '0x00ffff')
7
+ t.method.should == 'pad'
8
+ t.width.should == 128
9
+ t.height.should == 256
10
+ t.format.should == 'PNG'
11
+ t.mime.should == 'image/png'
12
+ t.options.should == { 'background' => '0x00ffff' }
13
+ end
14
+
15
+ it "should return full jpeg mime time on JPG format" do
16
+ t = ThumbnailSpec.new('pad', 128, 256, 'JPG')
17
+ t.format.should == 'JPG'
18
+ t.mime.should == 'image/jpeg'
19
+ end
20
+ end
21
+
22
+ describe Thumbnailer do
23
+ it 'should allow adding new method of thumbnails' do
24
+ t = Thumbnailer.new
25
+
26
+ t.method('test') do |image, spec|
27
+ image + spec.width + spec.height + spec.format.to_i + spec.options[:magic]
28
+ end
29
+
30
+ t.process_image(256, ThumbnailSpec.new('test', 128, 32, '8', :magic => 64)).should == 256 + 128 + 32 + 8 + 64
31
+ end
32
+ end
33
+
metadata ADDED
@@ -0,0 +1,265 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: httpthumbnailer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Jakub Pastuszek
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-23 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ type: :runtime
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ hash: 19
28
+ segments:
29
+ - 1
30
+ - 2
31
+ - 6
32
+ version: 1.2.6
33
+ prerelease: false
34
+ name: sinatra
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ type: :runtime
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 25
44
+ segments:
45
+ - 1
46
+ - 1
47
+ - 5
48
+ version: 1.1.5
49
+ prerelease: false
50
+ name: mongrel
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ type: :runtime
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ hash: 7
60
+ segments:
61
+ - 2
62
+ version: "2"
63
+ prerelease: false
64
+ name: rmagick
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ type: :runtime
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ~>
72
+ - !ruby/object:Gem::Version
73
+ hash: 5
74
+ segments:
75
+ - 3
76
+ version: "3"
77
+ prerelease: false
78
+ name: haml
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ type: :development
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 2
90
+ - 3
91
+ - 0
92
+ version: 2.3.0
93
+ prerelease: false
94
+ name: rspec
95
+ version_requirements: *id005
96
+ - !ruby/object:Gem::Dependency
97
+ type: :development
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ prerelease: false
108
+ name: cucumber
109
+ version_requirements: *id006
110
+ - !ruby/object:Gem::Dependency
111
+ type: :development
112
+ requirement: &id007 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ hash: 23
118
+ segments:
119
+ - 1
120
+ - 0
121
+ - 0
122
+ version: 1.0.0
123
+ prerelease: false
124
+ name: bundler
125
+ version_requirements: *id007
126
+ - !ruby/object:Gem::Dependency
127
+ type: :development
128
+ requirement: &id008 !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ hash: 7
134
+ segments:
135
+ - 1
136
+ - 6
137
+ - 4
138
+ version: 1.6.4
139
+ prerelease: false
140
+ name: jeweler
141
+ version_requirements: *id008
142
+ - !ruby/object:Gem::Dependency
143
+ type: :development
144
+ requirement: &id009 !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ hash: 3
150
+ segments:
151
+ - 0
152
+ version: "0"
153
+ prerelease: false
154
+ name: rcov
155
+ version_requirements: *id009
156
+ - !ruby/object:Gem::Dependency
157
+ type: :development
158
+ requirement: &id010 !ruby/object:Gem::Requirement
159
+ none: false
160
+ requirements:
161
+ - - ~>
162
+ - !ruby/object:Gem::Version
163
+ hash: 1
164
+ segments:
165
+ - 1
166
+ version: "1"
167
+ prerelease: false
168
+ name: daemon
169
+ version_requirements: *id010
170
+ - !ruby/object:Gem::Dependency
171
+ type: :development
172
+ requirement: &id011 !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ~>
176
+ - !ruby/object:Gem::Version
177
+ hash: 7
178
+ segments:
179
+ - 2
180
+ - 2
181
+ version: "2.2"
182
+ prerelease: false
183
+ name: httpclient
184
+ version_requirements: *id011
185
+ - !ruby/object:Gem::Dependency
186
+ type: :development
187
+ requirement: &id012 !ruby/object:Gem::Requirement
188
+ none: false
189
+ requirements:
190
+ - - ~>
191
+ - !ruby/object:Gem::Version
192
+ hash: 21
193
+ segments:
194
+ - 3
195
+ - 9
196
+ version: "3.9"
197
+ prerelease: false
198
+ name: rdoc
199
+ version_requirements: *id012
200
+ description: Provides HTTP API for thumbnailing images
201
+ email: jpastuszek@gmail.com
202
+ executables:
203
+ - httpthumbnailer
204
+ extensions: []
205
+
206
+ extra_rdoc_files:
207
+ - LICENSE.txt
208
+ - README.rdoc
209
+ files:
210
+ - .document
211
+ - .rspec
212
+ - Gemfile
213
+ - Gemfile.lock
214
+ - LICENSE.txt
215
+ - README.rdoc
216
+ - Rakefile
217
+ - VERSION
218
+ - bin/httpthumbnailer
219
+ - features/httpthumbnailer.feature
220
+ - features/step_definitions/httpthumbnailer_steps.rb
221
+ - features/support/env.rb
222
+ - features/support/test.jpg
223
+ - httpthumbnailer.gemspec
224
+ - lib/httpthumbnailer/multipart_response.rb
225
+ - lib/httpthumbnailer/thumbnail_specs.rb
226
+ - lib/httpthumbnailer/thumbnailer.rb
227
+ - spec/multipart_response_spec.rb
228
+ - spec/spec_helper.rb
229
+ - spec/thumbnail_specs_spec.rb
230
+ - spec/thumbnailer_spec.rb
231
+ homepage: http://github.com/jpastuszek/httpthumbnailer
232
+ licenses:
233
+ - MIT
234
+ post_install_message:
235
+ rdoc_options: []
236
+
237
+ require_paths:
238
+ - lib
239
+ required_ruby_version: !ruby/object:Gem::Requirement
240
+ none: false
241
+ requirements:
242
+ - - ">="
243
+ - !ruby/object:Gem::Version
244
+ hash: 3
245
+ segments:
246
+ - 0
247
+ version: "0"
248
+ required_rubygems_version: !ruby/object:Gem::Requirement
249
+ none: false
250
+ requirements:
251
+ - - ">="
252
+ - !ruby/object:Gem::Version
253
+ hash: 3
254
+ segments:
255
+ - 0
256
+ version: "0"
257
+ requirements: []
258
+
259
+ rubyforge_project:
260
+ rubygems_version: 1.8.10
261
+ signing_key:
262
+ specification_version: 3
263
+ summary: HTTP thumbnailing server
264
+ test_files: []
265
+