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/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