rack-thumb 0.2.3
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/.gitignore +3 -0
- data/README.rdoc +85 -0
- data/Rakefile +55 -0
- data/example/file.ru +7 -0
- data/example/frank.rb +14 -0
- data/example/public/images/imagick.jpg +0 -0
- data/example/static.ru +10 -0
- data/lib/rack/thumb.rb +249 -0
- data/rack-thumb.gemspec +57 -0
- data/spec/base_spec.rb +165 -0
- data/spec/helpers.rb +16 -0
- data/spec/media/imagick.jpg +0 -0
- metadata +86 -0
data/.gitignore
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
== Rack::Thumb: Drop-in image thumbnailing for your Rack stack
|
2
|
+
|
3
|
+
<tt>Rack::Thumb</tt> is drop-in dynamic thumbnailing middleware for Rack-based
|
4
|
+
applications, featuring simple configuration, optional security (via url-signing),
|
5
|
+
and maximum flexibility.
|
6
|
+
|
7
|
+
== Getting Started
|
8
|
+
|
9
|
+
You will need ImageMagick and the Mapel gem (http://github.com/akdubya/mapel).
|
10
|
+
|
11
|
+
gem install akdubya-rack-thumb
|
12
|
+
|
13
|
+
# rackup.ru
|
14
|
+
require 'myapp'
|
15
|
+
require 'rack/thumb'
|
16
|
+
|
17
|
+
use Rack::Thumb
|
18
|
+
use Rack::Static, :urls => ["/media"]
|
19
|
+
|
20
|
+
run MyApp.new
|
21
|
+
|
22
|
+
<tt>Rack::Thumb</tt> is file-server agnostic to provide maximum deployment
|
23
|
+
flexibility. Simply set it up in front of any application that's capable of
|
24
|
+
serving source images (I'm using it with an app that serves images from CouchDB).
|
25
|
+
|
26
|
+
See the example directory for more <tt>Rack</tt> configurations. Because
|
27
|
+
thumbnailing is an expensive operation, you should run <tt>Rack::Thumb</tt>
|
28
|
+
behind a cache, such as the excellent <tt>Rack::Cache</tt>.
|
29
|
+
|
30
|
+
== Rendering Options
|
31
|
+
|
32
|
+
<tt>Rack::Thumb</tt> intercepts requests for images that have urls of
|
33
|
+
the form <code>/path/to/image_{metadata}.ext</code> and returns rendered
|
34
|
+
thumbnails. Rendering options include +width+, +height+ and +gravity+. If
|
35
|
+
both +width+ and +height+ are supplied, images are cropped and resized
|
36
|
+
to fit the aspect ratio.
|
37
|
+
|
38
|
+
Link to thumbnails from your templates as follows:
|
39
|
+
|
40
|
+
/media/foobar_50x50.jpg # => Crop and resize to 50x50
|
41
|
+
/media/foobar_50x50-nw.jpg # => Crop and resize with northwest gravity
|
42
|
+
/media/foobar_50x.jpg # => Resize to a width of 50, preserving AR
|
43
|
+
/media/foobar_x50.jpg # => Resize to a height of 50, preserving AR
|
44
|
+
|
45
|
+
== URL Signing
|
46
|
+
|
47
|
+
To prevent pesky end-users and bots from flooding your application with
|
48
|
+
render requests you can set up <tt>Rack::Thumb</tt> to check for a <tt>SHA-1</tt>
|
49
|
+
signature that is unique to every url. Using this option, only thumbnails requested
|
50
|
+
by your templates will be valid. Example:
|
51
|
+
|
52
|
+
use Rack::Thumb, {
|
53
|
+
:secret => "My secret", # => Don't tell anyone!
|
54
|
+
:keylength => "16" # => Only use 16 digits of the SHA-1 key
|
55
|
+
}
|
56
|
+
|
57
|
+
You can then use your +secret+ to generate secure links in your templates using
|
58
|
+
Ruby's built-in <tt>Digest::SHA1</tt> library:
|
59
|
+
|
60
|
+
/media/foobar_50x100-sw-a267c193a7eff046.jpg # => Successful
|
61
|
+
/media/foobar_120x250-a267c193a7eff046.jpg # => Returns a bad request error
|
62
|
+
|
63
|
+
There are no helper modules just yet but it's easy enough to roll your own.
|
64
|
+
|
65
|
+
== Deep Thoughts
|
66
|
+
|
67
|
+
<tt>Rack::Thumb</tt> respects any extra headers you set in your downstream app.
|
68
|
+
You are free to set caching policies, etc. however you like. Incoming HEAD requests
|
69
|
+
skip the rendering step.
|
70
|
+
|
71
|
+
There are a decent number of specs but the middleware isn't very strict at
|
72
|
+
checking setup options at the moment, and I'm sure there are a few edge cases
|
73
|
+
that need to be looked into. Comments, suggestions and bug reports are welcome.
|
74
|
+
|
75
|
+
== Meta
|
76
|
+
|
77
|
+
Written by Aleks Williams (http://github.com/akdubya)
|
78
|
+
|
79
|
+
Credit goes to the repoze.bitblt (http://pypi.python.org/pypi/repoze.bitblt)
|
80
|
+
team for the clever url-signing implementation. My original security scheme
|
81
|
+
was stupidly complex.
|
82
|
+
|
83
|
+
Released under the MIT License: www.opensource.org/licenses/mit-license.php
|
84
|
+
|
85
|
+
github.com/akdubya/rack-thumb
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
name = 'rack-thumb'
|
5
|
+
version = '0.2.3'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
Jeweler::Tasks.new do |gem|
|
10
|
+
gem.name = name
|
11
|
+
gem.version = version
|
12
|
+
gem.summary = %Q{Drop-in image thumbnailing for your Rack stack.}
|
13
|
+
gem.email = "alekswilliams@earthlink.net"
|
14
|
+
gem.homepage = "http://github.com/akdubya/rack-thumb"
|
15
|
+
gem.authors = ["Aleksander Williams"]
|
16
|
+
gem.add_dependency "mapel", ">= 0.1.6"
|
17
|
+
gem.add_development_dependency "bacon", ">= 0"
|
18
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
19
|
+
end
|
20
|
+
Jeweler::GemcutterTasks.new
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'rake/testtask'
|
26
|
+
Rake::TestTask.new(:spec) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
29
|
+
spec.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'rcov/rcovtask'
|
34
|
+
Rcov::RcovTask.new do |spec|
|
35
|
+
spec.libs << 'spec'
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.verbose = true
|
38
|
+
end
|
39
|
+
rescue LoadError
|
40
|
+
task :rcov do
|
41
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
task :spec => :check_dependencies
|
46
|
+
|
47
|
+
task :default => :spec
|
48
|
+
|
49
|
+
require 'rake/rdoctask'
|
50
|
+
Rake::RDocTask.new do |rdoc|
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "rack-thumb #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
data/example/file.ru
ADDED
data/example/frank.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'thin'
|
3
|
+
require 'sinatra'
|
4
|
+
require 'rack/cache'
|
5
|
+
require 'rack/thumb'
|
6
|
+
|
7
|
+
use Rack::Cache,
|
8
|
+
:metastore => 'file:/var/cache/rack/meta',
|
9
|
+
:entitystore => 'file:/var/cache/rack/body'
|
10
|
+
use Rack::Thumb
|
11
|
+
|
12
|
+
get '/' do
|
13
|
+
"Hello World!"
|
14
|
+
end
|
Binary file
|
data/example/static.ru
ADDED
data/lib/rack/thumb.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'mapel'
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
require 'tempfile'
|
6
|
+
|
7
|
+
Tempfile.class_eval do
|
8
|
+
def make_tmpname(basename, n)
|
9
|
+
ext = nil
|
10
|
+
sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Rack
|
15
|
+
|
16
|
+
# The Rack::Thumb middleware intercepts requests for images that have urls of
|
17
|
+
# the form <code>/path/to/image_{metadata}.ext</code> and returns rendered
|
18
|
+
# thumbnails. Rendering options include +width+, +height+ and +gravity+. If
|
19
|
+
# both +width+ and +height+ are supplied, images are cropped and resized
|
20
|
+
# to fit the aspect ratio.
|
21
|
+
#
|
22
|
+
# Rack::Thumb is file-server agnostic to provide maximum deployment
|
23
|
+
# flexibility. Simply set it up in front of any downstream application that
|
24
|
+
# can serve the source images. Example:
|
25
|
+
#
|
26
|
+
# # rackup.ru
|
27
|
+
# require 'rack/thumb'
|
28
|
+
#
|
29
|
+
# use Rack::Thumb
|
30
|
+
# use Rack::Static, :urls => ["/media"]
|
31
|
+
#
|
32
|
+
# run MyApp.new
|
33
|
+
#
|
34
|
+
# See the example directory for more <tt>Rack</tt> configurations. Because
|
35
|
+
# thumbnailing is an expensive operation, you should run Rack::Thumb
|
36
|
+
# behind a cache, such as <tt>Rack::Cache</tt>.
|
37
|
+
#
|
38
|
+
# Link to thumbnails from your templates as follows:
|
39
|
+
#
|
40
|
+
# /media/foobar_50x50.jpg # => Crop and resize to 50x50
|
41
|
+
# /media/foobar_50x50-nw.jpg # => Crop and resize with northwest gravity
|
42
|
+
# /media/foobar_50x.jpg # => Resize to a width of 50, preserving AR
|
43
|
+
# /media/foobar_x50.jpg # => Resize to a height of 50, preserving AR
|
44
|
+
#
|
45
|
+
# To prevent pesky end-users and bots from flooding your application with
|
46
|
+
# render requests you can set up Rack::Thumb to check for a <tt>SHA-1</tt> signature
|
47
|
+
# that is unique to every url. Using this option, only thumbnails requested
|
48
|
+
# by your templates will be valid. Example:
|
49
|
+
#
|
50
|
+
# use Rack::Thumb, {
|
51
|
+
# :secret => "My secret",
|
52
|
+
# :keylength => "16" # => Only use 16 digits of the SHA-1 key
|
53
|
+
# }
|
54
|
+
#
|
55
|
+
# You can then use your +secret+ to generate secure links in your templates:
|
56
|
+
#
|
57
|
+
# /media/foobar_50x100-sw-a267c193a7eff046.jpg # => Successful
|
58
|
+
# /media/foobar_120x250-a267c193a7eff046.jpg # => Returns a bad request error
|
59
|
+
#
|
60
|
+
|
61
|
+
class Thumb
|
62
|
+
RE_TH_BASE = /_([0-9]+x|x[0-9]+|[0-9]+x[0-9]+)(-(?:nw|n|ne|w|c|e|sw|s|se))?/
|
63
|
+
RE_TH_EXT = /(\.(?:jpg|jpeg|png|gif))/i
|
64
|
+
TH_GRAV = {
|
65
|
+
'-nw' => :northwest,
|
66
|
+
'-n' => :north,
|
67
|
+
'-ne' => :northeast,
|
68
|
+
'-w' => :west,
|
69
|
+
'-c' => :center,
|
70
|
+
'-e' => :east,
|
71
|
+
'-sw' => :southwest,
|
72
|
+
'-s' => :south,
|
73
|
+
'-se' => :southeast
|
74
|
+
}
|
75
|
+
|
76
|
+
def initialize(app, options={})
|
77
|
+
@app = app
|
78
|
+
@keylen = options[:keylength]
|
79
|
+
@secret = options[:secret]
|
80
|
+
@routes = generate_routes(options[:urls] || ["/"], options[:prefix])
|
81
|
+
end
|
82
|
+
|
83
|
+
# Generates routes given a list of prefixes.
|
84
|
+
def generate_routes(urls, prefix = nil)
|
85
|
+
urls.map do |url|
|
86
|
+
prefix = prefix ? Regexp.escape(prefix) : ''
|
87
|
+
url = url == "/" ? '' : Regexp.escape(url)
|
88
|
+
if @keylen
|
89
|
+
/^#{prefix}(#{url}\/.+)#{RE_TH_BASE}-([0-9a-f]{#{@keylen}})#{RE_TH_EXT}$/
|
90
|
+
else
|
91
|
+
/^#{prefix}(#{url}\/.+)#{RE_TH_BASE}#{RE_TH_EXT}$/
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def call(env)
|
97
|
+
dup._call(env)
|
98
|
+
end
|
99
|
+
|
100
|
+
def _call(env)
|
101
|
+
response = catch(:halt) do
|
102
|
+
throw :halt unless %w{GET HEAD}.include? env["REQUEST_METHOD"]
|
103
|
+
@env = env
|
104
|
+
@path = env["PATH_INFO"]
|
105
|
+
@routes.each do |regex|
|
106
|
+
if match = @path.match(regex)
|
107
|
+
@source, dim, grav = extract_meta(match)
|
108
|
+
@image = get_source_image
|
109
|
+
@thumb = render_thumbnail(dim, grav) unless head?
|
110
|
+
serve
|
111
|
+
end
|
112
|
+
end
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
response || @app.call(env)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Extracts filename and options from the path.
|
120
|
+
def extract_meta(match)
|
121
|
+
result = if @keylen
|
122
|
+
extract_signed_meta(match)
|
123
|
+
else
|
124
|
+
extract_unsigned_meta(match)
|
125
|
+
end
|
126
|
+
|
127
|
+
throw :halt unless result
|
128
|
+
result
|
129
|
+
end
|
130
|
+
|
131
|
+
# Extracts filename and options from a signed path.
|
132
|
+
def extract_signed_meta(match)
|
133
|
+
base, dim, grav, sig, ext = match.captures
|
134
|
+
digest = Digest::SHA1.hexdigest("#{base}_#{dim}#{grav}#{ext}#{@secret}")[0..@keylen-1]
|
135
|
+
throw(:halt, bad_request) unless sig && (sig == digest)
|
136
|
+
[base + ext, dim, grav]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Extracts filename and options from an unsigned path.
|
140
|
+
def extract_unsigned_meta(match)
|
141
|
+
base, dim, grav, ext = match.captures
|
142
|
+
[base + ext, dim, grav]
|
143
|
+
end
|
144
|
+
|
145
|
+
# Fetch the source image from the downstream app, returning the downstream
|
146
|
+
# app's response if it is not a success.
|
147
|
+
def get_source_image
|
148
|
+
status, headers, body = @app.call(@env.merge(
|
149
|
+
"PATH_INFO" => @source
|
150
|
+
))
|
151
|
+
|
152
|
+
unless (status >= 200 && status < 300) &&
|
153
|
+
(headers["Content-Type"].split("/").first == "image")
|
154
|
+
throw :halt, [status, headers, body]
|
155
|
+
end
|
156
|
+
|
157
|
+
@source_headers = headers
|
158
|
+
|
159
|
+
if !head?
|
160
|
+
if body.respond_to?(:path)
|
161
|
+
::File.open(body.path, 'rb')
|
162
|
+
elsif body.respond_to?(:each)
|
163
|
+
data = ''
|
164
|
+
body.each { |part| data << part.to_s }
|
165
|
+
Tempfile.new(::File.basename(@path)).tap do |f|
|
166
|
+
f.binmode
|
167
|
+
f.write(data)
|
168
|
+
f.close
|
169
|
+
end
|
170
|
+
end
|
171
|
+
else
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Renders a thumbnail from the source image. Returns a Tempfile.
|
177
|
+
def render_thumbnail(dim, grav)
|
178
|
+
gravity = grav ? TH_GRAV[grav] : :center
|
179
|
+
width, height = parse_dimensions(dim)
|
180
|
+
origin_width, origin_height = Mapel.info(@image.path)[:dimensions]
|
181
|
+
width = [width, origin_width].min if width
|
182
|
+
height = [height, origin_height].min if height
|
183
|
+
output = create_tempfile
|
184
|
+
cmd = Mapel(@image.path).gravity(gravity)
|
185
|
+
if width && height
|
186
|
+
cmd.resize!(width, height)
|
187
|
+
else
|
188
|
+
cmd.resize(width, height, 0, 0, '>')
|
189
|
+
end
|
190
|
+
cmd.to(output.path).run
|
191
|
+
output
|
192
|
+
end
|
193
|
+
|
194
|
+
# Serves the thumbnail. If this is a HEAD request we strip the body as well
|
195
|
+
# as the content length because the render was never run.
|
196
|
+
def serve
|
197
|
+
response = if head?
|
198
|
+
@source_headers.delete("Content-Length")
|
199
|
+
[200, @source_headers, []]
|
200
|
+
else
|
201
|
+
[200, @source_headers.merge("Content-Length" => ::File.size(@thumb.path).to_s), self]
|
202
|
+
end
|
203
|
+
|
204
|
+
throw :halt, response
|
205
|
+
end
|
206
|
+
|
207
|
+
# Parses the rendering options; returns false if rendering options are invalid
|
208
|
+
def parse_dimensions(meta)
|
209
|
+
dimensions = meta.split('x').map do |dim|
|
210
|
+
if dim.empty?
|
211
|
+
nil
|
212
|
+
elsif dim[0].to_i == 0
|
213
|
+
throw :halt, bad_request
|
214
|
+
else
|
215
|
+
dim.to_i
|
216
|
+
end
|
217
|
+
end
|
218
|
+
dimensions.any? ? dimensions : throw(:halt, bad_request)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Creates a new tempfile
|
222
|
+
def create_tempfile
|
223
|
+
Tempfile.new(::File.basename(@path)).tap { |f| f.close }
|
224
|
+
end
|
225
|
+
|
226
|
+
def bad_request
|
227
|
+
body = "Bad thumbnail parameters in #{@path}\n"
|
228
|
+
[400, {"Content-Type" => "text/plain",
|
229
|
+
"Content-Length" => body.size.to_s},
|
230
|
+
[body]]
|
231
|
+
end
|
232
|
+
|
233
|
+
def head?
|
234
|
+
@env["REQUEST_METHOD"] == "HEAD"
|
235
|
+
end
|
236
|
+
|
237
|
+
def each
|
238
|
+
::File.open(@thumb.path, "rb") { |file|
|
239
|
+
while part = file.read(8192)
|
240
|
+
yield part
|
241
|
+
end
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
def to_path
|
246
|
+
@thumb.path
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
data/rack-thumb.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{rack-thumb}
|
8
|
+
s.version = "0.2.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Aleksander Williams"]
|
12
|
+
s.date = %q{2010-02-25}
|
13
|
+
s.email = %q{alekswilliams@earthlink.net}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"README.rdoc"
|
16
|
+
]
|
17
|
+
s.files = [
|
18
|
+
".gitignore",
|
19
|
+
"README.rdoc",
|
20
|
+
"Rakefile",
|
21
|
+
"example/file.ru",
|
22
|
+
"example/frank.rb",
|
23
|
+
"example/public/images/imagick.jpg",
|
24
|
+
"example/static.ru",
|
25
|
+
"lib/rack/thumb.rb",
|
26
|
+
"rack-thumb.gemspec",
|
27
|
+
"spec/base_spec.rb",
|
28
|
+
"spec/helpers.rb",
|
29
|
+
"spec/media/imagick.jpg"
|
30
|
+
]
|
31
|
+
s.homepage = %q{http://github.com/akdubya/rack-thumb}
|
32
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
s.rubygems_version = %q{1.3.5}
|
35
|
+
s.summary = %q{Drop-in image thumbnailing for your Rack stack.}
|
36
|
+
s.test_files = [
|
37
|
+
"spec/base_spec.rb",
|
38
|
+
"spec/helpers.rb"
|
39
|
+
]
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 3
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
46
|
+
s.add_runtime_dependency(%q<mapel>, [">= 0.1.6"])
|
47
|
+
s.add_development_dependency(%q<bacon>, [">= 0"])
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<mapel>, [">= 0.1.6"])
|
50
|
+
s.add_dependency(%q<bacon>, [">= 0"])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<mapel>, [">= 0.1.6"])
|
54
|
+
s.add_dependency(%q<bacon>, [">= 0"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helpers'
|
2
|
+
|
3
|
+
describe Rack::Thumb do
|
4
|
+
before do
|
5
|
+
@app = Rack::File.new(::File.dirname(__FILE__))
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should render a thumbnail with width only" do
|
9
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
10
|
+
|
11
|
+
res = request.get("/media/imagick_50x.jpg")
|
12
|
+
res.should.be.ok
|
13
|
+
res.content_type.should == "image/jpeg"
|
14
|
+
info = image_info(res.body)
|
15
|
+
info[:dimensions].should == [50, 52]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should render a thumbnail with height only" do
|
19
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
20
|
+
|
21
|
+
res = request.get("/media/imagick_x50.jpg")
|
22
|
+
res.should.be.ok
|
23
|
+
res.content_type.should == "image/jpeg"
|
24
|
+
info = image_info(res.body)
|
25
|
+
info[:dimensions].should == [48, 50]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should render a thumbnail with width and height (crop-resize)" do
|
29
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
30
|
+
|
31
|
+
res = request.get("/media/imagick_50x50.jpg")
|
32
|
+
res.should.be.ok
|
33
|
+
res.content_type.should == "image/jpeg"
|
34
|
+
info = image_info(res.body)
|
35
|
+
info[:dimensions].should == [50, 50]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should render a thumbnail with width, height and gravity (crop-resize)" do
|
39
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
40
|
+
|
41
|
+
res = request.get("/media/imagick_50x100-sw.jpg")
|
42
|
+
res.should.be.ok
|
43
|
+
res.content_type.should == "image/jpeg"
|
44
|
+
info = image_info(res.body)
|
45
|
+
info[:dimensions].should == [50, 100]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should render a thumbnail with a signature" do
|
49
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app, :keylength => 16,
|
50
|
+
:secret => "test"))
|
51
|
+
|
52
|
+
sig = Digest::SHA1.hexdigest("/media/imagick_50x100-sw.jpgtest")[0..15]
|
53
|
+
res = request.get("/media/imagick_50x100-sw-#{sig}.jpg")
|
54
|
+
res.should.be.ok
|
55
|
+
res.content_type.should == "image/jpeg"
|
56
|
+
info = image_info(res.body)
|
57
|
+
info[:dimensions].should == [50, 100]
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should not render a thumbnail that exceeds the original image's dimensions" do
|
61
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
62
|
+
|
63
|
+
res = request.get("/media/imagick_1000x1000.jpg")
|
64
|
+
res.should.be.ok
|
65
|
+
res.content_type.should == "image/jpeg"
|
66
|
+
info = image_info(res.body)
|
67
|
+
info[:dimensions].should == [572, 591]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should work with non-file source bodies" do
|
71
|
+
app = lambda { |env| [200, {"Content-Type" => "image/jpeg"},
|
72
|
+
[::File.read(::File.dirname(__FILE__) + "/media/imagick.jpg")]] }
|
73
|
+
|
74
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(app))
|
75
|
+
|
76
|
+
res = request.get("/media/imagick_50x.jpg")
|
77
|
+
res.should.be.ok
|
78
|
+
res.content_type.should == "image/jpeg"
|
79
|
+
info = image_info(res.body)
|
80
|
+
info[:dimensions].should == [50, 52]
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should return bad request if the signature is invalid" do
|
84
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app, :keylength => 16,
|
85
|
+
:secret => "test"))
|
86
|
+
|
87
|
+
res = request.get("/media/imagick_50x100-sw-9922d04b14049f85.jpg")
|
88
|
+
res.should.be.client_error
|
89
|
+
res.body.should == "Bad thumbnail parameters in /media/imagick_50x100-sw-9922d04b14049f85.jpg\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should return bad request if the dimensions are bad" do
|
93
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
94
|
+
|
95
|
+
res = request.get("/media/imagick_0x50.jpg")
|
96
|
+
res.should.be.client_error
|
97
|
+
res.body.should == "Bad thumbnail parameters in /media/imagick_0x50.jpg\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should return bad request if dimensions contain leading zeroes" do
|
101
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
102
|
+
|
103
|
+
res = request.get("/media/imagick_50x050.jpg")
|
104
|
+
res.should.be.client_error
|
105
|
+
res.body.should == "Bad thumbnail parameters in /media/imagick_50x050.jpg\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should return the application's response if the source file is not found" do
|
109
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
110
|
+
|
111
|
+
res = request.get("/media/dummy_50x50.jpg")
|
112
|
+
res.should.be.not_found
|
113
|
+
res.body.should == "File not found: /media/dummy.jpg\n"
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should return the application's response if it does not recognize render options" do
|
117
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app, :keylength => 16,
|
118
|
+
:secret => "test"))
|
119
|
+
|
120
|
+
res = request.get("/media/imagick_50x50!.jpg")
|
121
|
+
res.should.be.not_found
|
122
|
+
res.body.should == "File not found: /media/imagick_50x50!.jpg\n"
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should pass non-thumbnail image requests to the application" do
|
126
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app, :keylength => 16,
|
127
|
+
:secret => "test"))
|
128
|
+
|
129
|
+
res = request.get("/media/imagick.jpg")
|
130
|
+
res.should.be.ok
|
131
|
+
res.content_type.should == "image/jpeg"
|
132
|
+
res.content_length.should == 97374
|
133
|
+
res.body.bytesize.should == 97374
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should not render on a HEAD request" do
|
137
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
138
|
+
|
139
|
+
res = request.request("HEAD", "/media/imagick_50x50.jpg")
|
140
|
+
res.should.be.ok
|
141
|
+
res.content_type.should == "image/jpeg"
|
142
|
+
res.content_length.should.be.nil
|
143
|
+
res.body.bytesize.should == 0
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should preserve any extra headers provided by the downstream app" do
|
147
|
+
app = lambda { |env| [200, {"X-Foo" => "bar", "Content-Type" => "image/jpeg"},
|
148
|
+
::File.open(::File.dirname(__FILE__) + "/media/imagick.jpg")] }
|
149
|
+
|
150
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(app))
|
151
|
+
|
152
|
+
res = request.request("HEAD", "/media/imagick_50x50.jpg")
|
153
|
+
res.should.be.ok
|
154
|
+
res.content_type.should == "image/jpeg"
|
155
|
+
res.content_length.should.be.nil
|
156
|
+
res.headers["X-Foo"].should == "bar"
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should forward POST/PUT/DELETE requests to the downstream app" do
|
160
|
+
request = Rack::MockRequest.new(Rack::Thumb.new(@app))
|
161
|
+
|
162
|
+
res = request.post("/media/imagick_50x50.jpg")
|
163
|
+
res.should.not.be.successful
|
164
|
+
end
|
165
|
+
end
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bacon'
|
2
|
+
require File.dirname(File.dirname(__FILE__)) + '/lib/rack/thumb'
|
3
|
+
require 'rack/mock'
|
4
|
+
|
5
|
+
class String
|
6
|
+
def each(*args, &block)
|
7
|
+
each_line(*args, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def image_info(body)
|
12
|
+
t = Tempfile.new('foo.jpg').tap {|f| f.binmode; f.write(body); f.close }
|
13
|
+
Mapel.info(t.path)
|
14
|
+
end
|
15
|
+
|
16
|
+
Bacon.summary_on_exit
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-thumb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aleksander Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-25 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mapel
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.1.6
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bacon
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: alekswilliams@earthlink.net
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.rdoc
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- README.rdoc
|
46
|
+
- Rakefile
|
47
|
+
- example/file.ru
|
48
|
+
- example/frank.rb
|
49
|
+
- example/public/images/imagick.jpg
|
50
|
+
- example/static.ru
|
51
|
+
- lib/rack/thumb.rb
|
52
|
+
- rack-thumb.gemspec
|
53
|
+
- spec/base_spec.rb
|
54
|
+
- spec/helpers.rb
|
55
|
+
- spec/media/imagick.jpg
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/akdubya/rack-thumb
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --charset=UTF-8
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.3.5
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Drop-in image thumbnailing for your Rack stack.
|
84
|
+
test_files:
|
85
|
+
- spec/base_spec.rb
|
86
|
+
- spec/helpers.rb
|