rack_grid_thumb 0.0.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/README.txt +49 -0
- data/lib/rack_grid_thumb.rb +189 -0
- data/test/test_rack_grid_thumb.rb +0 -0
- metadata +59 -0
data/README.txt
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
Rack::GridThumb
|
2
|
+
|
3
|
+
Rack::GridThumb is used to dynamically create thumbnails when in front of rack_grid.
|
4
|
+
You should run Rack::GridThumb behind a cache such as Varnish or Rack::Cache
|
5
|
+
|
6
|
+
Installation
|
7
|
+
|
8
|
+
# gem install rack_grid_thumb
|
9
|
+
|
10
|
+
Usage Example with Sinatra.
|
11
|
+
|
12
|
+
# app.rb
|
13
|
+
require 'rack_grid'
|
14
|
+
require 'rack_grid_thumb'
|
15
|
+
|
16
|
+
configure do
|
17
|
+
use Rack::GridThumb, :prefix => 'grid'
|
18
|
+
use Rack::Grid, :prefix => 'grid'
|
19
|
+
end
|
20
|
+
|
21
|
+
# view.erb
|
22
|
+
<img src="/grid/4ba69fde8c8f369a6e000003_50x50.jpg" alt="My Image" />
|
23
|
+
|
24
|
+
Usage
|
25
|
+
|
26
|
+
/#{prefix}/#{uid}_50x50.jpg # => Crop and resize to 50x50
|
27
|
+
/#{prefix}/#{uid}_50x50-nw.jpg # => Crop and resize with northwest gravity
|
28
|
+
/#{prefix}/#{uid}_50x.jpg # => Resize to a width of 50, preserving AR
|
29
|
+
/#{prefix}/#{uid}_x50.jpg # => Resize to a height of 50, preserving AR
|
30
|
+
|
31
|
+
|
32
|
+
To prevent pesky end-users and bots from flooding your application with
|
33
|
+
render requests you can set up Rack::Thumb to check for an SHA-1 signature
|
34
|
+
that is unique to every url. Using this option, only thumbnails requested
|
35
|
+
by your templates will be valid. Example:
|
36
|
+
|
37
|
+
use Rack::Thumb, {
|
38
|
+
:secret => "My secret",
|
39
|
+
:keylength => "16" # => Only use 16 digits of the SHA-1 key
|
40
|
+
}
|
41
|
+
|
42
|
+
You can then use your +secret+ to generate secure links in your templates:
|
43
|
+
|
44
|
+
/#{prefix}/#{uid}_50x100-sw-a267c193a7eff046.jpg # => Successful
|
45
|
+
/#{prefix}/#{uid}_120x250-a267c193a7eff046.jpg # => Returns a bad request error
|
46
|
+
|
47
|
+
|
48
|
+
Inspired by:
|
49
|
+
https://github.com/akdubya/rack-thumb
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'mapel'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
|
8
|
+
class GridThumb
|
9
|
+
RE_TH_BASE = /_([0-9]+x|x[0-9]+|[0-9]+x[0-9]+)(-(?:nw|n|ne|w|c|e|sw|s|se))?/
|
10
|
+
RE_TH_EXT = /(\.(?:jpg|jpeg|png|gif))/i
|
11
|
+
TH_GRAV = {
|
12
|
+
'-nw' => :northwest,
|
13
|
+
'-n' => :north,
|
14
|
+
'-ne' => :northeast,
|
15
|
+
'-w' => :west,
|
16
|
+
'-c' => :center,
|
17
|
+
'-e' => :east,
|
18
|
+
'-sw' => :southwest,
|
19
|
+
'-s' => :south,
|
20
|
+
'-se' => :southeast
|
21
|
+
}
|
22
|
+
|
23
|
+
def initialize(app, options={})
|
24
|
+
@app = app
|
25
|
+
@keylen = options[:keylength]
|
26
|
+
@secret = options[:secret]
|
27
|
+
@route = generate_route(options[:prefix])
|
28
|
+
end
|
29
|
+
|
30
|
+
# Generates route given a prefixes.
|
31
|
+
def generate_route(prefix = nil)
|
32
|
+
if @keylen
|
33
|
+
/^(\/#{prefix}\/\w+).*#{RE_TH_BASE}-([0-9a-f]{#{@keylen}})#{RE_TH_EXT}$/
|
34
|
+
else
|
35
|
+
/^(\/#{prefix}\/\w+).*#{RE_TH_BASE}#{RE_TH_EXT}$/
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(env)
|
40
|
+
dup._call(env)
|
41
|
+
end
|
42
|
+
|
43
|
+
def _call(env)
|
44
|
+
response = catch(:halt) do
|
45
|
+
throw :halt unless %w{GET HEAD}.include? env["REQUEST_METHOD"]
|
46
|
+
@env = env
|
47
|
+
@path = env["PATH_INFO"]
|
48
|
+
if match = @path.match(@route)
|
49
|
+
@source, dim, grav = extract_meta(match)
|
50
|
+
@image = get_source_image
|
51
|
+
@thumb = render_thumbnail(dim, grav) unless head?
|
52
|
+
serve
|
53
|
+
end
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
response || @app.call(env)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extracts filename and options from the path.
|
61
|
+
def extract_meta(match)
|
62
|
+
result = if @keylen
|
63
|
+
extract_signed_meta(match)
|
64
|
+
else
|
65
|
+
extract_unsigned_meta(match)
|
66
|
+
end
|
67
|
+
|
68
|
+
throw :halt unless result
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
# Extracts filename and options from a signed path.
|
73
|
+
def extract_signed_meta(match)
|
74
|
+
base, dim, grav, sig, ext = match.captures
|
75
|
+
digest = Digest::SHA1.hexdigest("#{base}_#{dim}#{grav}#{ext}#{@secret}")[0..@keylen-1]
|
76
|
+
throw(:halt, bad_request) unless sig && (sig == digest)
|
77
|
+
[base + ext, dim, grav]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Extracts filename and options from an unsigned path.
|
81
|
+
def extract_unsigned_meta(match)
|
82
|
+
base, dim, grav, ext = match.captures
|
83
|
+
[base + ext, dim, grav]
|
84
|
+
end
|
85
|
+
|
86
|
+
# Fetch the source image from the downstream app, returning the downstream
|
87
|
+
# app's response if it is not a success.
|
88
|
+
def get_source_image
|
89
|
+
status, headers, body = @app.call(@env.merge(
|
90
|
+
"PATH_INFO" => @source
|
91
|
+
))
|
92
|
+
unless (status >= 200 && status < 300) &&
|
93
|
+
(headers["Content-Type"].split("/").first == "image")
|
94
|
+
throw :halt, [status, headers, body]
|
95
|
+
end
|
96
|
+
|
97
|
+
@source_headers = headers
|
98
|
+
|
99
|
+
if !head?
|
100
|
+
if body.respond_to?(:path)
|
101
|
+
::File.open(body.path, 'rb')
|
102
|
+
elsif body.respond_to?(:each)
|
103
|
+
data = ''
|
104
|
+
body.each { |part| data << part.to_s }
|
105
|
+
Tempfile.new(::File.basename(@path)).tap do |f|
|
106
|
+
f.binmode
|
107
|
+
f.write(data)
|
108
|
+
f.close
|
109
|
+
end
|
110
|
+
end
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Renders a thumbnail from the source image. Returns a Tempfile.
|
117
|
+
def render_thumbnail(dim, grav)
|
118
|
+
gravity = grav ? TH_GRAV[grav] : :center
|
119
|
+
width, height = parse_dimensions(dim)
|
120
|
+
origin_width, origin_height = Mapel.info(@image.path)[:dimensions]
|
121
|
+
width = [width, origin_width].min if width
|
122
|
+
height = [height, origin_height].min if height
|
123
|
+
output = create_tempfile
|
124
|
+
cmd = Mapel(@image.path).gravity(gravity)
|
125
|
+
if width && height
|
126
|
+
cmd.resize!(width, height)
|
127
|
+
else
|
128
|
+
cmd.resize(width, height, 0, 0, '>')
|
129
|
+
end
|
130
|
+
cmd.to(output.path).run
|
131
|
+
output
|
132
|
+
end
|
133
|
+
|
134
|
+
# Serves the thumbnail. If this is a HEAD request we strip the body as well
|
135
|
+
# as the content length because the render was never run.
|
136
|
+
def serve
|
137
|
+
response = if head?
|
138
|
+
@source_headers.delete("Content-Length")
|
139
|
+
[200, @source_headers, []]
|
140
|
+
else
|
141
|
+
[200, @source_headers.merge("Content-Length" => ::File.size(@thumb.path).to_s), self]
|
142
|
+
end
|
143
|
+
|
144
|
+
throw :halt, response
|
145
|
+
end
|
146
|
+
|
147
|
+
# Parses the rendering options; returns false if rendering options are invalid
|
148
|
+
def parse_dimensions(meta)
|
149
|
+
dimensions = meta.split('x').map do |dim|
|
150
|
+
if dim.empty?
|
151
|
+
nil
|
152
|
+
elsif dim[0].to_i == 0
|
153
|
+
throw :halt, bad_request
|
154
|
+
else
|
155
|
+
dim.to_i
|
156
|
+
end
|
157
|
+
end
|
158
|
+
dimensions.any? ? dimensions : throw(:halt, bad_request)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Creates a new tempfile
|
162
|
+
def create_tempfile
|
163
|
+
Tempfile.new(::File.basename(@path)).tap { |f| f.close }
|
164
|
+
end
|
165
|
+
|
166
|
+
def bad_request
|
167
|
+
body = "Bad thumbnail parameters in #{@path}\n"
|
168
|
+
[400, {"Content-Type" => "text/plain",
|
169
|
+
"Content-Length" => body.size.to_s},
|
170
|
+
[body]]
|
171
|
+
end
|
172
|
+
|
173
|
+
def head?
|
174
|
+
@env["REQUEST_METHOD"] == "HEAD"
|
175
|
+
end
|
176
|
+
|
177
|
+
def each
|
178
|
+
::File.open(@thumb.path, "rb") { |file|
|
179
|
+
while part = file.read(8192)
|
180
|
+
yield part
|
181
|
+
end
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def to_path
|
186
|
+
@thumb.path
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack_grid_thumb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dusty Doris
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-07-08 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mapel
|
16
|
+
requirement: &2157443320 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2157443320
|
25
|
+
description: Auto-create thumbnails when used with rack_grid
|
26
|
+
email: github@dusty.name
|
27
|
+
executables: []
|
28
|
+
extensions: []
|
29
|
+
extra_rdoc_files:
|
30
|
+
- README.txt
|
31
|
+
files:
|
32
|
+
- README.txt
|
33
|
+
- lib/rack_grid_thumb.rb
|
34
|
+
- test/test_rack_grid_thumb.rb
|
35
|
+
homepage: http://github.com/dusty/rack_grid_thumb
|
36
|
+
licenses: []
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project: none
|
55
|
+
rubygems_version: 1.8.5
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Auto-create thumbnails when used with rack_grid
|
59
|
+
test_files: []
|