File Coverage

blib/lib/GD/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 GD::Thumbnail;
2 1     1   31182 use strict;
  1         2  
  1         52  
3 1     1   7 use warnings;
  1         3  
  1         41  
4 1     1   6 use vars qw($VERSION %TMP);
  1         6  
  1         82  
5              
6             $VERSION = '1.41';
7              
8 1     1   13529 use GD;
  0            
  0            
9             use Carp qw( croak );
10              
11             use constant GIF_OK => $GD::VERSION >= 2.15 || $GD::VERSION <= 1.19;
12             use constant DEFAULT_MIME => 'png';
13             use constant BUFFER => 2; # y-buffer for info strips in pixels
14             use constant BLACK => [ 0, 0, 0 ];
15             use constant WHITE => [ 255, 255, 255 ];
16             use constant IMG_X => 0;
17             use constant IMG_Y => 1;
18             use constant ALL_MIME => qw(gif png jpeg gd gd2 wbmp);
19              
20             use constant KILOBYTE => 1024;
21             use constant MEGABYTE => 1024 * KILOBYTE;
22             use constant GIGABYTE => 1024 * MEGABYTE;
23              
24             use constant DEFAULT_MAX_PIXELS => 50;
25             use constant PATH_LENGTH => 255;
26             use constant MAX_JPEG_QUALITY => 100;
27             use constant MAX_PNG_COMPRESSION => 9;
28             use constant STAT_SIZE => 7;
29             use constant RATIO_CONSTANT => 100;
30             use constant RE_FILE_EXTENSION => qr{ [.] (png|gif|jpg|jpe|jpeg) \z }xmsi;
31             use constant RE_RATIO => qr{ (\d+)(?:\s+|)% }xms;
32              
33             %TMP = ( # global template. so that one can change the text
34             GB => '%.2f GB',
35             MB => '%.2f MB',
36             KB => '%.2f KB',
37             BY => '%s bytes',
38             TEXT => 'x ',
39             );
40              
41             my %KNOWN = map { ($_, $_) } ALL_MIME;
42             $KNOWN{'jpg'} = 'jpeg';
43             $KNOWN{'jpe'} = 'jpeg';
44              
45             my %IS_GD_FONT = map { ( lc($_), $_ ) } qw(Small Large MediumBold Tiny Giant);
46              
47             GD::Image->trueColor(1) if GD::Image->can('trueColor');
48              
49             sub new {
50             my($class, @args)= @_;
51             my %o = @args % 2 ? () : @args;
52             my $self = {
53             DIMENSION => [ 0, 0 ], # Thumbnail dimension
54             DIMENSION_CONSTRAINT => 0, # don't exceed w/h?
55             FRAME_COLOR => BLACK,
56             FRAME => 0, # bool: add frame?
57             FORCE_MIME => q{}, # force output type?
58             GD_FONT => 'Tiny', # info text color
59             INFO_COLOR => WHITE,
60             MIME => q{},
61             OVERLAY => 0, # bool: overlay info strips?
62             STRIP_COLOR => BLACK,
63             SQUARE => 0, # bool: make square thumb?
64             };
65              
66             $self->{FRAME} = $o{frame} ? 1 : 0;
67             $self->{SQUARE} = $o{square} ? $o{square} : 0;
68             $self->{OVERLAY} = ($o{overlay} || $self->{SQUARE}) ? 1 : 0;
69              
70             for my $name ( qw( FORCE_MIME DIMENSION_CONSTRAINT ) ) {
71             $self->{ $name } = $o{ lc $name } if defined $o{ lc $name };
72             }
73              
74             if ( $o{font} and my $font = $IS_GD_FONT{ lc $o{font} } ) {
75             $self->{GD_FONT} = $font;
76             }
77              
78             for my $id ( qw( STRIP_COLOR INFO_COLOR FRAME_COLOR ) ) {
79             if (my $color = $o{ lc $id }) {
80             if ( ref $color && ref $color eq 'ARRAY' && $#{$color} == 2 ) {
81             $self->{$id} = $color;
82             }
83             }
84             }
85              
86             bless $self, $class;
87             return $self;
88             }
89              
90             sub _check_type {
91             my($self, $image) = @_;
92             my $type;
93             if ( length $image <= PATH_LENGTH && $image =~ RE_FILE_EXTENSION ) {
94             $type = $KNOWN{lc $1};
95             if ( $type eq 'gif' && !GIF_OK ) {
96             # code will probably die at $gd assignment below
97             warn "GIF format is not supported by this version ($GD::VERSION) of GD\n";
98             $type = DEFAULT_MIME;
99             }
100             }
101              
102             $type = DEFAULT_MIME if ! $type;
103             return $type;
104             }
105              
106             sub _check_ratio {
107             my($self, $max, $w, $h) = @_;
108             my $ratio;
109             if ( $max =~ RE_RATIO ) {
110             $ratio = $1;
111             }
112             else {
113             my $n = $self->{DIMENSION_CONSTRAINT}
114             ? $w > $h ? $w : $h
115             : $w
116             ;
117             $ratio = sprintf '%.1f', $max * RATIO_CONSTANT / $n;
118             }
119             croak 'Can not determine thumbnail ratio' if ! $ratio;
120             return $ratio;
121             }
122              
123             sub _get_iy {
124             my($self, $info, $info2, $o, $y, $yy) = @_;
125             return 0 if ! $info;
126             return $o ? $y - $yy
127             : $info2 ? $y + $yy + BUFFER/2
128             : $y + BUFFER/2
129             ;
130             }
131              
132             sub _strips {
133             my($self, $info, $info2, $o, $x, $y, $yy) = @_;
134             my $iy = $self->_get_iy( $info, $info2, $o, $y, $yy );
135             my @strips;
136             push @strips, [ $info , 0, $iy, 0, 0, $x, $y , RATIO_CONSTANT ] if $info;
137             push @strips, [ $info2, 0, 0, 0, 0, $x, $yy, RATIO_CONSTANT ] if $info2;
138             return @strips;
139             }
140              
141             sub _alter_for_crop {
142             my($self, $xsmall, $x_ref, $y_ref, $dx_ref, $dy_ref) = @_;
143             if ( $xsmall ) {
144             my $diff = (${$y_ref} - ${$x_ref}) / ${$x_ref};
145             ${$x_ref} += ${$x_ref} * $diff;
146             ${$y_ref} += ${$y_ref} * $diff;
147             ${$dy_ref} = -${$dx_ref} * (2 - ${$x_ref} / ${$y_ref})**2;
148             ${$dx_ref} = 0;
149             }
150             else {
151             my $diff = (${$x_ref} - ${$y_ref}) / ${$y_ref};
152             ${$x_ref} += ${$x_ref} * $diff;
153             ${$y_ref} += ${$y_ref} * $diff;
154             ${$dx_ref} = -${$dy_ref} * ( 2 - ${$y_ref}/${$x_ref} )**2;
155             ${$dy_ref} = 0;
156             }
157             return;
158             }
159              
160             sub _setup_parameters {
161             my($self, $opt, $x_ref, $y_ref, $dx_ref, $dy_ref, $ty_ref ) = @_;
162             if ( $opt->{square} ) {
163             my $rx = $opt->{width} < $opt->{height} ? $opt->{width}/$opt->{height} : 1;
164             my $ry = $opt->{width} < $opt->{height} ? 1 : $opt->{height}/$opt->{width};
165             my $d;
166             if ( $opt->{xsmall} ) {
167             $d = ${$x_ref} * $rx;
168             ${$dx_ref} = (${$x_ref} - $d) / 2;
169             ${$x_ref} = $d;
170             }
171             else {
172             $d = ${$y_ref} * $ry;
173             ${$dy_ref} = (${$y_ref} - $d) / 2;
174             ${$y_ref} = $d;
175             }
176             }
177              
178             if ( ! $opt->{square} || ( $opt->{square} && $opt->{xsmall} ) ) {
179             # does not work if square & y_is_small,
180             # since we may have info bars which eat y space
181             ${$ty_ref} = 0; # TODO. test this more and remove from below
182             ${$y_ref} = ${$y_ref} - ${$ty_ref} - BUFFER/2 if $opt->{overlay};
183             }
184             return;
185             }
186              
187             sub create {
188             my $self = shift;
189             my $image = shift || croak 'Image parameter is missing';
190             my $max = shift || DEFAULT_MAX_PIXELS;
191             my $info = shift || 0;
192              
193             my $info2 = $info && $info == 2;
194             my $type = $self->_check_type( $image );
195             my $o = $self->{OVERLAY};
196             my $size = $info2 ? $self->_image_size( $image ) : 0;
197             my $gd = GD::Image->new($image) or croak "GD::Image->new error: $!";
198             my($w, $h) = $gd->getBounds or croak "getBounds() failed: $!";
199             my $ratio = $self->_check_ratio($max, $w, $h);
200             my $square = $self->{SQUARE} || 0;
201             my $crop = $square && lc $square eq 'crop';
202              
203             my $x = sprintf '%.0f', $w * $ratio / RATIO_CONSTANT;
204             my $def_y = sprintf '%.0f', $h * $ratio / RATIO_CONSTANT;
205             my $y = $square ? $x : $def_y;
206             my $yy = 0; # yy & yy2 has the same value
207             my $yy2 = 0;
208              
209             ($info , $yy ) = $self->_strip($self->_text($w,$h,$type), $x) if $info;
210             ($info2, $yy2) = $self->_strip($self->_size($size) , $x) if $info2;
211              
212             my $ty = $yy + $yy2;
213             my $new_y = $o ? $y : $y + $ty;
214             my $thumb = GD::Image->new( $x, $new_y );
215              
216             # RT#49353 | Alexander Vonk: prefill Thumbnail with strip color, as promised
217             $thumb->fill( 0, 0, $thumb->colorAllocate( @{ $self->{STRIP_COLOR} } ) );
218              
219             $thumb->colorAllocate(@{ +WHITE }) if ! $info;
220              
221             my @strips = $self->_strips( $info, $info2, $o, $x, $y, $yy );
222             my $dx = 0;
223             my $dy = $yy2 || 0;
224             my $xsmall = $x < $def_y;
225              
226             $self->_setup_parameters(
227             {
228             xsmall => $xsmall,
229             square => $square,
230             width => $w,
231             height => $h,
232             overlay => $o,
233             },
234             \$x, \$y, \$dx, \$dy, \$ty
235             );
236              
237             $self->_alter_for_crop( $xsmall, \$x, \$y, \$dx, \$dy ) if $crop;
238              
239             my $resize = $thumb->can('copyResampled') ? 'copyResampled' : 'copyResized';
240              
241             $thumb->$resize($gd, $dx, $dy, 0, 0, $x, $y, $w, $h);
242             $thumb->copyMerge( @{$_} ) for @strips;
243              
244             return $self->_finish( $thumb, $type );
245             }
246              
247             sub _finish {
248             my($self, $thumb, $type) = @_;
249             my @dim = $thumb->getBounds;
250              
251             $self->{DIMENSION}[IMG_X] = $dim[IMG_X];
252             $self->{DIMENSION}[IMG_Y] = $dim[IMG_Y];
253              
254             if ($self->{FRAME}) {
255             my $color = $thumb->colorAllocate(@{ $self->{FRAME_COLOR} });
256             $thumb->rectangle( 0, 0, $dim[IMG_X] - 1, $dim[IMG_Y] - 1, $color );
257             }
258              
259             my $mime = $self->_force_mime($thumb);
260             $type = $mime if $mime;
261             $self->{MIME} = $type;
262             my @iopt;
263             push @iopt, MAX_JPEG_QUALITY if $type eq 'jpeg';
264             push @iopt, MAX_PNG_COMPRESSION if $type eq 'png';
265             return $thumb->$type( @iopt );
266             }
267              
268             sub width { return shift->{DIMENSION}[IMG_X] }
269             sub height { return shift->{DIMENSION}[IMG_Y] }
270             sub mime { return shift->{MIME} }
271              
272             sub _force_mime {
273             my $self = shift;
274             my $gd = shift || return;
275             return if ! $self->{FORCE_MIME};
276             my %mime = map { ( $_, $_ ) } ALL_MIME;
277             my $type = $mime{ lc $self->{FORCE_MIME} } || return;
278             return unless $gd->can($type);
279             return $type;
280             }
281              
282             sub _text {
283             my($self, $w, $h, $type) = @_;
284             $type = uc $type;
285             my $tmp = $TMP{TEXT} || croak 'TEXT template is not set';
286             $tmp =~ s{}{$w}xmsg;
287             $tmp =~ s{}{$h}xmsg;
288             $tmp =~ s{}{$type}xmsg;
289             return $tmp;
290             }
291              
292             sub _image_size {
293             my $self = shift;
294             my $image = shift;
295             my $img_size = 0;
296             # don't do that at home. very dangerous :p
297             my $is_image = GD::Image->can('_image_type')
298             && GD::Image::_image_type($image); ## no critic (ProtectPrivateSubs)
299             if ( $is_image ) { # raw data
300             use bytes;
301             $img_size = length $image;
302             }
303             elsif ( defined fileno $image ) { # filehandle
304             binmode $image;
305             use bytes;
306             local $/;
307             $img_size = length <$image>;
308             }
309             else { # file
310             $img_size = (stat $image)[STAT_SIZE] if -e $image && !-d _;
311             }
312             return $img_size;
313             }
314              
315             sub _strip {
316             my $self = shift;
317             my $string = shift;
318             my $x = shift;
319             my $type = $self->{GD_FONT};
320             my $font = GD::Font->$type();
321             my $sw = $font->width * length $string;
322             my $sh = $font->height;
323             warn "Thumbnail width ($x) is too small for an info text\n" if $x < $sw;
324             my $info = GD::Image->new($x, $sh+BUFFER);
325             my $color = $info->colorAllocate(@{ $self->{STRIP_COLOR} });
326             $info->filledRectangle(0,0,$x,$sh+BUFFER,$color);
327             $info->string($font, ($x - $sw)/2, 0, $string, $info->colorAllocate(@{ $self->{INFO_COLOR} }));
328             return $info, $sh + BUFFER;
329             }
330              
331             sub _size {
332             my $self = shift;
333             my $size = shift || return '0 bytes';
334             return sprintf $TMP{GB}, $size / GIGABYTE if $size >= GIGABYTE;
335             return sprintf $TMP{MB}, $size / MEGABYTE if $size >= MEGABYTE;
336             return sprintf $TMP{KB}, $size / KILOBYTE if $size >= KILOBYTE;
337             return sprintf $TMP{BY}, $size;
338             }
339              
340             1;
341              
342             __END__