File Coverage

blib/lib/Plack/App/ImageMagick.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Plack::App::ImageMagick;
2             BEGIN {
3 1     1   923067 $Plack::App::ImageMagick::AUTHORITY = 'cpan:AJGB';
4             }
5             BEGIN {
6 1     1   38 $Plack::App::ImageMagick::VERSION = '1.110990';
7             }
8             # ABSTRACT: Create and manipulate images with Image::Magick
9              
10 1     1   10 use strict;
  1         3  
  1         38  
11 1     1   6 use warnings;
  1         3  
  1         49  
12              
13 1     1   5 use parent qw( Plack::Component );
  1         2  
  1         7  
14              
15 1     1   1581 use Image::Magick;
  0            
  0            
16             use Plack::App::File;
17             use File::Spec ();
18             use JSON::XS ();
19             use Digest::MD5 ();
20             use Plack::Request;
21             use HTTP::Date ();
22             use Plack::Util ();
23             use String::Bash ();
24             use Try::Tiny;
25              
26             use Plack::Util::Accessor qw(
27             handler
28             pre_process
29             post_process
30             apply
31             with_query
32             root
33             cache_dir
34             );
35              
36             my %replace_img_methods = map { $_ => 1 } qw(
37             FlattenImage
38             );
39              
40             my %push2stack_img_methods = map { $_ => 1 } qw(
41             Clone
42             EvaluateImages
43             Fx
44             Smush
45             Transform
46             );
47              
48             sub new {
49             my $class = shift;
50              
51             my $self = $class->SUPER::new(@_);
52              
53             my $apply = $self->apply;
54             my $handler = $self->handler;
55              
56             die "handler or apply is required"
57             unless defined $handler || defined $apply;
58              
59             die "handler and apply are mutually exclusive"
60             if defined $handler && defined $apply;
61              
62             die "with_query requires apply"
63             if defined $self->with_query && ! defined $apply;
64              
65             die "pre/post processing methods are allowed only for apply option"
66             if ! defined $apply && (
67             defined $self->pre_process
68             ||
69             defined $self->post_process
70             );
71              
72             die "apply should be non-empty array reference"
73             if defined $apply && (
74             ref $apply ne 'ARRAY'
75             ||
76             scalar @$apply == 0
77             );
78              
79              
80             return $self;
81             }
82              
83             sub call {
84             my ($self, $env) = @_;
85              
86             my $request_uri = $env->{REQUEST_URI};
87              
88             # try loading from cache
89             if ( $self->cache_dir ) {
90             my $cached_file = File::Spec->catfile(
91             $self->cache_dir,
92             Digest::MD5::md5_hex( $request_uri )
93             );
94              
95             if ( -r $cached_file ) {
96             return $self->_create_response_from_cache( $env, $cached_file );
97             }
98             }
99              
100             my $handler;
101             my $img = Image::Magick->new;
102              
103             if ( my $commands = $self->apply ) {
104              
105             # expand options from query string
106             if ( my $with_query = $self->with_query ) {
107             my $req = Plack::Request->new($env);
108             my $encoded = JSON::XS::encode_json( $commands );
109              
110             my $query_params = $req->query_parameters;
111             my $params = {};
112              
113             for my $param ( $query_params->keys ) {
114             # use last value
115             my $val = ($query_params->get_all($param))[-1];
116              
117             if ( $val ) {
118             # special chars forbidden
119             return http_response_403() unless $val =~ /\A[\w ]+\z/s;
120              
121             $params->{ $param } = $val;
122             };
123             };
124              
125             # params expanded
126             try {
127             $commands = JSON::XS::decode_json( String::Bash::bash($encoded, $params) );
128             } catch {
129             warn "Parsing query failed: $_";
130             return http_response_500();
131             };
132             }
133              
134             # create handler from commands
135             $handler = sub {
136             my ($app, $env, $img) = @_;
137              
138             unless ( ref $img eq 'Image::Magick' ) {
139             warn "Invalid object $img, required Image::Magick";
140             return http_response_500();
141             }
142              
143             # working on existing image
144             if ( my $img_root = $self->root ) {
145             my $path = File::Spec->catfile( $img_root, $env->{PATH_INFO} );
146             my $err = $img->Read( $path );
147             if ( "$err" ) {
148             warn "Read($path) failed: $err";
149             return http_response_404();
150             }
151             }
152              
153             for (my $i = 0; $i < @$commands; $i += 2 ) {
154             my ($method, $args) = @{ $commands }[ $i .. $i + 1 ];
155              
156             my @opts;
157             if ( ref $args eq 'HASH' ) {
158             @opts = %$args;
159             } elsif ( ref $args eq 'ARRAY' ) {
160             @opts = @$args;
161             }
162              
163             unless ( $method ) {
164             warn "Undefined method at index: $i";
165             return http_response_500();
166             }
167             my $x = $img->$method( @opts );
168              
169             if ( exists $push2stack_img_methods{ $method } ) {
170             unless ( ref $x ) {
171             warn "$method(@opts) failed: $x";
172             return http_response_500();
173             };
174             push @$img, $x;
175             } elsif ( exists $replace_img_methods{ $method } ) {
176             unless ( ref $x ) {
177             warn "$method(@opts) failed: $x";
178             return http_response_500();
179             };
180              
181             $img = $x;
182             } elsif ( "$x" ) {
183             warn "$method(@opts) failed: $x";
184             return http_response_500();
185             }
186             }
187             return $img;
188             };
189             } else {
190             $handler = $self->handler;
191             };
192              
193             if ( defined $handler ) {
194              
195             if ( my $pre_process = $self->pre_process ) {
196             $img = $pre_process->($self, $env, $img);
197              
198             unless ( ref $img eq 'Image::Magick' ) {
199             warn "Invalid object $img, required Image::Magick";
200             return http_response_500();
201             }
202             }
203              
204             if ( my $out = $handler->($self, $env, $img) ) {
205             if ( ref $out ne 'Image::Magick' ) {
206             return $out;
207             }
208              
209             if ( my $post_process = $self->post_process ) {
210             $out = $post_process->($self, $env, $out);
211              
212             unless ( ref $out eq 'Image::Magick' ) {
213             warn "Invalid object $out, required Image::Magick";
214             return http_response_500();
215             }
216              
217             }
218              
219             # flatten image before rendering
220             if ( @$out > 1 ) {
221             $out = $out->FlattenImage();
222             unless ( ref $out ) {
223             warn "FlattenImage() failed: $out";
224             return http_response_500();
225             };
226             }
227              
228             my $res;
229             if ( $self->cache_dir ) {
230             my $cached_file = File::Spec->catfile(
231             $self->cache_dir,
232             Digest::MD5::md5_hex( $request_uri )
233             );
234              
235             my $x = $out->Write( filename => $cached_file );
236             if ( "$x" ) {
237             warn "Write($cached_file) failed: $x";
238             return http_response_500();
239             };
240              
241             # serve via Plack::App::File, so middleware like XSendfile
242             # can be used
243             $res = $self->_create_response_from_cache(
244             $env, $cached_file, $out->Get('mime')
245             );
246             } else {
247             # use image blob as body
248             $res = $self->_create_response_from_img( $out );
249             }
250              
251             undef $out;
252             return $res;
253             }
254             }
255              
256             # we are supposed to do something
257             return http_response_500();
258             }
259              
260             sub _create_response_from_cache {
261             my ($self, $env, $file_path, $content_type) = @_;
262              
263             # discover content type from cached file
264             unless ( $content_type ) {
265             my $img = Image::Magick->new;
266             my $format = ($img->Ping( $file_path ))[3];
267             $content_type = $img->MagickToMime( $format );
268             };
269              
270              
271             my $file_app = Plack::App::File->new(
272             file => $file_path,
273             content_type => $content_type,
274             );
275              
276             local $env->{PATH_INFO} = $file_path;
277             return $file_app->call( $env );
278             };
279              
280             sub _create_response_from_img {
281             my ($self, $img) = @_;
282              
283             my $data = join('', $img->ImageToBlob);
284              
285             return [
286             200,
287             [
288             'Content-Type' => $img->Get('mime'),
289             'Content-Length' => length $data,
290             # be proxy friendly
291             'Last-Modified' => HTTP::Date::time2str( time ),
292             ],
293             [ $data ]
294             ];
295             }
296              
297              
298             # in case someone wants pretty error messages in subclasses those are public
299             sub http_response_403 {
300             my $self = shift;
301              
302             return [ 403,
303             [
304             'Content-Type' => 'text/plain',
305             'Content-Length' => 12,
306             ],
307             [ '403 Forbidden' ]
308             ]
309             }
310              
311             sub http_response_404 {
312             my $self = shift;
313              
314             return [ 404,
315             [
316             'Content-Type' => 'text/plain',
317             'Content-Length' => 12,
318             ],
319             [ '404 Not Found' ]
320             ]
321             }
322              
323             sub http_response_500 {
324             my $self = shift;
325              
326             return [ 500,
327             [
328             'Content-Type' => 'text/plain',
329             'Content-Length' => 22,
330             ],
331             [ '500 Service Unavailable' ]
332             ]
333             }
334              
335             1;
336              
337             __END__