line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
2
|
|
|
2
|
|
23912
|
use strict; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
136
|
|
2
|
|
|
|
|
|
|
package Plack::Middleware::Image::Scale; |
3
|
|
|
|
|
|
|
BEGIN { |
4
|
2
|
|
|
2
|
|
63
|
$Plack::Middleware::Image::Scale::AUTHORITY = 'cpan:PNU'; |
5
|
|
|
|
|
|
|
} |
6
|
|
|
|
|
|
|
{ |
7
|
|
|
|
|
|
|
$Plack::Middleware::Image::Scale::VERSION = '0.009'; |
8
|
|
|
|
|
|
|
} |
9
|
|
|
|
|
|
|
# ABSTRACT: Resize jpeg and png images on the fly |
10
|
|
|
|
|
|
|
|
11
|
2
|
|
|
2
|
|
1374
|
use Moose; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
use Class::MOP; |
13
|
|
|
|
|
|
|
use Plack::Util; |
14
|
|
|
|
|
|
|
use Plack::MIME; |
15
|
|
|
|
|
|
|
use Try::Tiny; |
16
|
|
|
|
|
|
|
use Image::Scale; |
17
|
|
|
|
|
|
|
use List::Util qw( min max ); |
18
|
|
|
|
|
|
|
use Carp; |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
extends 'Plack::Middleware'; |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
has path => ( |
24
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'RegexpRef|CodeRef|Str|Undef', |
25
|
|
|
|
|
|
|
default => undef |
26
|
|
|
|
|
|
|
); |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
has match => ( |
30
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'RegexpRef|CodeRef', |
31
|
|
|
|
|
|
|
default => sub { qr{^(.+?)(?:_([^_]+?))?(?:\.(jpe?g|png|image))$} } |
32
|
|
|
|
|
|
|
); |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
has size => ( |
36
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'RegexpRef|CodeRef|HashRef|Undef', |
37
|
|
|
|
|
|
|
default => sub { qr{^(\d+)?x(\d+)?(?:-(.+))?$} } |
38
|
|
|
|
|
|
|
); |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
has any_ext => ( |
42
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'Str|Undef', |
43
|
|
|
|
|
|
|
default => 'image' |
44
|
|
|
|
|
|
|
); |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
has orig_ext => ( |
48
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'ArrayRef', |
49
|
|
|
|
|
|
|
default => sub { [qw( jpg png gif )] } |
50
|
|
|
|
|
|
|
); |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
has memory_limit => ( |
54
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'Int|Undef', |
55
|
|
|
|
|
|
|
default => 10_000_000 # bytes |
56
|
|
|
|
|
|
|
); |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
has jpeg_quality => ( |
60
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'Int|Undef', |
61
|
|
|
|
|
|
|
default => undef |
62
|
|
|
|
|
|
|
); |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
has width => ( |
66
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'Int|Undef', |
67
|
|
|
|
|
|
|
default => undef |
68
|
|
|
|
|
|
|
); |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
has height => ( |
72
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'Int|Undef', |
73
|
|
|
|
|
|
|
default => undef |
74
|
|
|
|
|
|
|
); |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
has flags => ( |
78
|
|
|
|
|
|
|
is => 'rw', lazy => 1, isa => 'HashRef|Undef', |
79
|
|
|
|
|
|
|
default => undef |
80
|
|
|
|
|
|
|
); |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
sub call { |
83
|
|
|
|
|
|
|
my ($self,$env) = @_; |
84
|
|
|
|
|
|
|
my $path = $env->{PATH_INFO}; |
85
|
|
|
|
|
|
|
my @param; |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
if ( defined $self->path ) { |
88
|
|
|
|
|
|
|
my ($m) = _match($path,$self->path); |
89
|
|
|
|
|
|
|
return $self->app->($env) unless $m; |
90
|
|
|
|
|
|
|
} |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
my @m = _match($path,$self->match); |
93
|
|
|
|
|
|
|
return $self->app->($env) unless @m; |
94
|
|
|
|
|
|
|
($path, my $size, my $ext) = @m; |
95
|
|
|
|
|
|
|
return $self->app->($env) unless $path and $ext; |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
if ( defined $size ) { |
98
|
|
|
|
|
|
|
@param = _unroll(_match($size,$self->size)); |
99
|
|
|
|
|
|
|
return $self->app->($env) unless @param; |
100
|
|
|
|
|
|
|
} |
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
my $res = $self->fetch_orig($env,$path); |
103
|
|
|
|
|
|
|
return $self->app->($env) unless $res; |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
## Post-process the response with a body filter |
106
|
|
|
|
|
|
|
$self->response_cb( $res, sub { |
107
|
|
|
|
|
|
|
my $res = shift; |
108
|
|
|
|
|
|
|
my $orig_ct = Plack::Util::header_get( $res->[1], 'Content-Type' ); |
109
|
|
|
|
|
|
|
my $ct; |
110
|
|
|
|
|
|
|
if ( defined $self->any_ext and $ext eq $self->any_ext ) { |
111
|
|
|
|
|
|
|
$ct = Plack::Util::header_get( $res->[1], 'Content-Type' ); |
112
|
|
|
|
|
|
|
} else { |
113
|
|
|
|
|
|
|
$ct = Plack::MIME->mime_type(".$ext"); |
114
|
|
|
|
|
|
|
Plack::Util::header_set( $res->[1], 'Content-Type', $ct ); |
115
|
|
|
|
|
|
|
} |
116
|
|
|
|
|
|
|
return $self->body_scaler( $ct, @param, $orig_ct ); |
117
|
|
|
|
|
|
|
}); |
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
## Helper for matching a Scalar value against CodeRef, HashRef, |
121
|
|
|
|
|
|
|
## RegexpRef or Str. The first argument may be modified during match. |
122
|
|
|
|
|
|
|
sub _match { |
123
|
|
|
|
|
|
|
my @match; |
124
|
|
|
|
|
|
|
for ( $_[0] ) { |
125
|
|
|
|
|
|
|
my $match = $_[1]; |
126
|
|
|
|
|
|
|
@match = |
127
|
|
|
|
|
|
|
'CODE' eq ref $match ? $match->($_) : |
128
|
|
|
|
|
|
|
'HASH' eq ref $match ? $match->{$_} : |
129
|
|
|
|
|
|
|
'Regexp' eq ref $match ? $_ =~ $match : |
130
|
|
|
|
|
|
|
defined $match ? (substr($_,0,length $match) eq $match ? ($match) : ()) : |
131
|
|
|
|
|
|
|
undef; |
132
|
|
|
|
|
|
|
} |
133
|
|
|
|
|
|
|
return @match; |
134
|
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
## Helper for extracting (width,height,flags) from |
137
|
|
|
|
|
|
|
## HashRef or ArrayRef. |
138
|
|
|
|
|
|
|
sub _unroll { |
139
|
|
|
|
|
|
|
return unless @_; |
140
|
|
|
|
|
|
|
for ( $_[0] ) { |
141
|
|
|
|
|
|
|
## Config::General style hash of hashrefs. |
142
|
|
|
|
|
|
|
if ( ref eq 'HASH' ) { |
143
|
|
|
|
|
|
|
my %e = %{$_}; |
144
|
|
|
|
|
|
|
return (delete @e{'width','height'}, \%e); |
145
|
|
|
|
|
|
|
## Manual config friendly hash of arraysrefs. |
146
|
|
|
|
|
|
|
} elsif ( ref eq 'ARRAY' ) { |
147
|
|
|
|
|
|
|
return @{$_}; |
148
|
|
|
|
|
|
|
} |
149
|
|
|
|
|
|
|
} |
150
|
|
|
|
|
|
|
return @_; |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
sub fetch_orig { |
155
|
|
|
|
|
|
|
my ($self,$env,$basename) = @_; |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
for my $ext ( @{$self->orig_ext} ) { |
158
|
|
|
|
|
|
|
local $env->{PATH_INFO} = "$basename.$ext"; |
159
|
|
|
|
|
|
|
my $r = $self->app->($env); |
160
|
|
|
|
|
|
|
return $r unless ref $r eq 'ARRAY' and $r->[0] == 404; |
161
|
|
|
|
|
|
|
} |
162
|
|
|
|
|
|
|
return; |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
sub body_scaler { |
167
|
|
|
|
|
|
|
my $self = shift; |
168
|
|
|
|
|
|
|
my @args = @_; |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
my $buffer = q{}; |
171
|
|
|
|
|
|
|
my $filter_cb = sub { |
172
|
|
|
|
|
|
|
my $chunk = shift; |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
## Buffer until we get EOF |
175
|
|
|
|
|
|
|
if ( defined $chunk ) { |
176
|
|
|
|
|
|
|
$buffer .= $chunk; |
177
|
|
|
|
|
|
|
return q{}; #empty |
178
|
|
|
|
|
|
|
} |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
## Return EOF when done |
181
|
|
|
|
|
|
|
return if not defined $buffer; |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
## Process the buffer |
184
|
|
|
|
|
|
|
my $img = $buffer ? $self->image_scale(\$buffer,@args) : ''; |
185
|
|
|
|
|
|
|
undef $buffer; |
186
|
|
|
|
|
|
|
return $img; |
187
|
|
|
|
|
|
|
}; |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
return $filter_cb; |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
sub image_scale { |
194
|
|
|
|
|
|
|
my ($self, $bufref, $ct, $width, $height, $flags, $orig_ct) = @_; |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
## $flags can be a HashRef, or it's parsed as a string |
197
|
|
|
|
|
|
|
my %flag = 'HASH' eq ref $flags ? %{ $flags } : |
198
|
|
|
|
|
|
|
map { (split /(?<=\w)(?=\d)/, $_, 2)[0,1]; } split '-', $flags || ''; |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
$width = $self->width if defined $self->width; |
201
|
|
|
|
|
|
|
$height = $self->height if defined $self->height; |
202
|
|
|
|
|
|
|
%flag = %{ $self->flags } if defined $self->flags; |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
my $owidth = $width; |
205
|
|
|
|
|
|
|
my $oheight = $height; |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
if ( defined $flag{z} and $flag{z} > 0 ) { |
208
|
|
|
|
|
|
|
$width *= 1 + $flag{z} / 100 if $width; |
209
|
|
|
|
|
|
|
$height *= 1 + $flag{z} / 100 if $height; |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
my $output; |
213
|
|
|
|
|
|
|
if ($orig_ct eq 'application/pdf') { |
214
|
|
|
|
|
|
|
try { |
215
|
|
|
|
|
|
|
Class::MOP::load_class('Image::Magick::Thumbnail::PDF'); |
216
|
|
|
|
|
|
|
Class::MOP::load_class('File::Temp'); |
217
|
|
|
|
|
|
|
my $in = File::Temp->new( SUFFIX => '.pdf' ); |
218
|
|
|
|
|
|
|
my $out = File::Temp->new( SUFFIX => '.png' ); |
219
|
|
|
|
|
|
|
$in->write( $$bufref ); $in->close; |
220
|
|
|
|
|
|
|
Image::Magick::Thumbnail::PDF::create_thumbnail( |
221
|
|
|
|
|
|
|
$in->filename, $out->filename, $flag{p}||1, { |
222
|
|
|
|
|
|
|
frame => 0, normalize => 0, |
223
|
|
|
|
|
|
|
restriction => max($width, $height), |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
); |
226
|
|
|
|
|
|
|
my $pdfdata; |
227
|
|
|
|
|
|
|
$out->seek( 0, 0 ); |
228
|
|
|
|
|
|
|
$out->read( $pdfdata, 9999999 ); |
229
|
|
|
|
|
|
|
$bufref = \$pdfdata; |
230
|
|
|
|
|
|
|
} catch { |
231
|
|
|
|
|
|
|
carp $_; |
232
|
|
|
|
|
|
|
$output = $$bufref; |
233
|
|
|
|
|
|
|
}; |
234
|
|
|
|
|
|
|
} |
235
|
|
|
|
|
|
|
try { |
236
|
|
|
|
|
|
|
my $img = Image::Scale->new($bufref) |
237
|
|
|
|
|
|
|
or die 'Invalid data / image format not recognized'; |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
if ( exists $flag{crop} and defined $width and defined $height ) { |
240
|
|
|
|
|
|
|
my $ratio = $img->width / $img->height; |
241
|
|
|
|
|
|
|
$width = max $width , $height * $ratio; |
242
|
|
|
|
|
|
|
$height = max $height, $width / $ratio; |
243
|
|
|
|
|
|
|
} elsif ( exists $flag{fit} and defined $width and defined $height ) { |
244
|
|
|
|
|
|
|
my $ratio = $img->width / $img->height; |
245
|
|
|
|
|
|
|
$width = min $width , $height * $ratio; |
246
|
|
|
|
|
|
|
$height = min $height, $width / $ratio; |
247
|
|
|
|
|
|
|
} |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
unless ( defined $width or defined $height ) { |
250
|
|
|
|
|
|
|
## We want to keep the size, but Image::Scale |
251
|
|
|
|
|
|
|
## doesn't return data unless we call resize. |
252
|
|
|
|
|
|
|
$width = $img->width; $height = $img->height; |
253
|
|
|
|
|
|
|
} |
254
|
|
|
|
|
|
|
$img->resize({ |
255
|
|
|
|
|
|
|
defined $width ? (width => $width) : (), |
256
|
|
|
|
|
|
|
defined $height ? (height => $height) : (), |
257
|
|
|
|
|
|
|
exists $flag{fill} ? (keep_aspect => 1) : (), |
258
|
|
|
|
|
|
|
defined $flag{fill} ? (bgcolor => hex $flag{fill}) : (), |
259
|
|
|
|
|
|
|
defined $self->memory_limit ? |
260
|
|
|
|
|
|
|
(memory_limit => $self->memory_limit) : (), |
261
|
|
|
|
|
|
|
}); |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
$output = $ct eq 'image/jpeg' ? $img->as_jpeg($self->jpeg_quality || ()) : |
264
|
|
|
|
|
|
|
$ct eq 'image/png' ? $img->as_png : |
265
|
|
|
|
|
|
|
die "Conversion to '$ct' is not implemented"; |
266
|
|
|
|
|
|
|
} catch { |
267
|
|
|
|
|
|
|
carp $_; |
268
|
|
|
|
|
|
|
$output = $$bufref; |
269
|
|
|
|
|
|
|
}; |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
if ( defined $owidth and $width > $owidth or |
272
|
|
|
|
|
|
|
defined $oheight and $height > $oheight ) { |
273
|
|
|
|
|
|
|
try { |
274
|
|
|
|
|
|
|
Class::MOP::load_class('Imager'); |
275
|
|
|
|
|
|
|
my $img = Imager->new; |
276
|
|
|
|
|
|
|
$img->read( data => $output ) || die; |
277
|
|
|
|
|
|
|
my $crop = $img->crop( |
278
|
|
|
|
|
|
|
defined $owidth ? (width => $owidth) : (), |
279
|
|
|
|
|
|
|
defined $oheight ? (height => $oheight) : (), |
280
|
|
|
|
|
|
|
); |
281
|
|
|
|
|
|
|
$crop->write( data => \$output, type => (split '/', $ct)[1] ); |
282
|
|
|
|
|
|
|
} catch { |
283
|
|
|
|
|
|
|
carp $_; |
284
|
|
|
|
|
|
|
}; |
285
|
|
|
|
|
|
|
} |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
return $output; |
288
|
|
|
|
|
|
|
} |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
1; |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
__END__ |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
=pod |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
=head1 NAME |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
Plack::Middleware::Image::Scale - Resize jpeg and png images on the fly |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
=head1 VERSION |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
version 0.009 |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=head1 SYNOPSIS |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
## example1.psgi |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
builder { |
309
|
|
|
|
|
|
|
enable 'ConditionalGET'; |
310
|
|
|
|
|
|
|
enable 'Image::Scale'; |
311
|
|
|
|
|
|
|
enable 'Static', path => qr{^/images/}; |
312
|
|
|
|
|
|
|
$app; |
313
|
|
|
|
|
|
|
}; |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
A request to /images/foo_40x40.png will use images/foo.(png|jpg|gif) as |
316
|
|
|
|
|
|
|
original, scale it to 40x40 px size and convert to PNG format. |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
## example2.psgi |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
my $thumber = builder { |
321
|
|
|
|
|
|
|
enable 'ConditionalGET'; |
322
|
|
|
|
|
|
|
enable 'Image::Scale', |
323
|
|
|
|
|
|
|
width => 200, height => 100, |
324
|
|
|
|
|
|
|
flags => { fill => 'ff00ff' }; |
325
|
|
|
|
|
|
|
Plack::App::File->new( root => 'images' ); |
326
|
|
|
|
|
|
|
}; |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
builder { |
329
|
|
|
|
|
|
|
mount '/thumbs' => $thumber; |
330
|
|
|
|
|
|
|
mount '/' => $app; |
331
|
|
|
|
|
|
|
}; |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
A request to /thumbs/foo.png will use images/foo.(png|jpg|gif) as original, |
334
|
|
|
|
|
|
|
scale it small enough to fit 200x100 px size, fill extra borders (top/down or |
335
|
|
|
|
|
|
|
left/right, depending on the original image aspect ratio) with cyan |
336
|
|
|
|
|
|
|
background, and convert to PNG format. Also clipping is available, see |
337
|
|
|
|
|
|
|
L</CONFIGURATION>. |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
## see example4.psgi |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
my %imagesize = Config::General->new('imagesize.conf')->getall; |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
# ... |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
enable 'Image::Scale', size => \%imagesize; |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
A request to /images/foo_medium.png will use images/foo.(png|jpg|gif) as |
348
|
|
|
|
|
|
|
original. The size and flags are taken from the configuration file as |
349
|
|
|
|
|
|
|
parsed by Config::General. |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
## imagesize.conf |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
<medium> |
354
|
|
|
|
|
|
|
width 200 |
355
|
|
|
|
|
|
|
height 100 |
356
|
|
|
|
|
|
|
crop |
357
|
|
|
|
|
|
|
</medium> |
358
|
|
|
|
|
|
|
<big> |
359
|
|
|
|
|
|
|
width 300 |
360
|
|
|
|
|
|
|
height 100 |
361
|
|
|
|
|
|
|
crop |
362
|
|
|
|
|
|
|
</big> |
363
|
|
|
|
|
|
|
<thumbred> |
364
|
|
|
|
|
|
|
width 50 |
365
|
|
|
|
|
|
|
height 100 |
366
|
|
|
|
|
|
|
fill ff0000 |
367
|
|
|
|
|
|
|
</thumbred> |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
For more examples, browse into directory |
370
|
|
|
|
|
|
|
L<eg|http://cpansearch.perl.org/src/PNU/> inside the distribution |
371
|
|
|
|
|
|
|
directory for this version. |
372
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
=head1 DESCRIPTION |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
Scale and convert images to the requested format on the fly. By default the |
376
|
|
|
|
|
|
|
size and other scaling parameters are extracted from the request URI. Scaling |
377
|
|
|
|
|
|
|
is done with L<Image::Scale>. |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
The original image is not modified or even accessed directly by this module. |
380
|
|
|
|
|
|
|
The converted image is not cached, but the request can be validated |
381
|
|
|
|
|
|
|
(If-Modified-Since) against original image without doing the image processing. |
382
|
|
|
|
|
|
|
This middleware should be used together a cache proxy, that caches the |
383
|
|
|
|
|
|
|
converted images for all clients, and implements content validation. |
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
The response headers (like Last-Modified or ETag) are from the original image, |
386
|
|
|
|
|
|
|
but body is replaced with a PSGI L<content |
387
|
|
|
|
|
|
|
filter|Plack::Middleware/RESPONSE_CALLBACK> to do the image processing. The |
388
|
|
|
|
|
|
|
original image is fetched from next middleware layer or application with a |
389
|
|
|
|
|
|
|
normal PSGI request. You can use L<Plack::Middleware::Static>, or |
390
|
|
|
|
|
|
|
L<Catalyst::Plugin::Static::Simple> for example. |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
See L</CONFIGURATION> for various size/format specifications that can be used |
393
|
|
|
|
|
|
|
in the request URI, and L</ATTRIBUTES> for common configuration options |
394
|
|
|
|
|
|
|
that you can use when constructing the middleware. |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
=head2 path |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
Must be a L<RegexpRef|Moose::Util::TypeConstraints/Default_Type_Constraints>, |
401
|
|
|
|
|
|
|
L<CodeRef|Moose::Util::TypeConstraints/Default_Type_Constraints>, |
402
|
|
|
|
|
|
|
L<Str|Moose::Util::TypeConstraints/Default_Type_Constraints> or |
403
|
|
|
|
|
|
|
L<Undef|Moose::Util::TypeConstraints/Default_Type_Constraints>. |
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
The L<PATH_INFO|PSGI/The_Environment> is compared against this value to |
406
|
|
|
|
|
|
|
evaluate if the request should be processed. Undef (the default) will match |
407
|
|
|
|
|
|
|
always. C<PATH_INFO> is topicalized by settings it to C<$_>, and it may be |
408
|
|
|
|
|
|
|
rewritten during C<CodeRef> matching. Rewriting can be used to relocate image |
409
|
|
|
|
|
|
|
paths, much like C<path> parameter for L<Plack::Middleware::Static>. |
410
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
If path matches, next it will be compared against L</name>. If path doesn't |
412
|
|
|
|
|
|
|
match, the request will be delegated to the next middleware layer or |
413
|
|
|
|
|
|
|
application. |
414
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
=head2 match |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
Must be a L<RegexpRef|Moose::Util::TypeConstraints/Default_Type_Constraints>, |
418
|
|
|
|
|
|
|
or L<CodeRef|Moose::Util::TypeConstraints/Default_Type_Constraints>. |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
The L<PATH_INFO|PSGI/The_Environment>, possibly rewritten during L</path> |
421
|
|
|
|
|
|
|
matching, is compared against this value to extract C<name>, C<size> |
422
|
|
|
|
|
|
|
and C<ext>. The default value is: |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
qr{^(.+)(?:_(.+?))?(?:\.(jpe?g|png|image))$} |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
The expression is evaluated in array context and may return three elements: |
427
|
|
|
|
|
|
|
C<name>, C<size> and C<ext>. Returning an empty array means no match. |
428
|
|
|
|
|
|
|
Non-matching requests are delegated to the next middleware layer or |
429
|
|
|
|
|
|
|
application. |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
If the path matches, the original image is fetched from C<name>.L</orig_ext>, |
432
|
|
|
|
|
|
|
scaled with parameters extracted from C<size> and converted to the content type |
433
|
|
|
|
|
|
|
defined by C<ext>. See also L</any_ext>. |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
=head2 size |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
Must be a L<RegexpRef|Moose::Util::TypeConstraints/Default_Type_Constraints>, |
438
|
|
|
|
|
|
|
L<CodeRef|Moose::Util::TypeConstraints/Default_Type_Constraints>, |
439
|
|
|
|
|
|
|
L<HashRef|Moose::Util::TypeConstraints/Default_Type_Constraints>, |
440
|
|
|
|
|
|
|
L<Undef|Moose::Util::TypeConstraints/Default_Type_Constraints>. |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
The C<size> extracted by L</match> is compared against this value to evaluate |
443
|
|
|
|
|
|
|
if the request should be processed, and to map it into width, height and flags |
444
|
|
|
|
|
|
|
for image processing. Undef will match always and use default width, height |
445
|
|
|
|
|
|
|
and flags as defined by the L</ATTRIBUTES>. The default value is: |
446
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
qr{^(\d+)?x(\d+)?(?:-(.+))?$} |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
The expression is evaluated in array context and may return three elements; |
450
|
|
|
|
|
|
|
C<width>, C<height> and C<flags>. Returning an empty array means no match. |
451
|
|
|
|
|
|
|
Non-matching requests are delegated to the next middleware layer or |
452
|
|
|
|
|
|
|
application. |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
Optionally a hash reference can be returned. Keys C<width>, C<height>, and any |
455
|
|
|
|
|
|
|
remaining keys as an hash reference, will be unrolled from the hash reference. |
456
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
=head2 any_ext |
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
If defined and request C<ext> is equal to this, the content type of the original |
460
|
|
|
|
|
|
|
image is used in the output. This means that the image format of the original |
461
|
|
|
|
|
|
|
image is preserved. Default is C<image>. |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
=head2 orig_ext |
464
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
L<ArrayRef|Moose::Util::TypeConstraints/Default_Type_Constraints> |
466
|
|
|
|
|
|
|
of possible original image formats. See L</fetch_orig>. |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
=head2 memory_limit |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
Memory limit for the image scaling in bytes, as defined in |
471
|
|
|
|
|
|
|
L<Image::Scale|Image::Scale/resize(_\%OPTIONS_)>. |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
=head2 jpeg_quality |
474
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
JPEG quality, as defined in |
476
|
|
|
|
|
|
|
L<Image::Scale|Image::Scale/as_jpeg(_[_$QUALITY_]_)>. |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
=head2 width |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
Use this to set and override image width. |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
=head2 height |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
Use this to set and override image height. |
485
|
|
|
|
|
|
|
|
486
|
|
|
|
|
|
|
=head2 flags |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
Use this to set and override image processing flags. |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
=head1 METHODS |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
=head2 fetch_orig |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
Call parameters: PSGI request HashRef $env, Str $basename. |
495
|
|
|
|
|
|
|
Return value: PSGI response ArrayRef $res. |
496
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
The original image is fetched from the next layer or application. All |
498
|
|
|
|
|
|
|
possible extensions defined in L</orig_ext> are tried in order, to search for |
499
|
|
|
|
|
|
|
the original image. All other responses except a straight 404 (as returned by |
500
|
|
|
|
|
|
|
L<Plack::Middleware::Static> for example) are considered matches. |
501
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
=head2 body_scaler |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
Call parameters: @args. Return value: PSGI content filter CodeRef $cb. |
505
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
Create the content filter callback and return a CodeRef to it. The filter will |
507
|
|
|
|
|
|
|
buffer the data and call L</image_scale> with parameters C<@args> when EOF is |
508
|
|
|
|
|
|
|
received, and finally return the converted data. |
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
=head2 image_scale |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
Call parameters: ScalarRef $buffer, String $ct, Int $width, Int $height, HashRef|Str $flags. |
513
|
|
|
|
|
|
|
Return value: $imagedata |
514
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
Read image from $buffer, scale it to $width x $height and |
516
|
|
|
|
|
|
|
return as content-type $ct. Optional $flags to specify image processing |
517
|
|
|
|
|
|
|
options like background fills or cropping. |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
=head1 CONFIGURATION |
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
The default match pattern for URI is |
522
|
|
|
|
|
|
|
"I<...>_I<width>xI<height>-I<flags>.I<ext>". |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
If URI doesn't match, the request is passed through. Any number of flags can |
525
|
|
|
|
|
|
|
be specified, separated with C<->. Flags can be boolean (exists or doesn't |
526
|
|
|
|
|
|
|
exist), or have a numerical value. Flag name and value are separated with a |
527
|
|
|
|
|
|
|
zero-width word to number boundary. For example C<z20> specifies flag C<z> |
528
|
|
|
|
|
|
|
with value C<20>. |
529
|
|
|
|
|
|
|
|
530
|
|
|
|
|
|
|
=head2 width |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
Width of the output image. If not defined, it can be anything |
533
|
|
|
|
|
|
|
(to preserve the image aspect ratio). |
534
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
=head2 height |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
Height of the output image. If not defined, it can be anything |
538
|
|
|
|
|
|
|
(to preserve the image aspect ratio). |
539
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
=head2 flags: fill |
541
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
Image aspect ratio is preserved by scaling the image to fit within the |
543
|
|
|
|
|
|
|
specified size. This means scaling to the smaller or the two possible sizes |
544
|
|
|
|
|
|
|
that preserve aspect ratio. Extra borders of background color are added to |
545
|
|
|
|
|
|
|
fill the requested image size exactly. |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
/images/foo_400x200-fill.png |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
If fill has a value, it specifies the background color to use. Undefined color |
550
|
|
|
|
|
|
|
with png output means transparent background. |
551
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
=head2 flags: crop |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
Image aspect ratio is preserved by scaling and cropping from middle of the |
555
|
|
|
|
|
|
|
image. This means scaling to the bigger of the two possible sizes that |
556
|
|
|
|
|
|
|
preserve the aspect ratio, and then cropping to the exact size. |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
=head2 flags: fit |
559
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
Image aspect ratio is preserved by scaling the image to the smaller of the two |
561
|
|
|
|
|
|
|
possible sizes. This means that the resulting picture may have one dimension |
562
|
|
|
|
|
|
|
smaller than specified, but cropping or filling is avoided. |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
See documentation in distribution directory C<doc> for a visual explanation. |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
=head2 flags: z |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
Zoom the original image N percent bigger. For example C<z20> to zoom 20%. |
569
|
|
|
|
|
|
|
Zooming applies only to explicitly defined width and/or height, and it does |
570
|
|
|
|
|
|
|
not change the crop size. |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
/images/foo_40x-z20.png |
573
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
=head1 CAVEATS |
575
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
The cropping requires L<Imager>. This is a run-time dependency, and |
577
|
|
|
|
|
|
|
fallback is not to crop the image to the expected size. |
578
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
=head1 SEE ALSO |
580
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
L<Image::Scale> |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
L<Imager> |
584
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
L<Plack::App::ImageMagick> |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
=head1 AUTHOR |
588
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
Panu Ervamaa <pnu@cpan.org> |
590
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
592
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
This software is copyright (c) 2011 by Panu Ervamaa. |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
596
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
597
|
|
|
|
|
|
|
|
598
|
|
|
|
|
|
|
=cut |