File Coverage

blib/lib/Apache2/Imager/Resize.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Apache2::Imager::Resize;
2 1     1   1018 use strict;
  1         3  
  1         43  
3              
4 1     1   6 use File::Basename ();
  1         2  
  1         17  
5 1     1   7 use File::Path ();
  1         6  
  1         24  
6 1     1   2033 use Apache2::Const qw(:common);
  0            
  0            
7             use Apache2::RequestUtil;
8             use Apache2::Request;
9             use APR::Finfo;
10             use APR::Const qw(:finfo);
11             use Imager;
12             use Data::Dumper;
13              
14             use vars qw($VERSION);
15             $VERSION = '0.16';
16              
17             =head1 NAME
18              
19             Apache2::Imager::Resize - Fixup handler that resizes and crops images on the fly, caching the results, and doesn't require ImageMagick.
20              
21             =head1 SYNOPSIS
22              
23            
24             PerlFixupHandler Apache2::Imager::Resize
25             PerlSetVar ImgResizeCacheDir '/var/cache/AIRCache/'
26            
27              
28             # or
29              
30            
31             PerlHandler Apache2::Imager::Resize
32             PerlSetVar ImgResizeNoCache on
33             PerlSetVar ImgResizeWidthParam 'w'
34             PerlSetVar ImgResizeHeightParam 'h'
35            
36              
37             # and on a web page somewhere:
38              
39            
40              
41             =head1 INTRODUCTION
42              
43             This is a simple fixup class that only does one job: it resizes images before
44             they're delivered. All you have to do is append either a width and/or a height
45             parameter to any image file address, and AIR will make sure that an
46             appropriately shrunken image file is returned. It caches the results of each
47             operation, so the first request might take a little while but subsequent
48             similar requests should be very quick.
49              
50             This module is based on the code from L, which does the
51             same job for Apache 1.x. Some new parameters have been adden, but preexisting
52             parameters are backwards-compatible.
53              
54             =head1 PARAMETERS
55              
56             Apache2::Imager::Resize understands four query string parameters:
57              
58             =head2 w
59              
60             width in pixels. You can specify another name with an ImgResizeWidthParam directive.
61              
62             =head2 h
63              
64             height in pixels. You can specify another name with an ImgResizeHeightParam directive.
65              
66             =head2 reshape
67              
68             If this is 'crop', we will crop without resizing. The default behaviour is
69             to scale first and then crop to fit the other dimension (see below). If only
70             one dimension is specified, this parameter has no effect. There will be more
71             options here in later versions.
72              
73             =head2 cropto
74              
75             This can be left, right, top or bottom, and it dictates the part of the picture
76             that is kept when we crop the image. If only one dimension is specified, this
77             parameter has no effect. Future versions will allow combinations of these values.
78              
79             =head2 quality
80              
81             This should be an integer between 0 and 100. It only affects jpeg images. The default is 60.
82              
83             =head2 enlarge
84              
85             By default images won't images get scaled up. If you wan't to do this, set enlarge to 1.
86              
87             =head2 cropAR
88              
89             Overrides the default behaviour of configuration parameter "ImgResizeCropToAspectRatio".
90              
91             =head2 scaletype
92              
93             Scale type 'min', 'max', 'nonprop'. See L.
94              
95             =head2 qtype
96              
97             Quality of scaling 'normal', 'preview', 'mixing'. See L.
98              
99             =head1 CONFIGURATION
100              
101             In many cases, this will suffice:
102              
103            
104             PerlFixupHandler Apache::Imager::Resize
105            
106              
107             But you can also include one or more of these directives to modify the behaviour of the handler:
108              
109             =head2 ImgResizeCacheDir
110              
111             Sets the path to a directory that will be used to hold the resized versions of image
112             files. If you don't include this directive, resized images will be stored next to their
113             originals. The supplied value should be relative to your document root, eg:
114              
115            
116             PerlFixupHandler Apache::Imager::Resize
117             PerlSetVar ImgResizeCacheDir '/var/cache/AIRCache/'
118            
119              
120             You can put the cache inside a directory that is handled by AIR without ill effects,
121             though of course it will get a bit odd if you start serving images directly from the cache.
122              
123             =head2 ImgResizeNoCache
124              
125             If true, this will mean that images are resized for each request and no attempt
126             is made to keep a copy for future use.
127              
128             =head2 ImgResizeWidthParam
129              
130             Sets the name of the parameter that will be used to specify the width (in pixels)
131             of the returned image. Default is 'w'.
132              
133             =head2 ImgResizeHeightParam
134              
135             Sets the name of the parameter that will be used to specify the height (in pixels)
136             of the returned image. Default is 'h'.
137              
138             =head2 ImgResizeCropToAspectRatio
139              
140             If true, the image will be cropped if the specified width and height would lead to
141             a new aspect ratio. Default is '1'. The parameter 'cropAR' can be used to override
142             this behaviour.
143              
144             =head2 ImgResizeQtype
145              
146             Sets the default value for L.
147              
148             =head1 IMAGE FORMATS
149              
150             We can work with any image format that L can read, which includes all the
151             usual web files and most other bitmaps.
152              
153             =head1 SHRINKING RULES
154              
155             If only one dimension is specified, we will scale the image to that size,keeping
156             the aspect ratio.
157              
158             If both dimensions are specified and the combination preserves the aspect ratio
159             of the image, we scale the image to that size.
160              
161             If there is no 'reshape' parameter, the specified dimensions result in a change
162             of shape and the parameter "proportional" is set to 0, the aspect ratio of the
163             image will be changed.
164              
165             If there is no 'reshape' parameter, and the specified dimensions result in a
166             change of shape, we will first scale the image to the correct size in the dimension
167             that is changing less, then crop in the other dimension to achieve the right shape
168             and size without distorting the image. You can supply a 'cropto' parameter to specify
169             which part of the image is kept in the cropping step. You can set "ImgResizeCropToAspectRatio"
170             to 0 or the parameter "cropAR" to avoid the cropping of the image.
171              
172             If the reshape parameter is 'crop', we will crop in both dimensions without scaling
173             the image at all. You can supply a 'cropto' parameter to specify which part of the
174             image is kept. This is likely to yield better quality than scaling, when the original
175             size is close to the target size, but will have less useful results where they're
176             very different.
177              
178             =head1 CACHING AND EFFICIENCY
179              
180             Unless you've switched the cache off, the handler keeps a copy of every resized
181             file. When a request comes in, we look first for a cached file, and check that it's
182             no older than the original image file.
183              
184             By default we keep the cache files next to the originals, which can get messy. You
185             can also specify a cache directory, in which the directory structure of your site
186             will be partly recreated as resized images are stored in subdirectories corresponding
187             to the position of their originals in the main filing system. This makes it much
188             easier to prune or discard the cache.
189              
190             Note that at the moment it is assumed that your image cache will be within your
191             document root. There's no reason why it should have to be, so at some point soon
192             it will be possible to specify a whole page.
193              
194             Either way, this request:
195              
196            
197              
198             will produce (or use) a cache file named:
199              
200             [cachedir]/images/morecambe_120_150_left.jpg
201              
202             If either dimension is not specified, as is common, the filename will have an x
203             in that position. The cropto parameter is also usually omitted, so this:
204              
205            
206              
207             corresponds to this:
208              
209             [cachedir]/images/morecambe_120_x.jpg
210              
211             If neither width nor height is specified we bail out immediately, so the original
212             image will be returned.
213              
214             There is currently no mechanism for cache cleanup, but we do touch the access
215             date of each file each time it's used (leaving the modification date alone so that
216             it can be to compare with the original file). You could fairly easily set up a cron
217             job to go through your cache directory deleting all the image files that have not
218             been touched for a week or so.
219              
220             =cut
221              
222             sub handler {
223             my $r = Apache2::Request->new(shift);
224              
225             my $filename = $r->filename;
226              
227             my $nocache = $r->dir_config('ImgResizeNoCache');
228             my $cachedir = $r->dir_config('ImgResizeCacheDir');
229             $cachedir .= '/' if ( $cachedir !~ /\/$/);
230             my $widthparm = $r->dir_config('ImgResizeWidthParam') || 'w';
231             my $heightparm = $r->dir_config('ImgResizeHeightParam') || 'h';
232             my $default_quality = $r->dir_config('ImgResizeDefaultQuality') || '60';
233             my $default_qtype = $r->dir_config('ImgResizeQtype') || 'normal';
234             my $crop_aspect_ratio = $r->dir_config('ImgResizeCropToAspectRatio');
235             $crop_aspect_ratio = 1 unless defined $crop_aspect_ratio;
236              
237             # read basic input
238             my %img_args;
239             $img_args{w} = int( $r->param($widthparm) );
240             $img_args{h} = int( $r->param($heightparm) );
241             return OK unless $img_args{w} || $img_args{h};
242              
243             $img_args{cropto} = $r->param('cropto');
244             $img_args{reshape} = $r->param('reshape');
245             $img_args{enlarge} = $r->param('enlarge') || 0 ;
246             $img_args{crop_aspect_ratio} = defined $r->param('cropAR') ? $r->param('cropAR') : $crop_aspect_ratio;
247             $img_args{proportional} = $r->param('proportional');
248             $img_args{proportional} = 1 if not defined $img_args{proportional} or $img_args{proportional} eq '';
249             my $quality = $r->param('quality') || $default_quality;
250             $img_args{scale_type} = $r->param('scaletype');
251             $img_args{qtype} = $r->param('qtype') || $default_qtype;
252              
253             my $shrunk;
254             my ($name, $path, $suffix) = File::Basename::fileparse( $filename, '\.\w{2,5}' );
255              
256             unless ($nocache) {
257             my $docroot = $r->document_root;
258              
259             # interpolate the name of the cache directory if it has been supplied
260             $path =~ s/^$docroot/$cachedir/ if $cachedir;
261             $path =~ s/\/\//\//;
262             $shrunk = $path . $name . '_' . ( $img_args{w} || 'x' ) . '_' . ( $img_args{h} || 'x' );
263             $shrunk .= "_q$quality";
264              
265             if ($img_args{reshape} eq 'crop') {
266             $shrunk .= '_crop';
267             }
268              
269             if ($img_args{cropto} && $img_args{cropto} =~ /^(left|right|top|bottom)$/i) {
270             $shrunk .= "_".$img_args{cropto};
271             }
272              
273             if ($img_args{enlarge}) {
274             $shrunk .= "_enlarge";
275             }
276              
277             if ($img_args{proportional}) {
278             $shrunk .= "_proportional";
279             }
280              
281             if ($img_args{crop_aspect_ratio}) {
282             $shrunk .= "_cropAR";
283             }
284              
285             if ($img_args{scale_type}) {
286             $shrunk .= "_scaletype".$img_args{scale_type};
287             }
288              
289             if ($img_args{qtype}) {
290             $shrunk .= "_qtype".$img_args{qtype};
291             }
292              
293             $shrunk .= $suffix;
294              
295             if (file_ok( $shrunk, $filename )) {
296             $r->filename($shrunk);
297             $r->finfo(APR::Finfo::stat($shrunk, APR::Const::FINFO_NORM, $r->pool));
298             my $mtime = (stat( $shrunk ))[9];
299             utime time, $mtime, $shrunk;
300             return OK;
301             }
302              
303             # if we're using a separate cache directory, the necessary subdirectory might not exist yet
304              
305             if ($cachedir) {
306             eval { File::Path::mkpath($path) };
307             return fail( "mkpath failed for '$path': $@" ) if $@;
308             }
309             }
310              
311             # no cache hit, so we create an Imager object and go through the options
312             my $im = Imager->new;
313             $im->open( file => $filename ) or return fail("Cannot read $filename: " . $im->errstr);
314             $im = resize($im, \%img_args);
315              
316             # if the cache is disabled, we write the results directly back to the request.
317             # You shouldn't do this during fixup - though it works - so if running without a cache we ought to a perlhandler
318              
319             if ($nocache) {
320             my $type = $suffix;
321             $type =~ s/^\.//;
322             $type = 'jpeg' if $type eq 'jpg';
323              
324             my $imagedata;
325             $im->write(
326             type => $type,
327             jpegquality => $quality,
328             data => \$imagedata,
329             ) or return fail( "Failed to return image data: " . $im->errstr );
330              
331             $r->headers_out->{'Content-Length'} = length($imagedata);
332             $r->content_type("image/$type");
333             $r->print($imagedata);
334             return OK;
335              
336             # otherwise we write out the cache file and tell the request to use that filename
337              
338             } else {
339              
340             $im->write(
341             file => $shrunk,
342             jpegquality => $quality,
343             ) or return fail("Cannot write $shrunk: " . $im->errstr);
344              
345             $r->filename($shrunk);
346             $r->finfo(APR::Finfo::stat($shrunk, APR::Const::FINFO_NORM, $r->pool));
347             return OK;
348             }
349             }
350              
351             sub resize {
352             my $im = shift;
353             my $args = shift;
354              
355             my $imgwidth = $im->getwidth;
356             my $imgheight = $im->getheight;
357             my (%scale, %crop);
358              
359             ##############
360             # scale the image
361             if ($args->{w} && $args->{h}) {
362             if ($args->{reshape} eq 'crop') {
363             %scale = ();
364             }
365             else {
366             # Imager automatically resizes to make the larger image specified by the two dimensions
367             $scale{xpixels} = $args->{w};
368             $scale{ypixels} = $args->{h};
369             }
370              
371             } elsif ($args->{w}) {
372             $scale{xpixels} = $args->{w};
373              
374             } else {
375             $scale{ypixels} = $args->{h};
376             }
377              
378             if ($args->{qtype}) {
379             $scale{qtype} = $args->{qtype};
380             }
381              
382             if ($args->{scale_type}) {
383             $scale{type} = $args->{scale_type};
384             }
385              
386             # enlarge images only if the enlarge argument is set
387             if (
388             not $args->{enlarge}
389             and (
390             ( $scale{xpixels} and ($scale{xpixels} > $imgwidth) )
391             or ( $scale{ypixels} and ($scale{ypixels} > $imgheight) )
392             )
393             and (
394             ($args->{scale_type} ne 'min')
395             or (
396             ( $scale{xpixels} and ($scale{xpixels} > $imgwidth) )
397             and ( $scale{ypixels} and ($scale{ypixels} > $imgheight) )
398             )
399             )
400             ) {
401             %scale = ();
402             }
403              
404             if ( not $args->{proportional} and $scale{xpixels} and $scale{ypixels} ) {
405             $im = $im->scaleX(pixels=>$scale{xpixels})->scaleY(pixels=>$scale{ypixels});
406             }
407             elsif( %scale ) {
408             $im = $im->scale( %scale );
409             }
410              
411             ###############
412             # crop the image
413             if ($args->{w} && $args->{h} && ($args->{crop_aspect_ratio} || $args->{reshape} || $args->{cropto}) ) {
414              
415             # $dw and $dh are the multipliers by which each dimension is changing
416              
417             my $dw = $imgwidth / $args->{w} if $args->{w};
418             my $dh = $imgheight / $args->{h} if $args->{h};
419              
420             # cropto should really be a list parameter so that we can choose top left
421              
422             if ($args->{reshape} eq 'crop') {
423             if ($args->{cropto} eq 'left') {
424             $crop{left} = 0;
425             $crop{width} = $args->{w};
426             $crop{height} = $args->{h};
427              
428             } elsif ($args->{cropto} eq 'right') {
429             $crop{right} = $imgwidth;
430             $crop{width} = $args->{w};
431             $crop{height} = $args->{h};
432              
433             } elsif ($args->{cropto} eq 'top') {
434             $crop{top} = 0;
435             $crop{width} = $args->{w};
436             $crop{height} = $args->{h};
437              
438             } elsif ($args->{cropto} eq 'bottom') {
439             $crop{bottom} = $imgheight;
440             $crop{width} = $args->{w};
441             $crop{height} = $args->{h};
442              
443             } else {
444             $crop{width} = $args->{w};
445             $crop{height} = $args->{h};
446             }
447              
448             } elsif ($dw > $dh) {
449              
450             if ($args->{cropto} eq 'left') {
451             $crop{left} = 0;
452             $crop{width} = $args->{w};
453             } elsif ($args->{cropto} eq 'right') {
454             $crop{right} = $im->getwidth;
455             $crop{width} = $args->{w};
456             } else {
457             $crop{width} = $args->{w};;
458             }
459              
460             } elsif ($dh > $dw) {
461              
462             if ($args->{cropto} eq 'top') {
463             $crop{top} = 0;
464             $crop{height} = $args->{h};
465             } elsif ($args->{cropto} eq 'bottom') {
466             $crop{bottom} = $im->getheight;
467             $crop{height} = $args->{h};
468             } else {
469             $crop{height} = $args->{h};
470             }
471             }
472              
473             # if dw == dh, no cropping is required.
474             # then we scale the image, if any resizing remains to be done
475              
476             $im = $im->crop( %crop ) if %crop;
477             }
478              
479             return $im;
480             }
481              
482             # file_ok tests whether the given file exists and is useable
483             # you can also supply the original file path as a second parameter: in that case
484             # we will test whether the original is newer than our file, and reject the file if it is
485              
486             sub file_ok {
487             my ($filename, $original) = @_;
488             return unless -e $filename;
489             return unless -r _;
490             return unless -s _;
491             return if $original && -M _ > -M $original; # nb. this is age, not time
492             return 1;
493             }
494              
495             # a general purpose and rather feeble error handler. This should log through the request, at least.
496              
497             sub fail {
498             my $message = shift;
499             my ($package, $filename, $line) = caller;
500             warn "$message at $package line $line\n";
501             return SERVER_ERROR;
502             }
503              
504             =head1 BUGS
505              
506             No doubt. Reports in rt.cpan.org would be much appreciated.
507              
508             =head1 TODO
509              
510             =over
511              
512             =item * Accept more than one cropto parameter, eg top and left.
513              
514             =item * tests
515              
516             =back
517              
518             =head1 SEE ALSO
519              
520             L L L L
521              
522             =head1 AUTHOR
523              
524             Alexander Keusch, C<< >>
525              
526             =head1 CONTRIBUTORS
527              
528             William Ross, C<< >>
529             Jozef Kutej, C<< >>
530              
531             =head1 COPYRIGHT
532              
533             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
534              
535             =cut
536              
537             1;