File Coverage

lib/Dancer/Plugin/Thumbnail.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 Dancer::Plugin::Thumbnail;
2              
3             =head1 NAME
4              
5             Dancer::Plugin::Thumbnail - Easy thumbnails creating with Dancer and GD
6              
7             =cut
8              
9 9     9   1763053 use Dancer ':syntax';
  9         148204  
  9         36  
10 9     9   2114 use Dancer::MIME;
  9         13  
  9         168  
11 9     9   3613 use Dancer::Plugin;
  9         8897  
  9         505  
12 9     9   800697 use GD::Image;
  0            
  0            
13             use JSON::MaybeXS;
14             use List::Util qw( min max );
15             use Object::Signature;
16             use POSIX 'strftime';
17              
18              
19             =head1 VERSION
20              
21             Version 0.13
22              
23             =cut
24              
25             our $VERSION = '0.13';
26              
27              
28             =head1 SYNOPSIS
29              
30             use Dancer;
31             use Dancer::Plugin::Thumbnail;
32              
33             # simple resize
34             get '/resized/:width/:image' => sub {
35             resize param('image') => { w => param 'width' };
36             }
37              
38             # simple crop
39             get '/cropped/:width/:image' => sub {
40             crop param('image') => { w => param 'width' };
41             }
42              
43             # more complex
44             get '/thumb/:w/:h/:image' => sub {
45             thumbnail param('image') => [
46             crop => { w => 200, h => 200, a => 'lt' },
47             resize => { w => param('w'), h => param('h'), s => 'min' },
48             ], { format => 'jpeg', quality => 90 };
49             }
50              
51              
52             =head1 METHODS
53              
54             =head2 thumbnail ( $file, \@operations, \%options )
55              
56             Makes thumbnail image from original file by chain of graphic operations.
57             Image file name may be an absolute path or relative from config->{'public'}.
58             Each operation is a reference for two elements array. First element
59             is an operation name (currently supported 'resize' and 'crop') and second is
60             operation arguments as hash reference (described in appropriate operation
61             section).
62              
63             After operations chain completed final image creates with supplied options:
64              
65             =over
66              
67             =item cache
68              
69             Directory name for storing final results. Undefined setting (default) breaks
70             caching and isn't recommended for any serious production usage. Relative
71             cache directory will be prefixed with config->{'appdir'} automatically.
72             Cache path is generated from original file name, its modification time,
73             operations with arguments and an options. If you are worried about cache
74             garbage collecting you can create a simple cron job like:
75              
76             find /cache/path -type f -not -newerat '1 week ago' -delete
77              
78             =item format
79              
80             Specifies output image format. Supported formats are 'gif', 'jpeg' and 'png'.
81             Special format 'auto' (which is default) creates the same format as original
82             image has.
83              
84             =item compression
85              
86             PNG compression level. From '0' (no compression) to '9' (maximum).
87             Default is '-1' (default GD compression level for PNG creation).
88              
89             =item quality
90              
91             JPEG quality specifications. From '0' (the worse) to '100' (the best).
92             Default is 'undef' (default GD quality for JPEG creation).
93              
94             =back
95              
96             Defaults for these options can be specified in config.yml:
97              
98             plugins:
99             Thumbnail:
100             cache: var/cache
101             compression: 7
102             quality: 50
103              
104             =cut
105              
106             sub thumbnail {
107             my ( $file, $opers, $opts ) = @_;
108              
109             # load settings
110             my $conf = plugin_setting;
111              
112             # file argument is required
113             unless ( $file ) {
114             status 404;
115             return '404 Not Found';
116             }
117              
118             # create an absolute path
119             $file = path config->{ public }, $file
120             unless $file =~ m{^/};
121              
122             # check for file existance and readabilty
123             unless ( -f $file && -r _ ) {
124             status 404;
125             return '404 Not Found';
126             }
127              
128             # try to get stat info
129             my @stat = stat $file or do {
130             status 404;
131             return '404 Not Found';
132             };
133              
134             # prepare Last-Modified header
135             my $lmod = strftime '%a, %d %b %Y %H:%M:%S GMT', gmtime $stat[9];
136              
137             # processing conditional GET
138             if ( ( header('If-Modified-Since') || '' ) eq $lmod ) {
139             status 304;
140             return;
141             }
142              
143             # target format & content-type
144             my $mime = Dancer::MIME->instance;
145             my $fmt = $opts->{ format } || $conf->{ format } || 'auto';
146             my $type = $fmt eq 'auto' ?
147             $mime->for_file( $file ) :
148             $mime->for_name( $fmt )
149             ;
150             ( $fmt ) = $type->extensions
151             if $fmt eq 'auto';
152              
153             # target options
154             my $compression = $fmt eq 'png' ?
155             defined $opts->{ compression } ? $opts->{ compression } :
156             defined $conf->{ compression } ? $conf->{ compression } :
157             -1 : 0;
158             my $quality = $fmt eq 'jpeg' ?
159             ( exists $opts->{ quality } ?
160             $opts->{ quality } :
161             $conf->{ quality } ) :
162             undef;
163              
164             # try to resolve cache directory
165             my $cache_dir = exists $opts->{ cache } ? $opts->{ cache } : $conf->{ cache };
166              
167             if ( $cache_dir ) {
168             # check for an absolute path of cache directory
169             $cache_dir = path config->{ appdir }, $cache_dir
170             unless $cache_dir =~ m{^/};
171              
172             # check for existance of cache directory
173             unless ( -d $cache_dir && -w _ ) {
174             warning "no cache directory at '$cache_dir'";
175             undef $cache_dir;
176             }
177             }
178              
179             # cache path components
180             my ( $cache_key,@cache_hier,$cache_file );
181             if ( $cache_dir ) {
182             # key should include file, operations and calculated defaults
183             $cache_key = Object::Signature::signature(
184             [ $file,$stat[9],$opers,$quality,$compression ]
185             );
186             @cache_hier = map { substr $cache_key,$_->[0],$_->[1] } [0,1],[1,2];
187             $cache_file = path $cache_dir,@cache_hier,$cache_key;
188              
189             # try to get cached version
190             if ( -f $cache_file ) {
191             open FH, '<:raw', $cache_file or do {
192             error "can't read cache file '$cache_file'";
193             status 500;
194             return '500 Internal Server Error';
195             };
196              
197             # skip meta info
198             local $/ = "\n\n"; ; undef $/;
199              
200             # send useful headers & content
201             content_type $type->type;
202             header 'Last-Modified' => $lmod;
203             return scalar ;
204             }
205             }
206              
207             # load source image
208             my $src_img = GD::Image->new( $file ) or do {
209             error "can't load image '$file'";
210             status 500;
211             return '500 Internal Server Error';
212             };
213              
214             # original sizes
215             my ($src_w,$src_h) = $src_img->getBounds;
216              
217             # destination image and its serialized form
218             my ($dst_img,$dst_bytes);
219              
220             # trasformations loop
221             for ( my $i=0; $i<$#$opers; $i+=2 ) {
222             # next task and its arguments
223             my ($op,$args) = @$opers[$i,$i+1];
224              
225             # target sizes
226             my $dst_w = $args->{ w } || $args->{ width };
227             my $dst_h = $args->{ h } || $args->{ height };
228              
229             for ( $op ) {
230             if ( $_ eq 'resize') {
231             my $scale_mode = $args->{ s } || $args->{ scale } || 'max';
232             do {
233             error "unknown scale mode '$scale_mode'";
234             status 500;
235             return '500 Internal Server Error';
236             } unless $scale_mode eq 'max' || $scale_mode eq 'min';
237              
238             # calculate scale
239             no strict 'refs';
240             my $scale = &{ $scale_mode }(
241             grep { $_ } $dst_w && $src_w/$dst_w,
242             $dst_h && $src_h/$dst_h
243             );
244             $scale = max $scale,1;
245              
246             # recalculate target sizes
247             ($dst_w,$dst_h) = map { sprintf '%.0f',$_/$scale } $src_w,$src_h;
248              
249             # create new image
250             $dst_img = GD::Image->new($dst_w,$dst_h,1) or do {
251             error "can't create image for '$file'";
252             status 500;
253             return '500 Internal Server Error';
254             };
255              
256             # resize!
257             $dst_img->copyResampled( $src_img,0,0,0,0,
258             $dst_w,$dst_h,$src_w,$src_h
259             );
260             }
261             elsif ( $_ eq 'crop' ) {
262             $dst_w = min $src_w, $dst_w || $src_w;
263             $dst_h = min $src_h, $dst_h || $src_h;
264              
265             # anchors
266             my ($h_anchor,$v_anchor) =
267             ( $args->{ a } || $args->{ anchors } || 'cm' ) =~
268             /^([lcr])([tmb])$/ or do {
269             error "invalid anchors: '$args->{ anchors }'";
270             status 500;
271             return '500 Internal Server Error';
272             };
273              
274             # create new image
275             $dst_img = GD::Image->new($dst_w,$dst_h,1) or do {
276             error "can't create image for '$file'";
277             status 500;
278             return '500 Internal Server Error';
279             };
280              
281             # crop!
282             $dst_img->copy( $src_img,0,0,
283             sprintf('%.0f',
284             $h_anchor eq 'l' ? 0 :
285             $h_anchor eq 'c' ? ($src_w-$dst_w)/2 :
286             $src_w - $dst_w
287             ),
288             sprintf('%.0f',
289             $v_anchor eq 't' ? 0 :
290             $v_anchor eq 'm' ? ($src_h-$dst_h)/2 :
291             $src_h - $dst_h
292             ),
293             $dst_w,$dst_h
294             );
295             }
296             else {
297             error "unknown operation '$op'";
298             status 500;
299             return '500 Internal Server Error';
300             }
301             }
302              
303             # keep destination image as original
304             ($src_img,$src_w,$src_h) = ($dst_img,$dst_w,$dst_h);
305             }
306              
307             # generate image
308             for ( $fmt ) {
309             if ( $_ eq 'gif' ) {
310             $dst_bytes = $dst_img->$_;
311             }
312             elsif ( $_ eq 'jpeg' ) {
313             $dst_bytes = $quality ? $dst_img->$_( $quality ) : $dst_img->$_;
314             }
315             elsif ( $_ eq 'png' ) {
316             $dst_bytes = $dst_img->$_( $compression );
317             }
318             else {
319             error "unknown format '$_'";
320             status 500;
321             return '500 Internal Server Error';
322             }
323             }
324              
325             # store to cache (if requested)
326             if ( $cache_file ) {
327             # create cache subdirectories
328             for ( @cache_hier ) {
329             next if -d ( $cache_dir = path $cache_dir,$_ );
330             mkdir $cache_dir or do {
331             error "can't create cache directory '$cache_dir'";
332             status 500;
333             return '500 Internal Server Error';
334             };
335             }
336             open FH, '>:raw', $cache_file or do {
337             error "can't create cache file '$cache_file'";
338             status 500;
339             return '500 Internal Server Error';
340             };
341             # store serialized meta information (for future using)
342             print FH encode_json({
343             args => \@_,
344             compression => $compression,
345             conf => $conf,
346             format => $fmt,
347             lmod => $lmod,
348             mtime => $stat[9],
349             quality => $quality,
350             type => $type->type,
351             }) . "\n\n";
352             # store actual target image
353             print FH $dst_bytes;
354             }
355              
356             # send useful headers & content
357             content_type $type->type;
358             header 'Last-Modified' => $lmod;
359             return $dst_bytes;
360             }
361              
362             register thumbnail => \&thumbnail;
363              
364              
365             =head2 crop ( $file, \%arguments, \%options )
366              
367             This is shortcut (syntax sugar) fully equivalent to call:
368              
369             thumbnail ( $file, [ crop => \%arguments ], \%options )
370              
371             Arguments includes:
372              
373             =over
374              
375             =item w | width
376              
377             Desired width (optional, default not to crop by horizontal).
378              
379             =item h | height
380              
381             Desired height (optional, default not to crop by vertical).
382              
383             =item a | anchors
384              
385             Two characters string which indicates desired fragment of original image.
386             First character can be one of 'l/c/r' (left/right/center), and second - 't/m/b'
387             (top/middle/bottom). Default is 'cm' (centered by horizontal and vertical).
388              
389             =back
390              
391             =cut
392              
393             register crop => sub {
394             thumbnail shift, [ crop => shift ], @_;
395             };
396              
397              
398             =head2 resize ( $file, \%arguments, \%options )
399              
400             This is shortcut and fully equivalent to call:
401              
402             thumbnail ( $file, [ resize => \%arguments ], \%options )
403              
404             Arguments includes:
405              
406             =over
407              
408             =item w | width
409              
410             Desired width (optional, default not to resize by horizontal).
411              
412             =item h | height
413              
414             Desired height (optional, default not to resize by vertical).
415              
416             =item s | scale
417              
418             The operation always keeps original image proportions.
419             Horizontal and vertical scales calculates separately and 'scale' argument
420             helps to select maximum or minimum from "canditate" values.
421             Argument can be 'min' or 'max' (which is default).
422              
423             =back
424              
425             =cut
426              
427              
428             register resize => sub {
429             thumbnail shift, [ resize => shift ], @_;
430             };
431              
432              
433             register_plugin;
434              
435              
436             =head1 AUTHOR
437              
438             Oleg A. Mamontov, C<< >>
439              
440             =head1 BUGS
441              
442             Please report any bugs or feature requests to C, or through
443             the web interface at L. I will be notified, and then you'll
444             automatically be notified of progress on your bug as I make changes.
445              
446              
447              
448              
449             =head1 SUPPORT
450              
451             You can find documentation for this module with the perldoc command.
452              
453             perldoc Dancer::Plugin::Thumbnail
454              
455              
456             You can also look for information at:
457              
458             =over 4
459              
460             =item * RT: CPAN's request tracker (report bugs here)
461              
462             L
463              
464             =item * AnnoCPAN: Annotated CPAN documentation
465              
466             L
467              
468             =item * CPAN Ratings
469              
470             L
471              
472             =item * Search CPAN
473              
474             L
475              
476             =back
477              
478              
479             =head1 LICENSE AND COPYRIGHT
480              
481             Copyright 2011 Oleg A. Mamontov.
482              
483             This program is free software; you can redistribute it and/or modify it
484             under the terms of either: the GNU General Public License as published
485             by the Free Software Foundation; or the Artistic License.
486              
487             See http://dev.perl.org/licenses/ for more information.
488              
489              
490             =cut
491              
492             1;
493