Class: Rack::Thumb
- Inherits:
-
Object
- Object
- Rack::Thumb
- Defined in:
- lib/rack/thumb.rb
Overview
The Rack::Thumb middleware intercepts requests for images that have urls of
the form <code>/path/to/image_{metadata}.ext</code> and returns rendered
thumbnails. Rendering options include +width+, +height+ and +gravity+. If
both +width+ and +height+ are supplied, images are cropped and resized
to fit the aspect ratio.
Rack::Thumb is file-server agnostic to provide maximum deployment
flexibility. Simply set it up in front of any downstream application that
can serve the source images. Example:
# rackup.ru
require 'rack/thumb'
use Rack::Thumb
use Rack::Static, :urls => ["/media"]
run MyApp.new
See the example directory for more <tt>Rack</tt> configurations. Because
thumbnailing is an expensive operation, you should run Rack::Thumb
behind a cache, such as <tt>Rack::Cache</tt>.
Link to thumbnails from your templates as follows:
/media/foobar_50x50.jpg # => Crop and resize to 50x50
/media/foobar_50x50-nw.jpg # => Crop and resize with northwest gravity
/media/foobar_50x.jpg # => Resize to a width of 50, preserving AR
/media/foobar_x50.jpg # => Resize to a height of 50, preserving AR
To prevent pesky end-users and bots from flooding your application with
render requests you can set up Rack::Thumb to check for a <tt>SHA-1</tt> signature
that is unique to every url. Using this option, only thumbnails requested
by your templates will be valid. Example:
use Rack::Thumb, {
:secret => "My secret",
:keylength => "16" # => Only use 16 digits of the SHA-1 key
}
You can then use your +secret+ to generate secure links in your templates:
/media/foobar_50x100-sw-a267c193a7eff046.jpg # => Successful
/media/foobar_120x250-a267c193a7eff046.jpg # => Returns a bad request error
Constant Summary collapse
- RE_TH_BASE =
/_([0-9]+x|x[0-9]+|[0-9]+x[0-9]+)(-(?:nw|n|ne|w|c|e|sw|s|se))?/
- RE_TH_EXT =
/(\.(?:jpg|jpeg|png|gif))/i
- TH_GRAV =
{ '-nw' => :northwest, '-n' => :north, '-ne' => :northeast, '-w' => :west, '-c' => :center, '-e' => :east, '-sw' => :southwest, '-s' => :south, '-se' => :southeast }
Instance Method Summary collapse
- #_call(env) ⇒ Object
- #bad_request ⇒ Object
- #call(env) ⇒ Object
-
#create_tempfile ⇒ Object
Creates a new tempfile.
- #each ⇒ Object
-
#extract_meta(match) ⇒ Object
Extracts filename and options from the path.
-
#extract_signed_meta(match) ⇒ Object
Extracts filename and options from a signed path.
-
#extract_unsigned_meta(match) ⇒ Object
Extracts filename and options from an unsigned path.
-
#generate_routes(urls, prefix = nil) ⇒ Object
Generates routes given a list of prefixes.
-
#get_source_image ⇒ Object
Fetch the source image from the downstream app, returning the downstream app’s response if it is not a success.
- #head? ⇒ Boolean
-
#initialize(app, options = {}) ⇒ Thumb
constructor
A new instance of Thumb.
-
#parse_dimensions(meta) ⇒ Object
Parses the rendering options; returns false if rendering options are invalid.
-
#render_thumbnail(dim, grav) ⇒ Object
Renders a thumbnail from the source image.
-
#serve ⇒ Object
Serves the thumbnail.
- #to_path ⇒ Object
Constructor Details
#initialize(app, options = {}) ⇒ Thumb
Returns a new instance of Thumb.
68 69 70 71 72 73 |
# File 'lib/rack/thumb.rb', line 68 def initialize(app, ={}) @app = app @keylen = [:keylength] @secret = [:secret] @routes = generate_routes([:urls] || ["/"], [:prefix]) end |
Instance Method Details
#_call(env) ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/rack/thumb.rb', line 92 def _call(env) response = catch(:halt) do throw :halt unless %w{GET HEAD}.include? env["REQUEST_METHOD"] @env = env @path = env["PATH_INFO"] @routes.each do |regex| if match = @path.match(regex) @source, dim, grav = (match) @image = get_source_image @thumb = render_thumbnail(dim, grav) unless head? serve end end nil end response || @app.call(env) end |
#bad_request ⇒ Object
218 219 220 221 222 223 |
# File 'lib/rack/thumb.rb', line 218 def bad_request body = "Bad thumbnail parameters in #{@path}\n" [400, {"Content-Type" => "text/plain", "Content-Length" => body.size.to_s}, [body]] end |
#call(env) ⇒ Object
88 89 90 |
# File 'lib/rack/thumb.rb', line 88 def call(env) dup._call(env) end |
#create_tempfile ⇒ Object
Creates a new tempfile
214 215 216 |
# File 'lib/rack/thumb.rb', line 214 def create_tempfile Tempfile.new(::File.basename(@path)).tap { |f| f.close } end |
#each ⇒ Object
229 230 231 232 233 234 235 |
# File 'lib/rack/thumb.rb', line 229 def each ::File.open(@thumb.path, "rb") { |file| while part = file.read(8192) yield part end } end |
#extract_meta(match) ⇒ Object
Extracts filename and options from the path.
112 113 114 115 116 117 118 119 120 121 |
# File 'lib/rack/thumb.rb', line 112 def (match) result = if @keylen (match) else (match) end throw :halt unless result result end |
#extract_signed_meta(match) ⇒ Object
Extracts filename and options from a signed path.
124 125 126 127 128 129 |
# File 'lib/rack/thumb.rb', line 124 def (match) base, dim, grav, sig, ext = match.captures digest = Digest::SHA1.hexdigest("#{base}_#{dim}#{grav}#{ext}#{@secret}")[0..@keylen-1] throw(:halt, bad_request) unless sig && (sig == digest) [base + ext, dim, grav] end |
#extract_unsigned_meta(match) ⇒ Object
Extracts filename and options from an unsigned path.
132 133 134 135 |
# File 'lib/rack/thumb.rb', line 132 def (match) base, dim, grav, ext = match.captures [base + ext, dim, grav] end |
#generate_routes(urls, prefix = nil) ⇒ Object
Generates routes given a list of prefixes.
76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rack/thumb.rb', line 76 def generate_routes(urls, prefix = nil) urls.map do |url| prefix = prefix ? Regexp.escape(prefix) : '' url = url == "/" ? '' : Regexp.escape(url) if @keylen /^#{prefix}(#{url}\/.+)#{RE_TH_BASE}-([0-9a-f]{#{@keylen}})#{RE_TH_EXT}$/ else /^#{prefix}(#{url}\/.+)#{RE_TH_BASE}#{RE_TH_EXT}$/ end end end |
#get_source_image ⇒ Object
Fetch the source image from the downstream app, returning the downstream app’s response if it is not a success.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/rack/thumb.rb', line 139 def get_source_image status, headers, body = @app.call(@env.merge( "PATH_INFO" => @source )) unless (status >= 200 && status < 300) && (headers["Content-Type"].split("/").first == "image") throw :halt, [status, headers, body] end @source_headers = headers if !head? if body.respond_to?(:path) ::File.open(body.path, 'rb') elsif body.respond_to?(:each) data = '' body.each { |part| data << part.to_s } Tempfile.new(::File.basename(@path)).tap do |f| f.binmode f.write(data) f.close end end else nil end end |
#head? ⇒ Boolean
225 226 227 |
# File 'lib/rack/thumb.rb', line 225 def head? @env["REQUEST_METHOD"] == "HEAD" end |
#parse_dimensions(meta) ⇒ Object
Parses the rendering options; returns false if rendering options are invalid
200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/rack/thumb.rb', line 200 def parse_dimensions() dimensions = .split('x').map do |dim| if dim.empty? nil elsif dim[0].to_i == 0 throw :halt, bad_request else dim.to_i end end dimensions.any? ? dimensions : throw(:halt, bad_request) end |
#render_thumbnail(dim, grav) ⇒ Object
Renders a thumbnail from the source image. Returns a Tempfile.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/rack/thumb.rb', line 169 def render_thumbnail(dim, grav) gravity = grav ? TH_GRAV[grav] : :center width, height = parse_dimensions(dim) origin_width, origin_height = Mapel.info(@image.path)[:dimensions] width = [width, origin_width].min if width height = [height, origin_height].min if height output = create_tempfile cmd = Mapel(@image.path).gravity(gravity) if width && height cmd.resize!(width, height) else cmd.resize(width, height, 0, 0, '>') end cmd.to(output.path).run output end |
#serve ⇒ Object
Serves the thumbnail. If this is a HEAD request we strip the body as well as the content length because the render was never run.
188 189 190 191 192 193 194 195 196 197 |
# File 'lib/rack/thumb.rb', line 188 def serve response = if head? @source_headers.delete("Content-Length") [200, @source_headers, []] else [200, @source_headers.merge("Content-Length" => ::File.size(@thumb.path).to_s), self] end throw :halt, response end |
#to_path ⇒ Object
237 238 239 |
# File 'lib/rack/thumb.rb', line 237 def to_path @thumb.path end |