apache_image_resizer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog ADDED
@@ -0,0 +1,5 @@
1
+ = Revision history for apache_image_resizer
2
+
3
+ == 0.0.1 [2011-10-11]
4
+
5
+ * Birthday :-)
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