apache_image_resizer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/COPYING +663 -0
- data/ChangeLog +5 -0
- data/README +70 -0
- data/Rakefile +18 -0
- data/lib/apache/image_resizer.rb +90 -0
- data/lib/apache/image_resizer/util.rb +302 -0
- data/lib/apache/image_resizer/version.rb +31 -0
- data/lib/apache/mock_constants.rb +14 -0
- data/spec/apache/image_resizer/util_spec.rb +7 -0
- data/spec/apache/image_resizer_spec.rb +136 -0
- data/spec/spec_helper.rb +4 -0
- metadata +85 -0
data/ChangeLog
ADDED
data/README
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
= apache_image_resizer - Apache module providing image resizing functionality.
|
2
|
+
|
3
|
+
== VERSION
|
4
|
+
|
5
|
+
This documentation refers to apache_image_resizer version 0.0.1
|
6
|
+
|
7
|
+
|
8
|
+
== DESCRIPTION
|
9
|
+
|
10
|
+
Place the following snippet in your Apache config:
|
11
|
+
|
12
|
+
<IfModule mod_ruby.c>
|
13
|
+
RubyRequire apache/ruby-run
|
14
|
+
|
15
|
+
RubyRequire /path/to/apache_image_resizer
|
16
|
+
# or
|
17
|
+
#RubyRequire rubygems
|
18
|
+
#RubyRequire apache/image_resizer
|
19
|
+
|
20
|
+
<Location /image_resizer>
|
21
|
+
SetHandler ruby-object
|
22
|
+
RubyHandler "Apache::ImageResizer.new"
|
23
|
+
</Location>
|
24
|
+
|
25
|
+
<Directory /path/to/images>
|
26
|
+
ErrorDocument 404 /image_resizer
|
27
|
+
</Directory>
|
28
|
+
</IfModule>
|
29
|
+
|
30
|
+
Expected filesystem layout and URLs:
|
31
|
+
|
32
|
+
/images/bla/original/blob/blub_42-23.jpg
|
33
|
+
`-- base
|
34
|
+
`-- prefix
|
35
|
+
`-- source directory
|
36
|
+
`-- path --------->
|
37
|
+
|
38
|
+
/$BASE/$PREFIX/r42x23/$PATH
|
39
|
+
`-- directives
|
40
|
+
|
41
|
+
|
42
|
+
== LINKS
|
43
|
+
|
44
|
+
<b></b>
|
45
|
+
Documentation:: http://blackwinter.github.com/apache_image_resizer
|
46
|
+
Source code:: http://github.com/blackwinter/apache_image_resizer
|
47
|
+
|
48
|
+
|
49
|
+
== AUTHORS
|
50
|
+
|
51
|
+
* Jens Wille <mailto:jens.wille@uni-koeln.de>
|
52
|
+
|
53
|
+
|
54
|
+
== LICENSE AND COPYRIGHT
|
55
|
+
|
56
|
+
Copyright (C) 2011 University of Cologne,
|
57
|
+
Albertus-Magnus-Platz, 50923 Cologne, Germany
|
58
|
+
|
59
|
+
apache_image_resizer is free software: you can redistribute it and/or modify
|
60
|
+
it under the terms of the GNU Affero General Public License as published by
|
61
|
+
the Free Software Foundation, either version 3 of the License, or (at your
|
62
|
+
option) any later version.
|
63
|
+
|
64
|
+
apache_image_resizer is distributed in the hope that it will be useful, but
|
65
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
66
|
+
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
|
67
|
+
for more details.
|
68
|
+
|
69
|
+
You should have received a copy of the GNU Affero General Public License along
|
70
|
+
with apache_image_resizer. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path(%q{../lib/apache/image_resizer/version}, __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'hen'
|
5
|
+
|
6
|
+
Hen.lay! {{
|
7
|
+
:gem => {
|
8
|
+
:name => %q{apache_image_resizer},
|
9
|
+
:version => Apache::ImageResizer::VERSION,
|
10
|
+
:summary => %q{Apache module providing image resizing functionality.},
|
11
|
+
:author => %q{Jens Wille},
|
12
|
+
:email => %q{jens.wille@uni-koeln.de},
|
13
|
+
:homepage => :blackwinter
|
14
|
+
}
|
15
|
+
}}
|
16
|
+
rescue LoadError => err
|
17
|
+
warn "Please install the `hen' gem. (#{err})"
|
18
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
#--
|
2
|
+
###############################################################################
|
3
|
+
# #
|
4
|
+
# apache_image_resizer -- Apache module providing upload merging #
|
5
|
+
# functionality #
|
6
|
+
# #
|
7
|
+
# Copyright (C) 2011 University of Cologne, #
|
8
|
+
# Albertus-Magnus-Platz, #
|
9
|
+
# 50923 Cologne, Germany #
|
10
|
+
# #
|
11
|
+
# Authors: #
|
12
|
+
# Jens Wille <jens.wille@uni-koeln.de> #
|
13
|
+
# #
|
14
|
+
# apache_image_resizer is free software: you can redistribute it and/or #
|
15
|
+
# modify it under the terms of the GNU Affero General Public License as #
|
16
|
+
# published by the Free Software Foundation, either version 3 of the #
|
17
|
+
# License, or (at your option) any later version. #
|
18
|
+
# #
|
19
|
+
# apache_image_resizer is distributed in the hope that it will be #
|
20
|
+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty #
|
21
|
+
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
|
22
|
+
# Affero General Public License for more details. #
|
23
|
+
# #
|
24
|
+
# You should have received a copy of the GNU Affero General Public License #
|
25
|
+
# along with apache_image_resizer. If not, see http://www.gnu.org/licenses/. #
|
26
|
+
# #
|
27
|
+
###############################################################################
|
28
|
+
#++
|
29
|
+
|
30
|
+
require 'apache/image_resizer/util'
|
31
|
+
|
32
|
+
module Apache
|
33
|
+
|
34
|
+
class ImageResizer
|
35
|
+
|
36
|
+
include Util
|
37
|
+
|
38
|
+
# Creates a new RubyHandler instance for the Apache web server. It
|
39
|
+
# is to be installed as a custom 404 ErrorDocument handler.
|
40
|
+
def initialize(options = {})
|
41
|
+
@verbosity = options[:verbosity] || DEFAULT_VERBOSITY
|
42
|
+
@max_dim = options[:max_dim] || DEFAULT_MAX_DIM
|
43
|
+
@prefix_re = options[:prefix_re] || DEFAULT_PREFIX_RE
|
44
|
+
@source_dir = options[:source_dir] || DEFAULT_SOURCE_DIR
|
45
|
+
@proxy_cache = options[:proxy_cache] || DEFAULT_PROXY_CACHE
|
46
|
+
@enlarge = options[:enlarge]
|
47
|
+
@secret = options[:secret]
|
48
|
+
end
|
49
|
+
|
50
|
+
# If the current +request+ asked for a resource that's not there,
|
51
|
+
# it will be checked for resize directives and treated accordingly.
|
52
|
+
# If no matching resource could be found, the original error will be
|
53
|
+
# thrown.
|
54
|
+
def handler(request, &block)
|
55
|
+
request.add_common_vars # REDIRECT_URL, REDIRECT_QUERY_STRING
|
56
|
+
env = request.subprocess_env
|
57
|
+
|
58
|
+
url = env['REDIRECT_URL']
|
59
|
+
query = env['REDIRECT_QUERY_STRING']
|
60
|
+
url += "?#{query}" unless query.nil? || query.empty?
|
61
|
+
|
62
|
+
block ||= lambda { |msg|
|
63
|
+
log(2) { "#{url} - Elapsed #{Time.now - request.request_time} [#{msg}]" }
|
64
|
+
}
|
65
|
+
|
66
|
+
path, prefix, directives, dir = parse_url(url,
|
67
|
+
base = request.path_info, @prefix_re, &block)
|
68
|
+
|
69
|
+
return DECLINED unless path
|
70
|
+
|
71
|
+
log(2) {{ :Base => base, :Prefix => prefix, :Dir => dir, :Path => path }}
|
72
|
+
|
73
|
+
source, target = get_paths(request, path, base,
|
74
|
+
prefix, @source_dir, dir, @proxy_cache, @secret)
|
75
|
+
|
76
|
+
if source
|
77
|
+
log(2) {{ :Source => source, :Target => target, :Directives => directives }}
|
78
|
+
return DECLINED unless img = resize(source, target, directives, @enlarge, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
return DECLINED unless send_image(request, target, img, &block)
|
82
|
+
|
83
|
+
log { "#{url} - Elapsed #{Time.now - request.request_time}" }
|
84
|
+
|
85
|
+
OK
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
#--
|
2
|
+
###############################################################################
|
3
|
+
# #
|
4
|
+
# A component of apache_image_resizer. #
|
5
|
+
# #
|
6
|
+
# Copyright (C) 2011 University of Cologne, #
|
7
|
+
# Albertus-Magnus-Platz, #
|
8
|
+
# 50923 Cologne, Germany #
|
9
|
+
# #
|
10
|
+
# Authors: #
|
11
|
+
# Jens Wille <jens.wille@uni-koeln.de> #
|
12
|
+
# #
|
13
|
+
# apache_image_resizer is free software: you can redistribute it and/or #
|
14
|
+
# modify it under the terms of the GNU Affero General Public License as #
|
15
|
+
# published by the Free Software Foundation, either version 3 of the #
|
16
|
+
# License, or (at your option) any later version. #
|
17
|
+
# #
|
18
|
+
# apache_image_resizer is distributed in the hope that it will be #
|
19
|
+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty #
|
20
|
+
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
|
21
|
+
# Affero General Public License for more details. #
|
22
|
+
# #
|
23
|
+
# You should have received a copy of the GNU Affero General Public License #
|
24
|
+
# along with apache_image_resizer. If not, see http://www.gnu.org/licenses/. #
|
25
|
+
# #
|
26
|
+
###############################################################################
|
27
|
+
#++
|
28
|
+
|
29
|
+
require 'RMagick'
|
30
|
+
require 'fileutils'
|
31
|
+
require 'filemagic/ext'
|
32
|
+
require 'nuggets/uri/redirect'
|
33
|
+
require 'apache/secure_download/util'
|
34
|
+
|
35
|
+
unless RUBY_VERSION > '1.8.5'
|
36
|
+
|
37
|
+
class String # :nodoc:
|
38
|
+
def start_with?(prefix)
|
39
|
+
index(prefix) == 0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Net # :nodoc:
|
44
|
+
class BufferedIO # :nodoc:
|
45
|
+
private
|
46
|
+
|
47
|
+
def rbuf_fill
|
48
|
+
timeout(@read_timeout) {
|
49
|
+
@rbuf << @io.readpartial(1024)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
module Apache
|
58
|
+
|
59
|
+
class ImageResizer
|
60
|
+
|
61
|
+
module Util
|
62
|
+
|
63
|
+
extend self
|
64
|
+
|
65
|
+
DEFAULT_VERBOSITY = 0
|
66
|
+
DEFAULT_MAX_DIM = 8192
|
67
|
+
DEFAULT_HEIGHT = 999999
|
68
|
+
DEFAULT_PREFIX_RE = %r{[^/]+/}
|
69
|
+
DEFAULT_SOURCE_DIR = 'original'
|
70
|
+
DEFAULT_PROXY_CACHE = 'proxy_cache'
|
71
|
+
DEFAULT_FORMAT = 'jpeg:'
|
72
|
+
|
73
|
+
dimension_pattern = %q{\d+(?:\.\d+)?}
|
74
|
+
|
75
|
+
DIRECTIVES_RE = %r{
|
76
|
+
\A
|
77
|
+
(?:
|
78
|
+
(?:
|
79
|
+
r # resize
|
80
|
+
( #{dimension_pattern} ) # width
|
81
|
+
(?:
|
82
|
+
x
|
83
|
+
( #{dimension_pattern} ) # height (optional)
|
84
|
+
)?
|
85
|
+
)
|
86
|
+
|
|
87
|
+
(?:
|
88
|
+
o # offset
|
89
|
+
( #{dimension_pattern} ) # width
|
90
|
+
x
|
91
|
+
( #{dimension_pattern} ) # height
|
92
|
+
( [pm] #{dimension_pattern} ) # x
|
93
|
+
( [pm] #{dimension_pattern} ) # y
|
94
|
+
)
|
95
|
+
){1,2}
|
96
|
+
/
|
97
|
+
}x
|
98
|
+
|
99
|
+
PROXY_RE = %r{\Aproxy:}
|
100
|
+
|
101
|
+
REPLACE = %w[? $@$]
|
102
|
+
|
103
|
+
def secure_resize_url(secret, args = [], options = {})
|
104
|
+
url_for(resize_url(*args), secret, options)
|
105
|
+
end
|
106
|
+
|
107
|
+
def resize_url(size = DEFAULT_SOURCE_DIR, *path)
|
108
|
+
case size
|
109
|
+
when String then nil
|
110
|
+
when Numeric then size = "r#{size}"
|
111
|
+
when Array then size = "r#{size.join('x')}"
|
112
|
+
when Hash then size = size.map { |k, v| case k.to_s
|
113
|
+
when /\A[rs]/
|
114
|
+
"r#{Array(v).join('x')}" if v
|
115
|
+
when /\Ao/
|
116
|
+
x, y, w, h = v
|
117
|
+
"o#{w}x#{h}#{x < 0 ? 'm' : 'p'}#{x.abs}#{y < 0 ? 'm' : 'p'}#{y.abs}"
|
118
|
+
end }.compact.join
|
119
|
+
end
|
120
|
+
|
121
|
+
if path.empty?
|
122
|
+
size
|
123
|
+
else
|
124
|
+
file = path.pop.sub(*REPLACE)
|
125
|
+
File.join(path << size << file)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_url(url, base, prefix_re, &block)
|
130
|
+
block ||= lambda { |*| }
|
131
|
+
|
132
|
+
return block['No URL'] unless url
|
133
|
+
|
134
|
+
path = Apache::SecureDownload::Util.real_path(url)
|
135
|
+
return block['Invalid Format'] unless path.sub!(
|
136
|
+
%r{\A#{Regexp.escape(base)}/(#{prefix_re})}, ''
|
137
|
+
)
|
138
|
+
|
139
|
+
prefix, directives, dir = $1 || '', *extract_directives(path)
|
140
|
+
return block['Invalid Directives'] unless directives
|
141
|
+
return block['No Path'] if path.empty?
|
142
|
+
return block['No File'] if path =~ %r{/\z}
|
143
|
+
|
144
|
+
[path, prefix, directives, dir]
|
145
|
+
end
|
146
|
+
|
147
|
+
def extract_directives(path)
|
148
|
+
return unless path.sub!(DIRECTIVES_RE, '')
|
149
|
+
match, directives, max = Regexp.last_match, {}, @max_dim
|
150
|
+
|
151
|
+
if o = match[3]
|
152
|
+
h, *xy = match.values_at(4, 5, 6)
|
153
|
+
|
154
|
+
if (args = [o.to_f, h.to_f]).all? { |i| i <= max }
|
155
|
+
directives[:offset] = xy.map! { |i|
|
156
|
+
i.tr!('pm', '+-').to_f
|
157
|
+
}.concat(args)
|
158
|
+
else
|
159
|
+
return
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
if w = match[1]
|
164
|
+
h = match[2]
|
165
|
+
|
166
|
+
args = [w.to_f]
|
167
|
+
args << h.to_f if h
|
168
|
+
|
169
|
+
max *= 2 if o
|
170
|
+
|
171
|
+
if args.all? { |i| i <= max }
|
172
|
+
directives[:resize] = args
|
173
|
+
else
|
174
|
+
return
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
[directives, match[0]]
|
179
|
+
end
|
180
|
+
|
181
|
+
def get_paths(request, path, base, prefix, source_dir, target_dir, proxy_cache, secret)
|
182
|
+
real_base = request.lookup_uri(base).filename.untaint
|
183
|
+
target = File.join(real_base, prefix, target_dir, path).untaint
|
184
|
+
return [nil, target] if File.exist?(target)
|
185
|
+
|
186
|
+
source = request.lookup_uri(url_for(File.join(base, prefix,
|
187
|
+
source_dir, path.sub(*REPLACE.reverse)), secret)).filename.untaint
|
188
|
+
return [source, target] unless source.sub!(PROXY_RE, '') && proxy_cache
|
189
|
+
|
190
|
+
cache = File.join(real_base, prefix, proxy_cache, path).untaint
|
191
|
+
return [cache, target] if File.exist?(cache)
|
192
|
+
|
193
|
+
begin
|
194
|
+
URI.get_redirect(source) { |res|
|
195
|
+
if res.is_a?(Net::HTTPSuccess)
|
196
|
+
content = res.body.untaint
|
197
|
+
|
198
|
+
mkdir_for(cache)
|
199
|
+
File.open(cache, 'w') { |f| f.write(content) }
|
200
|
+
|
201
|
+
return [cache, target]
|
202
|
+
end
|
203
|
+
}
|
204
|
+
rescue => err
|
205
|
+
log_err(err, "#{source} => #{cache}")
|
206
|
+
end
|
207
|
+
|
208
|
+
[source, target]
|
209
|
+
end
|
210
|
+
|
211
|
+
def resize(source, target, directives, enlarge, &block)
|
212
|
+
img = do_magick('Read', source, block) { |value|
|
213
|
+
Magick::Image.read(value).first
|
214
|
+
} or return
|
215
|
+
|
216
|
+
resize, offset = directives.values_at(:resize, :offset)
|
217
|
+
|
218
|
+
if resize
|
219
|
+
w, h = resize
|
220
|
+
h ||= DEFAULT_HEIGHT
|
221
|
+
|
222
|
+
unless enlarge || offset
|
223
|
+
w = [w, img.columns].min
|
224
|
+
h = [h, img.rows ].min
|
225
|
+
end
|
226
|
+
|
227
|
+
img.resize_to_fit!(w, h)
|
228
|
+
end
|
229
|
+
|
230
|
+
if offset
|
231
|
+
img.crop!(*offset)
|
232
|
+
end
|
233
|
+
|
234
|
+
mkdir_for(target)
|
235
|
+
|
236
|
+
do_magick('Write', target, block) { |value|
|
237
|
+
img.write(value) or raise Magick::ImageMagickError
|
238
|
+
}
|
239
|
+
rescue => err
|
240
|
+
log_err(err)
|
241
|
+
block['Resize Error: %s' % err] if block
|
242
|
+
end
|
243
|
+
|
244
|
+
def send_image(request, target, img = nil)
|
245
|
+
File.open(target) { |f|
|
246
|
+
request.content_type = (t = img && img.format) ?
|
247
|
+
"image/#{t.downcase}" : f.content_type
|
248
|
+
|
249
|
+
request.status = HTTP_OK
|
250
|
+
request.send_fd(f)
|
251
|
+
}
|
252
|
+
rescue IOError => err
|
253
|
+
request.log_reason(err.message, target)
|
254
|
+
yield('Send Error') if block_given?
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def do_magick(what, value, block)
|
260
|
+
yield value
|
261
|
+
rescue Magick::ImageMagickError => err
|
262
|
+
unless value.start_with?(DEFAULT_FORMAT)
|
263
|
+
value = "#{DEFAULT_FORMAT}#{value}"
|
264
|
+
retry
|
265
|
+
else
|
266
|
+
block['%s Error: %s' % [what, err]] if block
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def mkdir_for(path)
|
271
|
+
dir = File.dirname(path).untaint
|
272
|
+
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
273
|
+
end
|
274
|
+
|
275
|
+
def url_for(url, sec = nil, opt = {})
|
276
|
+
sec ? Apache::SecureDownload::Util.secure_url(sec, url, opt) : url
|
277
|
+
end
|
278
|
+
|
279
|
+
def log_err(err, msg = nil)
|
280
|
+
log(0, 1, err.backtrace) {
|
281
|
+
"[ERROR] #{"#{msg}: " if msg}#{err} (#{err.class})"
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
def log(level = 1, loc = 0, trace = nil)
|
286
|
+
return if @verbosity < level
|
287
|
+
|
288
|
+
case msg = yield
|
289
|
+
when Array then msg = msg.shift % msg
|
290
|
+
when Hash then msg = msg.map { |k, v|
|
291
|
+
"#{k} = #{v.inspect}"
|
292
|
+
}.join(', ')
|
293
|
+
end
|
294
|
+
|
295
|
+
warn "#{caller[loc]}: #{msg}#{" [#{trace.join(', ')}]" if trace}"
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|