apache_image_resizer 0.0.1
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/.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
|