File Coverage

blib/lib/Plack/Middleware/Image/Scale.pm
Criterion Covered Total %
statement 20 22 90.9
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 28 30 93.3


line stmt bran cond sub pod time code
1 3     3   14113 use strict;
  3         4  
  3         117  
2             package Plack::Middleware::Image::Scale;
3             BEGIN {
4 3     3   41 $Plack::Middleware::Image::Scale::VERSION = '0.002'; # TRIAL
5             }
6             # ABSTRACT: Resize jpeg and png images on the fly
7              
8 3     3   1247 use Moose;
  3         919172  
  3         20  
9 3     3   15117 use Class::MOP;
  3         5  
  3         58  
10 3     3   673 use Plack::Util;
  3         7112  
  3         61  
11 3     3   416 use Plack::MIME;
  3         565  
  3         64  
12 3     3   12 use Try::Tiny;
  3         3  
  3         203  
13 3     3   1016 use Image::Scale;
  0            
  0            
14             use List::Util qw( max );
15             use Carp;
16              
17             extends 'Plack::Middleware';
18              
19              
20             has match_path => (
21             is => 'rw', lazy => 1, isa => 'RegexpRef',
22             default => sub { qr<^(.+)_(.+)\.(png|jpg|jpeg)$> }
23             );
24              
25              
26             has match_spec => (
27             is => 'rw', lazy => 1, isa => 'RegexpRef',
28             default => sub { qr<^(\d+)?x(\d+)?(?:-(.+))?$> }
29             );
30              
31              
32             has orig_ext => (
33             is => 'rw', lazy => 1, isa => 'ArrayRef',
34             default => sub { [qw( jpg png gif )] }
35             );
36              
37              
38             has memory_limit => (
39             is => 'rw', lazy => 1, isa => 'Int',
40             default => 10_000_000 # bytes
41             );
42              
43              
44             has jpeg_quality => (
45             is => 'rw', lazy => 1, isa => 'Maybe[Int]',
46             default => undef
47             );
48              
49              
50             sub call {
51             my ($self,$env) = @_;
52              
53             ## Check that uri matches and extract the pieces, or pass thru
54             my @match_path = $env->{PATH_INFO} =~ $self->match_path;
55             return $self->app->($env) unless @match_path;
56              
57             ## Extract image size and flags
58             my ($basename,$prop,$ext) = @match_path;
59             my @match_spec = $prop =~ $self->match_spec;
60             return $self->app->($env) unless @match_spec;
61              
62             my $res = $self->fetch_orig($env,$basename);
63             return $self->app->($env) unless $res;
64              
65             ## Post-process the response with a body filter
66             Plack::Util::response_cb( $res, sub {
67             my $res = shift;
68             my $ct = Plack::MIME->mime_type(".$ext");
69             Plack::Util::header_set( $res->[1], 'Content-Type', $ct );
70             return $self->body_scaler( $ct, @match_spec );
71             });
72             }
73              
74              
75             sub fetch_orig {
76             my ($self,$env,$basename) = @_;
77              
78             for my $ext ( @{$self->orig_ext} ) {
79             local $env->{PATH_INFO} = "$basename.$ext";
80             my $r = $self->app->($env);
81             return $r unless ref $r eq 'ARRAY' and $r->[0] == 404;
82             }
83             return;
84             }
85              
86              
87             sub body_scaler {
88             my $self = shift;
89             my @args = @_;
90              
91             my $buffer = q{};
92             my $filter_cb = sub {
93             my $chunk = shift;
94              
95             ## Buffer until we get EOF
96             if ( defined $chunk ) {
97             $buffer .= $chunk;
98             return q{}; #empty
99             }
100              
101             ## Return EOF when done
102             return if not defined $buffer;
103              
104             ## Process the buffer
105             my $img = $self->image_scale(\$buffer,@args);
106             undef $buffer;
107             return $img;
108             };
109              
110             return $filter_cb;
111             }
112              
113              
114             sub image_scale {
115             my ($self, $bufref, $ct, $width, $height, $flags) = @_;
116             my %flag = map { (split /(?<=\w)(?=\d)/, $_, 2)[0,1]; } split '-', $flags || '';
117             my $owidth = $width;
118             my $oheight = $height;
119              
120             if ( defined $flag{z} and $flag{z} > 0 ) {
121             $width *= 1 + $flag{z} / 100 if $width;
122             $height *= 1 + $flag{z} / 100 if $height;
123             }
124              
125             my $output;
126             try {
127             my $img = Image::Scale->new($bufref);
128              
129             if ( exists $flag{crop} and defined $width and defined $height ) {
130             my $ratio = $img->width / $img->height;
131             $width = max $width , $height * $ratio;
132             $height = max $height, $width / $ratio;
133             }
134              
135             unless ( defined $width or defined $height ) {
136             ## We want to keep the size, but Image::Scale
137             ## doesn't return data unless we call resize.
138             $width = $img->width; $height = $img->height;
139             }
140             $img->resize({
141             defined $width ? (width => $width) : (),
142             defined $height ? (height => $height) : (),
143             exists $flag{fill} ? (keep_aspect => 1) : (),
144             defined $flag{fill} ? (bgcolor => hex $flag{fill}) : (),
145             memory_limit => $self->memory_limit,
146             });
147              
148             $output = $ct eq 'image/jpeg' ? $img->as_jpeg($self->jpeg_quality || ()) :
149             $ct eq 'image/png' ? $img->as_png :
150             die "Conversion to $ct is not implemented";
151             } catch {
152             carp $_;
153             return;
154             };
155              
156             if ( defined $owidth and $width > $owidth or
157             defined $oheight and $height > $oheight ) {
158             try {
159             Class::MOP::load_class('Imager');
160             my $img = Imager->new;
161             $img->read( data => $output ) || die;
162             my $crop = $img->crop(
163             defined $owidth ? (width => $owidth) : (),
164             defined $oheight ? (height => $oheight) : (),
165             );
166             $crop->write( data => \$output, type => (split '/', $ct)[1] );
167             } catch {
168             carp $_;
169             return;
170             };
171             }
172              
173             return $output;
174             }
175              
176             1;
177              
178              
179             __END__
180             =pod
181              
182             =head1 NAME
183              
184             Plack::Middleware::Image::Scale - Resize jpeg and png images on the fly
185              
186             =head1 VERSION
187              
188             version 0.002
189              
190             =head1 SYNOPSIS
191              
192             # app.psgi
193             builder {
194             enable 'ConditionalGET';
195             enable 'Image::Scale';
196             enable 'Static', path => qr{^/images/};
197             $app;
198             };
199              
200             Start the application with L<plackup|Plack>.
201              
202             Suppose you have original image in path images/foo.jpg.
203             Now you can do request to an uri like /images/foo_40x40.png to
204             convert the size and format of the image, on the fly.
205              
206             Attributes can be set with named parameters to C<enable>.
207              
208             #...
209             enable 'Image::Scale', jpeg_quality => 60;
210              
211             See L</ATTRIBUTES> for configuration options.
212              
213             =head1 DESCRIPTION
214              
215             B<This is a trial release. The interface may change.>
216              
217             This middleware implements a I<content filter> that scales images to
218             requested sizes on the fly. The conversion happens only if the response
219             body is actually used.
220              
221             This module should be use in conjunction with a cache that stores
222             and revalidates the entries.
223              
224             The format of the output is defined by the width, height, flags and extension,
225             that are extracted from the request URI as defined in L</call>.
226              
227             =head2 width
228              
229             Width of the output image. If not defined, it can be anything
230             (to preserve the image aspect ratio).
231              
232             =head2 height
233              
234             Height of the output image. If not defined, it can be anything
235             (to preserve the image aspect ratio).
236              
237             =head2 flags
238              
239             Multiple flags can be defined by joining them with C<->.
240             See L</image_scale>.
241              
242             =head3 fill
243              
244             Image aspect ratio is preserved by scaling the image to fit
245             within the specified size. Extra borders of background color
246             are added to fill the requested image size.
247              
248             If fill has a value (for example C<fill0xff0000> for red), it specifies the
249             background color to use. Undefined color with png output means transparent
250             background.
251              
252             =head3 crop
253              
254             Image aspect ratio is preserved by cropping from middle of the image.
255              
256             =head3 z
257              
258             Zoom the original image N percent bigger. For example C<z20> to zoom 20%.
259             The zooming applies only to defined width or height, and does not change
260             the crop size.
261              
262             =head1 ATTRIBUTES
263              
264             =head2 match_path
265              
266             Only matching URIs are processed with this module. The match is done against
267             L<PATH_INFO|PSGI/The_Environment>.
268             Non-matching requests are delegated to the next middleware layer or
269             application. The value must be a
270             L<RegexpRef|Moose::Util::TypeConstraints/Default_Type_Constraints>,
271             that returns 3 captures. Default value is:
272              
273             qr<^(.+)_(.+)\.(png|jpg|jpeg)$>
274              
275             First capture is the path and basename of the requested image.
276             The original image will be fetched by L</fetch_orig> with this argument.
277             See L</call>.
278              
279             Second capture is the size specification for the requested image.
280             See L</match_spec>.
281              
282             Third capture is the extension for the desired output format. This extension
283             is mapped with L<Plack::MIME> to the Content-Type to be used in the HTTP
284             response. The content type defined the output format used in image processing.
285             Currently only jpeg and png format are supported.
286             See L</call>.
287              
288             =head2 match_spec
289              
290             The size specification captured by L</match_path> is matched against this.
291             Only matching URIs are processed with this module. The value must be a
292             L<RegexpRef|Moose::Util::TypeConstraints/Default_Type_Constraints>.
293             Default values is:
294              
295             qr<^(\d+)?x(\d+)?(?:-(.+))?$>
296              
297             First and second captures are the desired width and height of the
298             resulting image. Both are optional, but note that the "x" between is required.
299              
300             Third capture is an optional flag. This value is parsed in the
301             L</image_scale> method during the conversion.
302              
303             =head2 orig_ext
304              
305             L<ArrayRef|Moose::Util::TypeConstraints/Default_Type_Constraints>
306             of possible original image formats. See L</fetch_orig>.
307              
308             =head2 memory_limit
309              
310             Memory limit for the image scaling in bytes, as defined in
311             L<Image::Scale|Image::Scale/resize(_\%OPTIONS_)>.
312              
313             =head2 jpeg_quality
314              
315             JPEG quality, as defined in
316             L<Image::Scale|Image::Scale/as_jpeg(_[_$QUALITY_]_)>.
317              
318             =head1 METHODS
319              
320             =head2 call
321              
322             Process the request. The original image is fetched from the backend if
323             L</match_path> and L</match_spec> match as specified. Normally you should
324             use L<Plack::Middleware::Static> or similar backend, that
325             returns a filehandle or otherwise delayed body. Content-Type of the response
326             is set according the request.
327              
328             The body of the response is replaced with a
329             L<streaming body|PSGI/Delayed_Reponse_and_Streaming_Body>
330             that implements a content filter to do the actual resizing for the original
331             body. This means that if response body gets discarded due to header validation
332             (If-Modified-Since or If-None-Match for example), the body never needs to be
333             processed.
334              
335             However, if the original image has been modified, for example the modification
336             date has been changed, the streaming body gets passed to the HTTP server
337             (or another middleware layer that needs to read it in), and the conversion
338             happens.
339              
340             =head2 fetch_orig
341              
342             The original image is fetched from the next layer or application.
343             The basename of the request URI, as matched by L</match_path>, and all
344             possible extensions defined in L</orig_ext> are used in order, to search
345             for the original image. All other responses except a straight 404 (as
346             returned by L<Plack::Middleware::Static> for example) are considered
347             matches.
348              
349             =head2 body_scaler
350              
351             Create a content filter callback to do the conversion with specified arguments.
352             The callback binds to a closure with a buffer and the image_scale arguments.
353             The callback will buffer the response and call L</image_scale> after an EOF.
354              
355             =head2 image_scale
356              
357             Do the actual scaling and cropping of the image.
358             Arguments are width, height and flags, as parsed in L</call>.
359              
360             Multiple flags can be specified separated with a C<-> (hyphen).
361              
362             Flags can be boolean (exists or doesn't exist), or have a numerical
363             value. Flag name and value are separated with a zero-width word-to-number
364             boundary. For example C<z20> specifies flag C<z> with value C<20>.
365              
366             See L</DESCRIPTION> for description of various sizes and flags.
367              
368             =head1 CAVEATS
369              
370             The cropping requires L<Imager>. This is a run-time dependency, and
371             fallback is not to crop the image to the desired size.
372              
373             =head1 SEE ALSO
374              
375             L<Image::Scale>
376              
377             L<Imager>
378              
379             =head1 AUTHOR
380              
381             Panu Ervamaa <pnu@cpan.org>
382              
383             =head1 COPYRIGHT AND LICENSE
384              
385             This software is copyright (c) 2011 by Panu Ervamaa.
386              
387             This is free software; you can redistribute it and/or modify it under
388             the same terms as the Perl 5 programming language system itself.
389              
390             =cut
391