root/branches/release-36/lib/MT/Image.pm @ 2085

Revision 2085, 13.4 kB (checked in by bchoate, 19 months ago)

Support for 'jpeg' image file extensions by MT::Image::NetPBM. Thanks, Andrey. BugId:79466

  • 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', jpeg => 'jpeg', gif => 'gif', 'png' => 'png');
250    my $type = $image->{type} = $Types{ lc $param{Type} };
251    if (!$type) {
252        return $image->error(MT->translate("Unsupported image file type: [_1]", $type));
253    }
254    my($out, $err);
255    my $pbm = $image->_find_pbm or return;
256    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
257    my @out = ("${pbm}pnmfile", '-allimages');
258    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
259        \@out, \$out, \$err)
260        or return $image->error(MT->translate(
261            "Reading image failed: [_1]", $err));
262    ($image->{width}, $image->{height}) = $out =~ /(\d+)\s+by\s+(\d+)/;
263    $image;
264}
265
266sub scale {
267    my $image = shift;
268    my($w, $h) = $image->get_dimensions(@_);
269    my $type = $image->{type};
270    my($out, $err);
271    my $pbm = $image->_find_pbm or return;
272    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
273    my @scale = ("${pbm}pnmscale", '-width', $w, '-height', $h);
274    my @out;
275    for my $try (qw( ppm pnm )) {
276        my $prog = "${pbm}${try}to$type";
277        @out = ($prog), last if -x $prog;
278    }
279    my(@quant);
280    if ($type eq 'gif') {
281        push @quant, ([ "${pbm}ppmquant", 256 ], '|');
282    }
283    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
284        \@scale, '|',
285        @quant,
286        \@out, \$out, \$err)
287        or return $image->error(MT->translate(
288            "Scaling to [_1]x[_2] failed: [_3]", $w, $h, $err));
289    ($image->{width}, $image->{height}) = ($w, $h);
290    wantarray ? ($out, $w, $h) : $out;
291}
292
293sub crop {
294    my $image = shift;
295    my %param = @_;
296    my ($size, $x, $y) = @param{qw( Size X Y )};
297   
298    my($w, $h) = $image->get_dimensions(@_);
299    my $type = $image->{type};
300    my($out, $err);
301    my $pbm = $image->_find_pbm or return;
302    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
303
304    my @crop = ("${pbm}pnmcut", $x, $y, $size, $size);
305    my @out;
306    for my $try (qw( ppm pnm )) {
307        my $prog = "${pbm}${try}to$type";
308        @out = ($prog), last if -x $prog;
309    }
310    my(@quant);
311    if ($type eq 'gif') {
312        push @quant, ([ "${pbm}ppmquant", 256 ], '|');
313    }
314    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
315        \@crop, '|',
316        @quant,
317        \@out, \$out, \$err)
318        or return $image->error(MT->translate(
319            "Cropping to [_1]x[_1] failed: [_2]", $size, $err));
320    ($image->{width}, $image->{height}) = ($w, $h);
321    wantarray ? ($out, $w, $h) : $out;
322}
323
324sub convert {
325    my $image = shift;
326    my %param = @_;
327
328    my $type = $image->{type};
329    my $outtype = lc $param{Type};
330
331    my($out, $err);
332    my $pbm = $image->_find_pbm or return;
333    my @in = ("$pbm${type}topnm", ($image->{file} ? $image->{file} : ()));
334
335    my @out;
336    for my $try (qw( ppm pnm )) {
337        my $prog = "${pbm}${try}to$outtype";
338        @out = ($prog), last if -x $prog;
339    }
340    my(@quant);
341    if ($type eq 'gif') {
342        push @quant, ([ "${pbm}ppmquant", 256 ], '|');
343    }
344    IPC::Run::run(\@in, '<', ($image->{file} ? \undef : \$image->{data}), '|',
345        @quant,
346        \@out, \$out, \$err)
347        or return $image->error(MT->translate(
348            "Converting to [_1] failed: [_2]", $type, $err));
349    $out;
350}
351
352sub _find_pbm {
353    my $image = shift;
354    return $image->{__pbm_path} if $image->{__pbm_path};
355    my @NetPBM = qw( /usr/local/netpbm/bin /usr/local/bin /usr/bin );
356    my $pbm;
357    for my $path (MT->config->NetPBMPath, @NetPBM) {
358        next unless $path;
359        $path .= '/' unless $path =~ m!/$!;
360        $pbm = $path, last if -x "${path}pnmscale";
361    }
362    return $image->error(MT->translate(
363        "You do not have a valid path to the NetPBM tools on your machine."))
364        unless $pbm;
365    $image->{__pbm_path} = $pbm;
366}
367
3681;
369__END__
370
371=head1 NAME
372
373MT::Image - Movable Type image manipulation routines
374
375=head1 SYNOPSIS
376
377    use MT::Image;
378    my $img = MT::Image->new( Filename => '/path/to/image.jpg' );
379    my($blob, $w, $h) = $img->scale( Width => 100 );
380
381    open FH, ">thumb.jpg" or die $!;
382    binmode FH;
383    print FH $blob;
384    close FH;
385
386=head1 DESCRIPTION
387
388I<MT::Image> contains image manipulation routines using either the
389I<NetPBM> tools or the I<ImageMagick> and I<Image::Magick> Perl module.
390The backend framework used (NetPBM or ImageMagick) depends on the value of
391the I<ImageDriver> setting in the F<mt.cfg> file (or, correspondingly, set
392on an instance of the I<MT::ConfigMgr> class).
393
394Currently all this is used for is to create thumbnails from uploaded images.
395
396=head1 USAGE
397
398=head2 MT::Image->new(%arg)
399
400Constructs a new I<MT::Image> object. Returns the new object on success; on
401error, returns C<undef>, and the error message is in C<MT::Image-E<gt>errstr>.
402
403I<%arg> can contain:
404
405=over 4
406
407=item * Filename
408
409The path to an image to load.
410
411=item * Data
412
413The actual contents of an image, already loaded from a file, a database,
414etc.
415
416=item * Type
417
418The image format of the data in I<Data>. This should be either I<JPG> or
419I<GIF>.
420
421=back
422
423=head2 $img->scale(%arg)
424
425Creates a thumbnail from the image represented by I<$img>; on success, returns
426a list containing the binary contents of the thumbnail image, the width of the
427scaled image, and the height of the scaled image. On error, returns C<undef>,
428and the error message is in C<$img-E<gt>errstr>.
429
430I<%arg> can contain:
431
432=over 4
433
434=item * Width
435
436=item * Height
437
438The width and height of the final image, respectively. If you provide only one
439of these arguments, the other dimension will be scaled appropriately. If you
440provide neither, the image will be scaled to C<100%> of the original (that is,
441the same size). If you provide both, the image will likely look rather
442distorted.
443
444=item * Scale
445
446To be used instead of I<Width> and I<Height>; the value should be a percentage
447(ie C<100> to return the original image without resizing) by which both the
448width and height will be scaled equally.
449
450=back
451
452=head2 $img->get_dimensions(%arg)
453
454This utility method returns a width and height value pair after applying
455the given arguments. Valid arguments are the same as the L<scale> method.
456If 'Width' is given, a proportionate height will be calculated. If a
457'Height' is given, the width will be calculated. If 'Scale' is given
458the height and width will be calculated based on that scale (a value
459between 1 to 100).
460
461=head1 AUTHOR & COPYRIGHT
462
463Please see the I<MT> manpage for author, copyright, and license information.
464
465=cut
Note: See TracBrowser for help on using the browser.