root/branches/release-26/lib/MT/Image.pm @ 1174

Revision 1174, 13.3 kB (checked in by bchoate, 23 months ago)

Updated copyright year for source.

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::Image;
8
9use strict;
10use MT;
11use base qw( MT::ErrorHandler );
12
13sub new {
14    my $class = shift;
15    $class .= "::" . MT->config->ImageDriver;
16    my $image = bless {}, $class;
17    $image->load_driver
18        or return $class->error( $image->errstr );
19    if (@_) {
20        $image->init(@_)
21            or return $class->error( $image->errstr );
22    }
23    $image;
24}
25
26sub get_dimensions {
27    my $image = shift;
28    my %param = @_;
29    my($w, $h) = ($image->{width}, $image->{height});
30    if (my $pct = $param{Scale}) {
31        ($w, $h) = (int($w * $pct / 100), int($h * $pct / 100));
32    } else {
33        if ($param{Width} && $param{Height}) {
34            ($w, $h) = ($param{Width}, $param{Height});
35        } else {
36            my $x = $param{Width} || $w;
37            my $y = $param{Height} || $h;
38            my $w_pct = $x / $w;
39            my $h_pct = $y / $h;
40            my $pct = $param{Width} ? $w_pct : $h_pct;
41            ($w, $h) = (int($w * $pct), int($h * $pct));
42        }
43    }
44    ($w, $h);
45}
46
47sub inscribe_square {
48    my $class = shift;
49    my %params = @_;
50    my ($w, $h) = @params{qw( Width Height )};
51
52    my ($dim, $x, $y);
53
54    if ($w > $h) {
55        $dim = $h;
56        $x = int(($w - $dim) / 2);
57        $y = 0;
58    }
59    else {
60        $dim = $w;
61        $x = 0;
62        $y = int(($h - $dim) / 2);
63    }
64
65    return ( Size => $dim, X => $x, Y => $y ); 
66}
67
68sub make_square {
69    my $image = shift;
70    my %square = $image->inscribe_square(
71        Width  => $image->{width},
72        Height => $image->{height},
73    );
74    $image->crop(%square);
75}
76
77sub check_upload {
78    my $class = shift;
79    my %params = @_;
80
81    my $fh = $params{Fh};
82
83    ## Use Image::Size to check if the uploaded file is an image, and if so,
84    ## record additional image info (width, height). We first rewind the
85    ## filehandle $fh, then pass it in to imgsize.
86    seek $fh, 0, 0;
87    eval { require Image::Size; };
88    return $class->error(
89        MT->translate(
90                "Perl module Image::Size is required to determine "
91              . "width and height of uploaded images."
92        )
93    ) if $@;
94    my ( $w, $h, $id ) = Image::Size::imgsize($fh);
95
96    my $write_file = sub {
97        $params{Fmgr}->put( $fh, $params{Local}, 'upload' );
98    };
99
100    ## Check file size?
101    my $file_size;
102    if ($params{Max}) {
103        ## Seek to the end of the handle to find the size.
104        seek $fh, 0, 2;  # wind to end
105        $file_size = tell $fh;
106        seek $fh, 0, 0;
107    }
108
109    ## If the image exceeds the dimension limit, resize it before writing.
110    if (my $max_dim = $params{MaxDim}) {
111        if (defined($w) && defined($h) && ($w > $max_dim || $h > $max_dim)) {
112            my $uploaded_data = eval { local $/; <$fh> };
113            my $img = $class->new( Data => $uploaded_data )
114                or return $class->error($class->errstr);
115
116            if ($params{Square}) {
117                (undef, $w, $h) = $img->make_square()
118                    or return $class->error($img->errstr);
119            }
120            (my($resized_data), $w, $h) = $img->scale(
121                (($w > $h) ? 'Width' : 'Height') => $max_dim )
122                    or return $class->error($img->errstr);
123
124            $write_file = sub {
125                $params{Fmgr}->put_data( $resized_data, $params{Local}, 'upload' )
126            };
127            $file_size = length $resized_data;
128        }
129    }
130
131    if (my $max_size = $params{Max}) {
132        if ($max_size < $file_size) {
133            return $class->error(MT->translate( "File size exceeds maximum allowed: [_1] > [_2]",
134                    $file_size, $max_size ) );
135        }
136    }
137
138    ($w, $h, $id, $write_file);
139}
140
141package MT::Image::ImageMagick;
142@MT::Image::ImageMagick::ISA = qw( MT::Image );
143
144sub load_driver {
145    my $image = shift;
146    eval { require Image::Magick };
147    if (my $err = $@) {
148        return $image->error(MT->translate("Can't load Image::Magick: [_1]", $err));
149    }
150    1;
151}
152
153sub init {
154    my $image = shift;
155    my %param = @_;
156    my %arg = ();
157    if (my $type = $param{Type}) {
158        %arg = (magick => lc($type));
159    } elsif (my $file = $param{Filename}) {
160        (my $ext = $file) =~ s/.*\.//;
161        %arg = (magick => lc($ext));
162    }
163    my $magick = $image->{magick} = Image::Magick->new(%arg);
164    if (my $file = $param{Filename}) {
165        my $x = $magick->Read($file);
166        return $image->error(MT->translate(
167            "Reading file '[_1]' failed: [_2]", $file, $x)) if $x;
168        ($image->{width}, $image->{height}) = $magick->Get('width', 'height');
169    } elsif ($param{Data}) {
170        my $x = $magick->BlobToImage($param{Data});
171        return $image->error(MT->translate(
172            "Reading image failed: [_1]", $x)) if $x;
173        ($image->{width}, $image->{height}) = $magick->Get('width', 'height');
174    }
175    $image;
176}
177
178sub scale {
179    my $image = shift;
180    my($w, $h) = $image->get_dimensions(@_);
181    my $magick = $image->{magick};
182    my $err = $magick->can('Resize') ?
183              $magick->Resize(width => $w, height => $h) :
184              $magick->Scale(width => $w, height => $h);
185    return $image->error(MT->translate(
186        "Scaling to [_1]x[_2] failed: [_3]", $w, $h, $err)) if $err;
187    $magick->Profile("*") if $magick->can('Profile');
188    ($image->{width}, $image->{height}) = ($w, $h);
189    wantarray ? ($magick->ImageToBlob, $w, $h) : $magick->ImageToBlob;
190}
191
192sub crop {
193    my $image = shift;
194    my %param = @_;
195    my ($size, $x, $y) = @param{qw( Size X Y )};
196    my $magick = $image->{magick};
197
198    my $err = $magick->Crop(width => $size, height => $size, x => $x, y => $y);
199    return $image->error(MT->translate(
200        "Cropping a [_1]x[_1] square at [_2],[_3] failed: [_4]", $size, $x,
201        $y, $err)) if $err;
202
203    ## Remove page offsets from the original image, per this thread:
204    ## http://studio.imagemagick.org/pipermail/magick-users/2003-September/010803.html
205    $magick->Set( page => '+0+0' );
206
207    ($image->{width}, $image->{height}) = ($size, $size);
208    wantarray ? ($magick->ImageToBlob, $size, $size) : $magick->ImageToBlob;
209}
210
211sub convert {
212    my $image = shift;
213    my %param = @_;
214    my $type = $param{Type};
215
216    my $magick = $image->{magick};
217    my $err = $magick->Set( magick => uc $type );
218    return $image->error(MT->translate(
219            "Converting image to [_1] failed: [_2]", $type, $err)) if $err;
220
221    $magick->ImageToBlob;
222}
223
224package MT::Image::NetPBM;
225@MT::Image::NetPBM::ISA = qw( MT::Image );
226
227sub load_driver {
228    my $image = shift;
229    eval { require IPC::Run };
230    if (my $err = $@) {
231        return $image->error(MT->translate("Can't load IPC::Run: [_1]", $err));
232    }
233    my $pbm = $image->_find_pbm or return;
234    1;
235}
236
237sub init {
238    my $image = shift;
239    my %param = @_;
240    if (my $file = $param{Filename}) {
241        $image->{file} = $file;
242        if (!defined $param{Type}) {
243            (my $ext = $file) =~ s/.*\.//;
244            $param{Type} = uc $ext;
245        }
246    } elsif (my $blob = $param{Data}) {
247        $image->{data} = $blob;
248    }
249    my %Types = (jpg => 'jpeg', gif => 'gif', 'png' => 'png');
250    my $type = $image->{type} = $Types{ lc $param{Type} };
251    my($out, $err);
252    my $pbm = $image->_find_pbm or return;
253    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
254    my @out = ("${pbm}pnmfile", '-allimages');
255    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
256        \@out, \$out, \$err)
257        or return $image->error(MT->translate(
258            "Reading image failed: [_1]", $err));
259    ($image->{width}, $image->{height}) = $out =~ /(\d+)\s+by\s+(\d+)/;
260    $image;
261}
262
263sub scale {
264    my $image = shift;
265    my($w, $h) = $image->get_dimensions(@_);
266    my $type = $image->{type};
267    my($out, $err);
268    my $pbm = $image->_find_pbm or return;
269    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
270    my @scale = ("${pbm}pnmscale", '-width', $w, '-height', $h);
271    my @out;
272    for my $try (qw( ppm pnm )) {
273        my $prog = "${pbm}${try}to$type";
274        @out = ($prog), last if -x $prog;
275    }
276    my(@quant);
277    if ($type eq 'gif') {
278        push @quant, ([ "${pbm}ppmquant", 256 ], '|');
279    }
280    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
281        \@scale, '|',
282        @quant,
283        \@out, \$out, \$err)
284        or return $image->error(MT->translate(
285            "Scaling to [_1]x[_2] failed: [_3]", $w, $h, $err));
286    ($image->{width}, $image->{height}) = ($w, $h);
287    wantarray ? ($out, $w, $h) : $out;
288}
289
290sub crop {
291    my $image = shift;
292    my %param = @_;
293    my ($size, $x, $y) = @param{qw( Size X Y )};
294   
295    my($w, $h) = $image->get_dimensions(@_);
296    my $type = $image->{type};
297    my($out, $err);
298    my $pbm = $image->_find_pbm or return;
299    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
300
301    my @crop = ("${pbm}pnmcut", $x, $y, $size, $size);
302    my @out;
303    for my $try (qw( ppm pnm )) {
304        my $prog = "${pbm}${try}to$type";
305        @out = ($prog), last if -x $prog;
306    }
307    my(@quant);
308    if ($type eq 'gif') {
309        push @quant, ([ "${pbm}ppmquant", 256 ], '|');
310    }
311    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
312        \@crop, '|',
313        @quant,
314        \@out, \$out, \$err)
315        or return $image->error(MT->translate(
316            "Cropping to [_1]x[_1] failed: [_2]", $size, $err));
317    ($image->{width}, $image->{height}) = ($w, $h);
318    wantarray ? ($out, $w, $h) : $out;
319}
320
321sub convert {
322    my $image = shift;
323    my %param = @_;
324
325    my $type = $image->{type};
326    my $outtype = lc $param{Type};
327
328    my($out, $err);
329    my $pbm = $image->_find_pbm or return;
330    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
331
332    my @out;
333    for my $try (qw( ppm pnm )) {
334        my $prog = "${pbm}${try}to$outtype";
335        @out = ($prog), last if -x $prog;
336    }
337    my(@quant);
338    if ($type eq 'gif') {
339        push @quant, ([ "${pbm}ppmquant", 256 ], '|');
340    }
341    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
342        @quant,
343        \@out, \$out, \$err)
344        or return $image->error(MT->translate(
345            "Converting to [_1] failed: [_2]", $type, $err));
346    $out;
347}
348
349sub _find_pbm {
350    my $image = shift;
351    return $image->{__pbm_path} if $image->{__pbm_path};
352    my @NetPBM = qw( /usr/local/netpbm/bin /usr/local/bin /usr/bin );
353    my $pbm;
354    for my $path (MT->config->NetPBMPath, @NetPBM) {
355        next unless $path;
356        $path .= '/' unless $path =~ m!/$!;
357        $pbm = $path, last if -x "${path}pnmscale";
358    }
359    return $image->error(MT->translate(
360        "You do not have a valid path to the NetPBM tools on your machine."))
361        unless $pbm;
362    $image->{__pbm_path} = $pbm;
363}
364
3651;
366__END__
367
368=head1 NAME
369
370MT::Image - Movable Type image manipulation routines
371
372=head1 SYNOPSIS
373
374    use MT::Image;
375    my $img = MT::Image->new( Filename => '/path/to/image.jpg' );
376    my($blob, $w, $h) = $img->scale( Width => 100 );
377
378    open FH, ">thumb.jpg" or die $!;
379    binmode FH;
380    print FH $blob;
381    close FH;
382
383=head1 DESCRIPTION
384
385I<MT::Image> contains image manipulation routines using either the
386I<NetPBM> tools or the I<ImageMagick> and I<Image::Magick> Perl module.
387The backend framework used (NetPBM or ImageMagick) depends on the value of
388the I<ImageDriver> setting in the F<mt.cfg> file (or, correspondingly, set
389on an instance of the I<MT::ConfigMgr> class).
390
391Currently all this is used for is to create thumbnails from uploaded images.
392
393=head1 USAGE
394
395=head2 MT::Image->new(%arg)
396
397Constructs a new I<MT::Image> object. Returns the new object on success; on
398error, returns C<undef>, and the error message is in C<MT::Image-E<gt>errstr>.
399
400I<%arg> can contain:
401
402=over 4
403
404=item * Filename
405
406The path to an image to load.
407
408=item * Data
409
410The actual contents of an image, already loaded from a file, a database,
411etc.
412
413=item * Type
414
415The image format of the data in I<Data>. This should be either I<JPG> or
416I<GIF>.
417
418=back
419
420=head2 $img->scale(%arg)
421
422Creates a thumbnail from the image represented by I<$img>; on success, returns
423a list containing the binary contents of the thumbnail image, the width of the
424scaled image, and the height of the scaled image. On error, returns C<undef>,
425and the error message is in C<$img-E<gt>errstr>.
426
427I<%arg> can contain:
428
429=over 4
430
431=item * Width
432
433=item * Height
434
435The width and height of the final image, respectively. If you provide only one
436of these arguments, the other dimension will be scaled appropriately. If you
437provide neither, the image will be scaled to C<100%> of the original (that is,
438the same size). If you provide both, the image will likely look rather
439distorted.
440
441=item * Scale
442
443To be used instead of I<Width> and I<Height>; the value should be a percentage
444(ie C<100> to return the original image without resizing) by which both the
445width and height will be scaled equally.
446
447=back
448
449=head2 $img->get_dimensions(%arg)
450
451This utility method returns a width and height value pair after applying
452the given arguments. Valid arguments are the same as the L<scale> method.
453If 'Width' is given, a proportionate height will be calculated. If a
454'Height' is given, the width will be calculated. If 'Scale' is given
455the height and width will be calculated based on that scale (a value
456between 1 to 100).
457
458=head1 AUTHOR & COPYRIGHT
459
460Please see the I<MT> manpage for author, copyright, and license information.
461
462=cut
Note: See TracBrowser for help on using the browser.