root/branches/release-41/lib/MT/Image.pm @ 2747

Revision 2747, 17.6 kB (checked in by bchoate, 17 months ago)

Updated POD.

  • 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
368package MT::Image::GD;
369@MT::Image::GD::ISA = qw( MT::Image );
370
371sub load_driver {
372    my $image = shift;
373    eval { require GD };
374    if (my $err = $@) {
375        return $image->error(MT->translate("Can't load GD: [_1]", $err));
376    }
377    1;
378}
379
380sub init {
381    my $image = shift;
382    my %param = @_;
383
384    if ((!defined $param{Type}) && (my $file = $param{Filename})) {
385    (my $ext = $file) =~ s/.*\.//;
386    $param{Type} = lc $ext;
387    }
388    my %Types = (jpg => 'jpeg', jpeg => 'jpeg', gif => 'gif', 'png' => 'png');
389    $image->{type} = $Types{ lc $param{Type} }
390        or return $image->error(MT->translate("Unsupported image file type: [_1]", $param{Type}));
391
392    if (my $file = $param{Filename}) {
393    $image->{gd} = GD::Image->new($file)
394        or return $image->error(MT->translate("Reading file '[_1]' failed: [_2]", $file, $@));
395    } elsif (my $blob = $param{Data}) {
396    $image->{gd} = GD::Image->new($blob)
397        or return $image->error(MT->translate("Reading image failed: [_1]", $@));
398    }
399    ($image->{width}, $image->{height}) = $image->{gd}->getBounds();
400    $image;
401}
402
403sub blob {
404    my $image = shift;
405    my $type = $image->{type};
406    $image->{gd}->$type;
407}
408
409sub scale {
410    my $image = shift;
411    my($w, $h) = $image->get_dimensions(@_);
412    my $src = $image->{gd};
413    my $gd = GD::Image->new($w, $h);
414    $gd->copyResampled($src, 0, 0, 0, 0, $w, $h, $image->{width}, $image->{height});
415    ($image->{gd}, $image->{width}, $image->{height}) = ($gd, $w, $h);
416    wantarray ? ($image->blob, $w, $h) : $image->blob;
417}
418
419sub crop {
420    my $image = shift;
421    my %param = @_;
422    my ($size, $x, $y) = @param{qw( Size X Y )};
423    my $src = $image->{gd};
424    my $gd = GD::Image->new($size, $size);
425    $gd->copy($src, 0, 0, $x, $y, $size, $size);
426    ($image->{gd}, $image->{width}, $image->{height}) = ($gd, $size, $size);
427    wantarray ? ($image->blob, $size, $size) : $image->blob;
428}
429
430sub convert {
431    my $image = shift;
432    my %param = @_;
433    $image->{type} = lc $param{Type};
434    $image->blob;
435}
436
4371;
438__END__
439
440=head1 NAME
441
442MT::Image - Movable Type image manipulation routines
443
444=head1 SYNOPSIS
445
446    use MT::Image;
447    my $img = MT::Image->new( Filename => '/path/to/image.jpg' );
448    my($blob, $w, $h) = $img->scale( Width => 100 );
449
450    open FH, ">thumb.jpg" or die $!;
451    binmode FH;
452    print FH $blob;
453    close FH;
454
455=head1 DESCRIPTION
456
457I<MT::Image> contains image manipulation routines using either the
458I<NetPBM> tools, the I<ImageMagick> and I<Image::Magick> Perl module,
459or the I<GD> and I<GD> Perl module.
460The backend framework used (NetPBM, ImageMagick, GD) depends on the value of
461the I<ImageDriver> setting in the F<mt.cfg> file (or, correspondingly, set
462on an instance of the I<MT::ConfigMgr> class).
463
464Currently all this is used for is to create thumbnails from uploaded images.
465
466=head1 USAGE
467
468=head2 MT::Image->new( %arg )
469
470Constructs a new I<MT::Image> object. Returns the new object on success; on
471error, returns C<undef>, and the error message is in C<MT::Image-E<gt>errstr>.
472
473I<%arg> can contain:
474
475=over 4
476
477=item * Filename
478
479The path to an image to load.
480
481=item * Data
482
483The actual contents of an image, already loaded from a file, a database,
484etc.
485
486=item * Type
487
488The image format of the data in I<Data>. This should be either I<JPG> or
489I<GIF>.
490
491=back
492
493=head2 $img->scale( %arg )
494
495Creates a thumbnail from the image represented by I<$img>; on success, returns
496a list containing the binary contents of the thumbnail image, the width of the
497scaled image, and the height of the scaled image. On error, returns C<undef>,
498and the error message is in C<$img-E<gt>errstr>.
499
500I<%arg> can contain:
501
502=over 4
503
504=item * Width
505
506=item * Height
507
508The width and height of the final image, respectively. If you provide only one
509of these arguments, the other dimension will be scaled appropriately. If you
510provide neither, the image will be scaled to C<100%> of the original (that is,
511the same size). If you provide both, the image will likely look rather
512distorted.
513
514=item * Scale
515
516To be used instead of I<Width> and I<Height>; the value should be a percentage
517(ie C<100> to return the original image without resizing) by which both the
518width and height will be scaled equally.
519
520=back
521
522=head2 MT::Image->inscribe_square( %arg )
523
524Calculates a square of dimensions that are capable of holding an image
525of the height and width indicated. This method receives I<%arg>, which
526may contain:
527
528=over 4
529
530=item * Height
531
532=item * Width
533
534=back
535
536The square will be the smaller value of the Height and Width parameter.
537
538The method returns a hash containing the following information:
539
540=over 4
541
542=item * Size
543
544The size of the calculated square, in pixels.
545
546=item * X
547
548The horizontal space to crop from the image, in pixels.
549
550=item * Y
551
552The vertical space to crop from the image, in pixels.
553
554=back
555
556This information is suited for the L<crop> method.
557
558=head2 $img->make_square()
559
560Takes an image which may or may not be a square in dimension and forces
561it into a square shape (trimming the longer side, as necesary).
562
563=head2 $img->get_dimensions(%arg)
564
565This utility method returns a width and height value pair after applying
566the given arguments. Valid arguments are the same as the L<scale> method.
567If 'Width' is given, a proportionate height will be calculated. If a
568'Height' is given, the width will be calculated. If 'Scale' is given
569the height and width will be calculated based on that scale (a value
570between 1 to 100).
571
572=head2 MT::Image->check_upload( %arg )
573
574Utility method used to handle image upload and storage, along with some
575constraining factors. The I<%arg> hash may contain the following elements:
576
577=over 4
578
579=item * Fh
580
581A filehandle for the uploaded file.
582
583=item * Fmgr
584
585A handle to a L<MT::FileMgr> object that will be used for writing the
586file into place.
587
588=item * Local
589
590A path and filename for the location to write the uploaded file.
591
592=item * Max (optional)
593
594A number that specifies the maximum physical file size for the uploaded
595image (specified in bytes).
596
597=item * MaxDim (optional)
598
599A number that specifies the maximum dimension allowed for the uploaded
600image (specified in pixels).
601
602=back
603
604If the uploaded image is valid and passes the file size and image
605dimension requirements (assuming those parameters are given),
606the return value is a list consisting of the following elements:
607
608=over 4
609
610=item * $width
611
612The width of the uploaded image, in pixels.
613
614=item * $height
615
616The height of the uploaded image, in pixels.
617
618=item * $id
619
620A string identifying the type of image file (returned by L<Image::Size>,
621so typically "GIF", "JPG", "PNG").
622
623=item * $write_coderef
624
625A Perl coderef that, when invoked writes the image to the specified
626location.
627
628=back
629
630If any error occurs from this routine, it will return 'undef', and
631assign the error message, accessible using the L<errstr> class method.
632
633=head1 AUTHOR & COPYRIGHT
634
635Please see the I<MT> manpage for author, copyright, and license information.
636
637=cut
Note: See TracBrowser for help on using the browser.