httpthumbnailer 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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