httpthumbnailer 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.4
data/bin/httpthumbnailer CHANGED
@@ -17,30 +17,42 @@ sinatra.set :server, ['mongrel']
17
17
  sinatra.set :lock, true
18
18
  sinatra.set :boundary, "thumnail image data"
19
19
  sinatra.set :logging, true
20
+ sinatra.set :debug, true
20
21
 
21
22
  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)
23
+ logger.level = Logger::DEBUG if settings.debug == true
24
+ if $thumbnailer.nil?
25
+ $thumbnailer = Thumbnailer.new(:logger => logger)
26
+
27
+ $thumbnailer.method('crop') do |image, spec|
28
+ image.resize_to_fill!(spec.width, spec.height)
26
29
  end
27
30
 
28
- @thumbnailer.method('fit') do |image, spec|
29
- image.resize_to_fit(spec.width, spec.height)
31
+ $thumbnailer.method('fit') do |image, spec|
32
+ image.resize_to_fit!(spec.width, spec.height)
30
33
  end
31
34
 
32
- @thumbnailer.method('pad') do |image, spec|
33
- Magick::Image.new(spec.width, spec.height) {
35
+ $thumbnailer.method('pad') do |image, spec|
36
+ image.resize_to_fit!(spec.width, spec.height)
37
+
38
+ out = Magick::Image.new(spec.width, spec.height) {
34
39
  self.background_color = Magick::Pixel.new(Magick::MaxRGB, Magick::MaxRGB, Magick::MaxRGB, Magick::MaxRGB) # transparent
35
- }.composite!(image.resize_to_fit(spec.width, spec.height), Magick::CenterGravity, Magick::OverCompositeOp)
40
+ }.composite!(image, Magick::CenterGravity, Magick::OverCompositeOp)
41
+
42
+ image.destroy!
43
+ out
36
44
  end
37
45
  end
38
46
  end
39
47
 
40
48
  sinatra.helpers do
41
- def plain_exception(exception)
49
+ def plain_response(msg)
42
50
  headers "Content-Type" => "text/plain"
43
- body "Error: #{exception.class.name}: #{exception}\n"
51
+ body msg.gsub("\n", "\r\n") + "\r\n"
52
+ end
53
+
54
+ def plain_exception(exception)
55
+ plain_response("Error: #{exception.class.name}: #{exception}")
44
56
  end
45
57
  end
46
58
 
@@ -48,45 +60,50 @@ sinatra.get '/' do
48
60
  logger.info 'hello'
49
61
  end
50
62
 
51
- sinatra.put %r{/thumbnail/(.*)} do |specs|
52
- image = begin
53
- @thumbnailer.load('current', request.body)
54
- rescue => e
55
- plain_exception(e)
56
- halt 415
57
- end
63
+ sinatra.get '/stats/images' do
64
+ $thumbnailer.images.to_s
65
+ end
58
66
 
67
+ sinatra.put %r{/thumbnail/(.*)} do |specs|
59
68
  thumbnail_specs = ThumbnailSpecs.from_uri(specs)
60
-
61
- status 200
62
- headers "Content-Type" => "multipart/mixed; boundary=\"#{settings.boundary}\""
63
- stream do |out|
64
- thumbnail_specs.each do |spec|
65
- logger.info "Thumbnailing: #{spec}"
66
- out << "--#{settings.boundary}\r\n"
67
-
68
- begin
69
- thumbnail = @thumbnailer.thumbnail('current', spec)
70
- thumbnail_data = thumbnail.to_blob do |inf|
71
- inf.format = spec.format
69
+ $thumbnailer.load(request.body) do |original_image_handler|
70
+ status 200
71
+ headers "Content-Type" => "multipart/mixed; boundary=\"#{settings.boundary}\""
72
+
73
+ stream do |out| # this is non blocking
74
+ original_image_handler.use do |original_image|
75
+ thumbnail_specs.each do |spec|
76
+ logger.info "Thumbnailing: #{spec}"
77
+ out << "--#{settings.boundary}\r\n"
78
+
79
+ begin
80
+ thumbnail_data = original_image.thumbnail(spec)
81
+
82
+ out << "Content-Type: #{spec.mime}\r\n\r\n"
83
+ out << thumbnail_data
84
+ rescue => e
85
+ logger.error "Thumbnailing error: #{e.class.name}: #{e}: \n#{e.backtrace.join("\n")}"
86
+ out << "Content-Type: text/plain\r\n\r\n"
87
+ out << "Error: #{e.class.name}: #{e}\r\n"
88
+ ensure
89
+ out << "\r\n"
90
+ end
72
91
  end
73
-
74
- out << "Content-Type: #{spec.mime}\r\n\r\n"
75
- out << thumbnail_data
76
-
77
- thumbnail_data = nil
78
- thumbnail.destroy!
79
- rescue => e
80
- out << "Content-Type: text/plain\r\n\r\n"
81
- out << "Error: #{e.class.name}: #{e}\r\n"
82
- ensure
83
- out << "\r\n"
92
+ out << "--#{settings.boundary}--"
84
93
  end
85
94
  end
86
- out << "--#{settings.boundary}--"
87
95
  end
88
96
  end
89
97
 
98
+ sinatra.error Thumbnailer::UnsupportedMediaTypeError do
99
+ plain_exception(env['sinatra.error'])
100
+ halt 415
101
+ end
102
+
103
+ sinatra.error 404 do
104
+ plain_response("Resource '#{request.path_info}' not found")
105
+ end
106
+
90
107
  sinatra.error do
91
108
  plain_exception(env['sinatra.error'])
92
109
  end
@@ -4,53 +4,131 @@ Feature: Generating set of thumbnails with single PUT request
4
4
  /thumbnail[/<thumbnail type>,<width>,<height>,<format>[,<option key>:<option value>]+]+
5
5
 
6
6
  Background:
7
+ Given httpthumbnailer log is empty
7
8
  Given httpthumbnailer server is running at http://localhost:3100/
8
9
 
9
10
  Scenario: Single thumbnail
10
11
  Given test.jpg file content as request body
11
12
  When I do PUT request http://localhost:3100/thumbnail/crop,16,16,PNG
12
- Then I will get multipart response
13
- Then first part mime type will be image/png
14
- And first part will contain PNG image of size 16x16
13
+ Then response status will be 200
14
+ And I will get multipart response
15
+ Then first part will contain PNG image of size 16x16
16
+ And first part mime type will be image/png
17
+ And there will be no leaked images
15
18
 
16
19
  Scenario: Multiple thumbnails
17
20
  Given test.jpg file content as request body
18
21
  When I do PUT request http://localhost:3100/thumbnail/crop,16,16,PNG/crop,4,8,JPG/crop,16,32,JPEG
19
- Then I will get multipart response
20
- Then first part mime type will be image/png
21
- And first part will contain PNG image of size 16x16
22
- Then second part mime type will be image/jpeg
23
- And second part will contain JPEG image of size 4x8
24
- Then third part mime type will be image/jpeg
25
- And third part will contain JPEG image of size 16x32
22
+ Then response status will be 200
23
+ And I will get multipart response
24
+ Then first part will contain PNG image of size 16x16
25
+ And first part mime type will be image/png
26
+ Then second part will contain JPEG image of size 4x8
27
+ And second part mime type will be image/jpeg
28
+ Then third part will contain JPEG image of size 16x32
29
+ And third part mime type will be image/jpeg
30
+ And there will be no leaked images
26
31
 
27
32
  Scenario: Transparent image to JPEG handling - default background color white
28
33
  Given test-transparent.png file content as request body
29
34
  When I do PUT request http://localhost:3100/thumbnail/fit,128,128,JPEG
30
- Then I will get multipart response
35
+ Then response status will be 200
36
+ And I will get multipart response
31
37
  And first part body will be saved as test-transparent-default.png for human inspection
32
38
  And first part will contain JPEG image of size 128x128
33
39
  And that image pixel at 32x32 will be of color white
40
+ And there will be no leaked images
34
41
 
35
42
  Scenario: Fit thumbnailing method
36
43
  Given test.jpg file content as request body
37
44
  When I do PUT request http://localhost:3100/thumbnail/fit,128,128,PNG
38
- Then I will get multipart response
45
+ Then response status will be 200
46
+ And I will get multipart response
39
47
  And first part will contain PNG image of size 91x128
48
+ And there will be no leaked images
40
49
 
41
50
  Scenario: Pad thumbnailing method - default background color white
42
51
  Given test.jpg file content as request body
43
52
  When I do PUT request http://localhost:3100/thumbnail/pad,128,128,PNG
44
- Then I will get multipart response
53
+ Then response status will be 200
54
+ And I will get multipart response
45
55
  And first part body will be saved as test-pad.png for human inspection
46
56
  And first part will contain PNG image of size 128x128
47
57
  And that image pixel at 2x2 will be of color white
58
+ And there will be no leaked images
48
59
 
49
60
  Scenario: Pad thumbnailing method with specified background color
50
61
  Given test.jpg file content as request body
51
62
  When I do PUT request http://localhost:3100/thumbnail/pad,128,128,PNG,background-color:green
52
- Then I will get multipart response
63
+ Then response status will be 200
64
+ And I will get multipart response
53
65
  And first part body will be saved as test-pad-background-color.png for human inspection
54
66
  And first part will contain PNG image of size 128x128
55
67
  And that image pixel at 2x2 will be of color green
68
+ And there will be no leaked images
69
+
70
+ Scenario: Image leaking on error
71
+ Given test.jpg file content as request body
72
+ When I do PUT request http://localhost:3100/thumbnail/crop,0,0,PNG/fit,0,0,JPG/pad,0,0,JPEG
73
+ Then response status will be 200
74
+ And I will get multipart response
75
+ And first part content type will be text/plain
76
+ And second part content type will be text/plain
77
+ And third part content type will be text/plain
78
+ And there will be no leaked images
79
+
80
+ Scenario: Reporitng of missing resource
81
+ When I do GET request http://localhost:3100/blah
82
+ Then response status will be 404
83
+ And response content type will be text/plain
84
+ And response body will be CRLF endend lines
85
+ """
86
+ Resource '/blah' not found
87
+ """
88
+
89
+ Scenario: Reporitng of unsupported media type
90
+ Given test.txt file content as request body
91
+ When I do PUT request http://localhost:3100/thumbnail/crop,128,128,PNG
92
+ Then response status will be 415
93
+ And response content type will be text/plain
94
+ And response body will be CRLF endend lines like
95
+ """
96
+ Error: Thumbnailer::UnsupportedMediaTypeError: Magick::ImageMagickError:
97
+ """
98
+
99
+ Scenario: Reporitng of bad thumbanil spec format - missing param
100
+ Given test.txt file content as request body
101
+ When I do PUT request http://localhost:3100/thumbnail/crop,128,PNG
102
+ Then response status will be 500
103
+ And response content type will be text/plain
104
+ And response body will be CRLF endend lines
105
+ """
106
+ Error: ThumbnailSpecs::BadThubnailSpecFormat: missing argument in: crop,128,PNG
107
+ """
108
+
109
+ Scenario: Reporitng of bad thumbanil spec format - bad options format
110
+ Given test.txt file content as request body
111
+ When I do PUT request http://localhost:3100/thumbnail/crop,128,128,PNG,fas-fda
112
+ Then response status will be 500
113
+ And response content type will be text/plain
114
+ And response body will be CRLF endend lines
115
+ """
116
+ Error: ThumbnailSpecs::BadThubnailSpecFormat: missing option key or value in: fas-fda
117
+ """
118
+
119
+ Scenario: Reporitng of image thumbnailing errors
120
+ Given test.jpg file content as request body
121
+ When I do PUT request http://localhost:3100/thumbnail/crop,16,16,PNG/crop,0,0,JPG/crop,16,32,JPEG
122
+ Then response status will be 200
123
+ And I will get multipart response
124
+ Then first part will contain PNG image of size 16x16
125
+ And first part mime type will be image/png
126
+ And second part content type will be text/plain
127
+ And second part body will be CRLF endend lines
128
+ """
129
+ Error: ArgumentError: invalid result dimension (0, 0 given)
130
+ """
131
+ Then third part will contain JPEG image of size 16x32
132
+ And third part mime type will be image/jpeg
133
+ And there will be no leaked images
56
134
 
@@ -1,3 +1,7 @@
1
+ Given /httpthumbnailer log is empty/ do
2
+ (support_dir + 'server.log').truncate(0)
3
+ end
4
+
1
5
  Given /httpthumbnailer server is running at (.*)/ do |url|
2
6
  start_server(
3
7
  "bundle exec #{script('httpthumbnailer')}",
@@ -20,10 +24,37 @@ Then /I will get multipart response/ do
20
24
  @response_multipart = MultipartResponse.new(@response.header['Content-Type'].last, @response.body)
21
25
  end
22
26
 
27
+ Then /response body will be CRLF endend lines like/ do |body|
28
+ @response.body.should match(body)
29
+ @response.body.each do |line|
30
+ line[-2,2].should == "\r\n"
31
+ end
32
+ end
33
+
34
+ Then /response body will be CRLF endend lines$/ do |body|
35
+ @response.body.should == body.gsub("\n", "\r\n") + "\r\n"
36
+ end
37
+
38
+ Then /response status will be (.*)/ do |status|
39
+ @response.status.should == status.to_i
40
+ end
41
+
42
+ Then /response content type will be (.*)/ do |content_type|
43
+ @response.header['Content-Type'].first.should == content_type
44
+ end
45
+
23
46
  Then /(.*) part mime type will be (.*)/ do |part, mime|
24
47
  @response_multipart.part[part_no(part)].header['Content-Type'].should == mime
25
48
  end
26
49
 
50
+ Then /(.*) part content type will be (.*)/ do |part, content_type|
51
+ @response_multipart.part[part_no(part)].header['Content-Type'].should == content_type
52
+ end
53
+
54
+ Then /(.*) part body will be CRLF endend lines$/ do |part, body|
55
+ @response_multipart.part[part_no(part)].body.should == body.gsub("\n", "\r\n") + "\r\n"
56
+ end
57
+
27
58
  Then /(.*) part will contain (.*) image of size (.*)x(.*)/ do |part, format, width, height|
28
59
  mime = @response_multipart.part[part_no(part)].header['Content-Type']
29
60
  data = @response_multipart.part[part_no(part)].body
@@ -46,3 +77,8 @@ And /that image pixel at (.*)x(.*) will be of color (.*)/ do |x, y, color|
46
77
  @image.pixel_color(x.to_i, y.to_i).to_color.sub(/^#/, '0x').should == color
47
78
  end
48
79
 
80
+
81
+ And /there will be no leaked images/ do
82
+ HTTPClient.new.get_content("http://localhost:3100/stats/images").to_i.should == 0
83
+ end
84
+
@@ -0,0 +1 @@
1
+ hello world
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "httpthumbnailer"
8
- s.version = "0.0.3"
8
+ s.version = "0.0.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jakub Pastuszek"]
12
- s.date = "2011-11-28"
12
+ s.date = "2011-11-29"
13
13
  s.description = "Provides HTTP API for thumbnailing images"
14
14
  s.email = "jpastuszek@gmail.com"
15
15
  s.executables = ["httpthumbnailer"]
@@ -32,6 +32,7 @@ Gem::Specification.new do |s|
32
32
  "features/support/env.rb",
33
33
  "features/support/test-transparent.png",
34
34
  "features/support/test.jpg",
35
+ "features/support/test.txt",
35
36
  "httpthumbnailer.gemspec",
36
37
  "lib/httpthumbnailer/multipart_response.rb",
37
38
  "lib/httpthumbnailer/thumbnail_specs.rb",
@@ -1,16 +1,22 @@
1
1
  require 'httpthumbnailer/thumbnailer'
2
2
 
3
3
  class ThumbnailSpecs < Array
4
+ class BadThubnailSpecFormat < ArgumentError
5
+ end
6
+
4
7
  def self.from_uri(specs)
5
8
  ts = ThumbnailSpecs.new
6
9
  specs.split('/').each do |spec|
7
10
  method, width, height, format, *options = *spec.split(',')
11
+ raise BadThubnailSpecFormat, "missing argument in: #{spec}" unless method and width and height and format
12
+
8
13
  width = width.to_i
9
14
  height = height.to_i
10
15
 
11
16
  opts = {}
12
17
  options.each do |option|
13
18
  key, value = option.split(':')
19
+ raise BadThubnailSpecFormat, "missing option key or value in: #{option}" unless key and value
14
20
  opts[key] = value
15
21
  end
16
22
 
@@ -1,5 +1,18 @@
1
1
  require 'logger'
2
2
 
3
+ class Magick::Image
4
+ def render_on_background!(background_color)
5
+ Thumbnailer::ImageHandler.new do
6
+ self
7
+ end.use do |image|
8
+ Magick::Image.new(image.columns, image.rows) {
9
+ self.background_color = background_color
10
+ }.composite!(image, Magick::CenterGravity, Magick::OverCompositeOp)
11
+ end
12
+ end
13
+ end
14
+
15
+
3
16
  class ThumbnailSpec
4
17
  def initialize(method, width, height, format, options = {})
5
18
  @method = method
@@ -31,55 +44,120 @@ class Thumbnailer
31
44
  end
32
45
  end
33
46
 
34
- class ImageNotFound < ArgumentError
35
- def initialize(id)
36
- super("No image of ID '#{id}' found")
47
+ class UnsupportedMediaTypeError < ArgumentError
48
+ def initialize(e)
49
+ super("#{e.class.name}: #{e}")
50
+ end
51
+ end
52
+
53
+ class ImageHandler
54
+ class ImageDestroyedError < RuntimeError
55
+ def initialize
56
+ super("image was already used")
57
+ end
58
+ end
59
+
60
+ def initialize
61
+ @image = yield
62
+ end
63
+
64
+ def use
65
+ raise ImageDestroyedError unless @image
66
+ begin
67
+ yield @image
68
+ ensure
69
+ @image.destroy!
70
+ @image = nil
71
+ end
72
+ end
73
+
74
+ def destroy!
75
+ return unless @image
76
+ @image.destroy!
77
+ @image = nil
78
+ end
79
+ end
80
+
81
+ class OriginalImage
82
+ def initialize(io, methods, options = {})
83
+ @options = options
84
+ @logger = (options[:logger] or Logger.new('/dev/null'))
85
+
86
+ begin
87
+ @image = Magick::Image.from_blob(io.read).first.strip!
88
+ rescue Magick::ImageMagickError => e
89
+ raise UnsupportedMediaTypeError, e
90
+ end
91
+ @methods = methods
92
+ end
93
+
94
+ def thumbnail(spec)
95
+ ImageHandler.new do
96
+ process_image(@image, spec).render_on_background!((spec.options['background-color'] or 'white').sub(/^0x/, '#'))
97
+ end.use do |thumb|
98
+ thumb.to_blob do |inf|
99
+ inf.format = spec.format
100
+ end
101
+ end
102
+ end
103
+
104
+ def destroy!
105
+ @image.destroy!
106
+ end
107
+
108
+ private
109
+
110
+ def process_image(image, spec)
111
+ impl = @methods[spec.method] or raise UnsupportedMethodError.new(spec.method)
112
+ copy = image.copy
113
+ begin
114
+ impl.call(copy, spec)
115
+ rescue
116
+ copy.destroy!
117
+ raise
118
+ end
37
119
  end
38
120
  end
39
121
 
40
122
  def initialize(options = {})
41
- @images = {}
42
123
  @methods = {}
43
124
  @options = options
44
- @options = options
45
125
  @logger = (options[:logger] or Logger.new('/dev/null'))
46
- end
47
126
 
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!
127
+ @logger.info "Initializing thumbniler"
52
128
 
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)
129
+ @images = 0
130
+ Magick.trace_proc = lambda do |which, description, id, method|
131
+ case which
132
+ when :c
133
+ @images += 1
134
+ when :d
135
+ @images -= 1
136
+ end
137
+ @logger.debug "Image event: #{which}, #{description}, #{id}, #{method}: loaded images: #{images}"
61
138
  end
62
139
  end
63
140
 
64
- def method(method, &impl)
65
- @methods[method] = impl
66
- end
141
+ def load(io)
142
+ h = ImageHandler.new do
143
+ OriginalImage.new(io, @methods, @options)
144
+ end
67
145
 
68
- def thumbnail(id, spec)
69
- image = @images[id] or raise ImageNotFound.new(id)
70
- thumb = process_image(image, spec)
71
- replace_transparency(thumb, spec)
146
+ begin
147
+ yield h
148
+ rescue
149
+ # make sure that we destroy original image if there was an error before it could be used
150
+ h.destroy!
151
+ raise
152
+ end
72
153
  end
73
154
 
74
- def replace_transparency(image, spec)
75
- Magick::Image.new(image.columns, image.rows) {
76
- self.background_color = (spec.options['background-color'] or 'white').sub(/^0x/, '#')
77
- }.composite!(image, Magick::CenterGravity, Magick::OverCompositeOp)
155
+ def images
156
+ @images
78
157
  end
79
158
 
80
- def process_image(image, spec)
81
- impl = @methods[spec.method] or raise UnsupportedMethodError.new(spec.method)
82
- impl.call(image, spec)
159
+ def method(method, &impl)
160
+ @methods[method] = impl
83
161
  end
84
162
  end
85
163
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpthumbnailer
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 3
10
- version: 0.0.3
9
+ - 4
10
+ version: 0.0.4
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jakub Pastuszek
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-11-28 00:00:00 Z
18
+ date: 2011-11-29 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  type: :runtime
@@ -221,6 +221,7 @@ files:
221
221
  - features/support/env.rb
222
222
  - features/support/test-transparent.png
223
223
  - features/support/test.jpg
224
+ - features/support/test.txt
224
225
  - httpthumbnailer.gemspec
225
226
  - lib/httpthumbnailer/multipart_response.rb
226
227
  - lib/httpthumbnailer/thumbnail_specs.rb