File Coverage

blib/lib/Image/Shoehorn.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             {
2              
3             =head1 NAME
4              
5             Image::Shoehorn - massage the dimensions and filetype of an image
6              
7             =head1 SYNOPSIS
8              
9             use Image::Shoehorn;
10             use Data::Dumper;
11              
12             my $image = Image::Shoehorn->new({
13             tmpdir => "/usr/tmp",
14             cleanup => \&my_cleanup
15             }) || die Image::Shoehorn->last_error();
16              
17             my $imgs = $image->import({
18             source => "/some/large/image.jpg",
19             max_height => 600,
20             valid => [ "png" ],
21             convert => 1,
22             scale => { thumb => "x50", small => "50%" },
23             overwrite => 1,
24             }) || die Image::Shoehorn->last_error();
25              
26             print &Dumper($imgs);
27              
28             =head1 DESCRIPTION
29              
30             Image::Shoehorn will massage the dimensions and filetype of an image,
31             optionally creating one or more "scaled" copies.
32              
33             It uses Image::Magick to do the heavy lifting and provides a single
34             "import" objet method to hide a number of tasks from the user.
35              
36             =head1 RATIONALE
37              
38             Just before I decided to submit this package to the CPAN, I noticed that
39             Lee Goddard had just released Image::Magick::Thumbnail. Although there is
40             a certain amount of overlap, creating thumbnails is only a part of the
41             functionality of Image::Shoehorn.
42              
43             Image::Shoehorn is designed for taking a single image, optionally converting
44             its file type and resizing it, and then creating one or more "scaled"
45             versions of the (modified) image.
46              
47             One example would be a photo-gallery application where the gallery may define
48             (n) number of scaled versions. In a mod_perl context, if the scaled image had
49             not already been created, the application might create the requested image
50             for the request and then register a cleanup handler to create the remaining
51             "scaled" versions. Additionally, scaled images may be defined as "25%", "x50",
52             "200x" or "25x75" (Apache::Image::Shoehorn is next...)
53              
54             =head1 SHOEHORN ?!
55              
56             This package started life as Image::Import; designed to slurp and munge images
57             into a database. It's not a very exciting name and, further, is a bit ambiguous.
58              
59             So, I started fishing around for a better name and for a while I was thinking
60             about Image::Tailor - a module for taking in the "hem" of an image, of fussing
61             and making an image fit properly.
62              
63             When I asked the Dict servers for a definition of "tailor", it returned a
64             WordNet entry containing the definition...
65              
66             make fit for a specific purpose [syn: {shoehorn}]
67              
68             ..and that was that.
69              
70             =cut
71              
72             package Image::Shoehorn;
73 1     1   1626 use strict;
  1         2  
  1         56  
74              
75             $Image::Shoehorn::VERSION = '1.42';
76              
77 1     1   9 use File::Basename;
  1         3  
  1         97  
78              
79 1     1   16 use Carp;
  1         3  
  1         72  
80 1     1   15416 use Error;
  1         9252  
  1         83  
81              
82             # use Data::Dumper;
83              
84 1     1   802 use Image::Magick 5.44;
  0            
  0            
85             use File::MMagic;
86              
87             =head1 PACKAGE METHODS
88              
89             =cut
90              
91             =head2 __PACKAGE__->last_error()
92              
93             Returns the last error recorded by the object.
94              
95             =cut
96              
97             sub last_error {
98             my $pkg = shift;
99             my $e = shift;
100            
101             if ($e) {
102             my $caller = (caller(1))[3];
103             Error::Simple->record("[$caller] $e.");
104             return 1;
105             }
106            
107             return Error->prior();
108             }
109              
110             =head2 __PACKAGE__->dimensions_for_scale($x,$y,$scale)
111              
112             =cut
113              
114             sub dimensions_for_scale {
115             my $pkg = shift;
116             my $x = shift;
117             my $y = shift;
118             my $scale = shift;
119              
120             if ($scale =~ /^(\d+)x(\d+)$/) {
121             $x = $1;
122             $y = $2;
123             }
124            
125             elsif ($scale =~ /^(\d+)%$/) {
126             $x = ($x/100) * $1;
127             $y = ($y/100) * $1;
128             }
129            
130             elsif ($scale =~ /^(\d+)x$/) {
131             ($x,$y) = __PACKAGE__->scaled_dimensions([$x,$y,$1,undef]);
132             }
133            
134             elsif ($scale =~ /^x(\d+)$/) {
135             ($x,$y) = __PACKAGE__->scaled_dimensions([$x,$y,undef,$1]);
136             }
137            
138             else {
139             return ();
140             }
141              
142             return (int($x),int($y));
143             }
144              
145             =head2 __PACKAGE__->scaled_name([$source,$scale])
146              
147             =cut
148              
149             sub scaled_name {
150             my $pkg = shift;
151             my $args = shift;
152              
153             my $scaled = &basename($args->[0]);
154              
155             my $id = ($args->[1]) ? "-$args->[1]" : "";
156              
157             $scaled =~ s/(.*)(\.[^\.]+)$/$1$id$2/;
158             $scaled =~ s/%/percent/;
159              
160             return $scaled;
161             }
162              
163             =head2 __PACKAGE__->converted_name([$source,$type])
164              
165             =cut
166              
167             sub converted_name {
168             my $pkg = shift;
169             my $args = shift;
170              
171             if (! $args->[1]) { return $args->[0]; }
172              
173             my $converted = $args->[0];
174             $converted =~ s/^(.*)\.([^\.]+)$/$1\.$args->[1]/;
175              
176             return $converted;
177             }
178              
179             =head2 __PACKAGE__->scaled_dimensions([$cur_x,$cur_y,$new_x,$new_y])
180              
181             =cut
182              
183             sub scaled_dimensions {
184             my $pkg = shift;
185             my $width = $_[0]->[0];
186             my $height = $_[0]->[1];
187             my $x = $_[0]->[2] || $width;
188             my $y = $_[0]->[3] || $height;
189              
190             if (($width == $x) && ($height == $y)) {
191             return ($x,$y);
192             }
193              
194             #
195              
196             foreach ($width, $height, $x, $y) {
197             if ($_ < 1) {
198             carp "Dimension (width:$width, height:$height, x:$x, y:$y) less than one. ".
199             "Returning 0,0 to avoid possible divide by zero error.\n";
200              
201             return (0,0);
202             }
203             }
204              
205             #
206              
207             my $h_percentage = $y / $height;
208             my $w_percentage = $x / $width;
209             my $percentage = 100;
210            
211             if (($x) && ($y )) { $percentage = ($h_percentage <= $w_percentage) ? $h_percentage : $w_percentage; }
212             if (($x) && (!$y)) { $percentage = $w_percentage; }
213             if ((!$x) && ($y )) { $percentage = $h_percentage; }
214            
215             $x = int($width * $percentage);
216             $y = int($height * $percentage);
217            
218             return ($x,$y);
219             }
220              
221             =head2 $pkg = __PACKAGE__->new(\%args)
222              
223             Object constructor. Valid arguments are :
224              
225             =over 4
226              
227             =item *
228              
229             B
230              
231             String.
232              
233             The path to a directory where your program has permissions to create new files. I
234              
235             =item *
236              
237             B
238              
239             Code reference.
240              
241             By default, any new images that are created, in the tmp directory, are deleted
242             when a different image is imported or when the I
243             method is invoked.
244              
245             You may optionally provide your own cleanup method which will be called in
246             place.
247              
248             Your method will be passed a hash reference where the keys are "source" and
249             any other names you may define in the I parameter of the I
250             object method. Each key points to a hash reference whose keys are :
251              
252             =over 4
253              
254             =item *
255              
256             I
257              
258             =item *
259              
260             I
261              
262             =item *
263              
264             I
265              
266             =item *
267              
268             I
269              
270             =item *
271              
272             I
273              
274             =back
275              
276             Note that this method will only affect B images. The original source file
277             may be altered, if it is imported with the I parameter, but will
278             not be deleted.
279              
280             =back
281              
282             Returns an object. Woot!
283              
284             =cut
285              
286             sub new {
287             my $pkg = shift;
288              
289             my $self = {};
290             bless $self,$pkg;
291              
292             if (! $self->init(@_)) {
293             return undef;
294             }
295              
296             return $self
297             }
298              
299             sub init {
300             my $self = shift;
301             my $args = shift;
302              
303             if (! -d $args->{'tmpdir'} ) {
304             $self->last_error("Unable to locate tmp dir");
305             return 0;
306             }
307              
308             if (($args->{'cleanup'}) && (ref($args->{'cleanup'}) ne "CODE")) {
309             $self->last_error("Cleanup is not a code reference.");
310             return 0;
311             }
312              
313             if (! $self->_magick()) {
314             $self->last_error("Unable to get Image::Magick : $!");
315             return 0;
316             }
317              
318             $self->{'__cleanup'} = $args->{'cleanup'};
319             $self->{'__tmpdir'} = $args->{'tmpdir'};
320             return 1;
321             }
322              
323             =head1 OBJECT METHODS
324              
325             =cut
326              
327             =head2 $obj->import(\%args)
328              
329             Valid arguments are :
330              
331             =over 4
332              
333             =item *
334              
335             B
336              
337             String.
338              
339             The path to the image you are trying to import. If ImageMagick can read it,
340             you can import it.
341              
342             I
343              
344             =item *
345              
346             B
347              
348             Int.
349              
350             The maximum width that the image you are importing may be. Height is scaled
351             accordingly.
352              
353             =item *
354              
355             B
356              
357             Int.
358              
359             The maximum height that the image you are importing may be. Width is scaled
360             accordingly.
361              
362             =item *
363              
364             B
365              
366             Hash reference.
367              
368             One or more key-value pairs that define scaling dimensions for creating
369             multiple instances of the current image.
370              
371             The key is a human readable label because humans are simple that way. The
372             key may be anything you'd like B "source" which is reserved for the
373             image the object is munging.
374              
375             The value for a given key is the dimension flag which may be represented as :
376              
377             =over 4
378              
379             =item *
380              
381             B%
382              
383             =item *
384              
385             BxB
386              
387             =item *
388              
389             xB
390              
391             =item *
392              
393             Bx
394              
395             =back
396              
397             Note that images are scaled B the original source file may have been
398             resized according to the I, I flags and I
399             flags.
400              
401             Scaled images are created in the I defined in the object constructor.
402              
403             =item *
404              
405             B
406              
407             Array reference.
408              
409             An list of valid file-types for which I has encoding support.
410              
411             =item *
412              
413             B
414              
415             Boolean.
416              
417             If this value is true and the source does not a valid file-type, the method
418             will create a temporary file attempt to convert it to one of the specified
419             valid file-types. The method will try to convert in the order the valid
420             file-types are specified, stopping on success.
421              
422             =item *
423              
424             B
425              
426             Code reference.
427              
428             Define a per instance cleanup function for an image. This functions exactly
429             the same way that a cleanup function defined in the object constructor does,
430             except that it is forgotten as soon as a new image is imported.
431              
432             =item *
433              
434             B
435              
436             Boolean.
437              
438             Indicates whether or not to preserve the source file. By default, the package
439             will B perform munging on the source file itself and will instead create
440             a new file in the I defined in the object constructor.
441              
442             =back
443              
444             Returns a hash reference with information for the source image -- note that
445             this may or may not be the input document, but the newly converted/resized
446             image created in you tmp directory -- and any scaled images you may have
447             defined.
448              
449             The keys of the hash are human readable names. The values are hash references
450             whose keys are :
451              
452             =over 4
453              
454             =item *
455              
456             I
457              
458             =item *
459              
460             I
461              
462             =item *
463              
464             I
465              
466             =item *
467              
468             I
469              
470             =item *
471              
472             I
473              
474             =item *
475              
476             I
477              
478             =item *
479              
480             I
481              
482             Deprecated in favour or I
483              
484             =back
485              
486             If there was an error, the method will return undef.
487              
488             =cut
489              
490             sub import {
491             my $self = shift;
492             my $args = shift;
493              
494             #
495              
496             if (! -e $args->{'source'}) {
497             $self->last_error("Unknown file $args->{'source'}");
498             return undef;
499             }
500              
501             if (($args->{'cleanup'}) && (ref($args->{'cleanup'}) ne "CODE")) {
502             $self->last_error("Cleanup is not a code reference.");
503             return undef;
504             }
505              
506             if (! $self->_magick()->Ping($args->{'source'})) {
507             $self->last_error("Unable to ping $args->{'source'}: $!");
508             return undef;
509             }
510              
511             #
512              
513             if (($self->{'__source'}) && ($args->{'source'} ne $self->{'__source'})) {
514             $self->_cleanup();
515             }
516              
517             if ($args->{'cleanup'}) {
518             $self->{'__instancecleanup'} = $args->{'cleanup'};
519             }
520              
521             #
522              
523             $self->{'__source'} = $args->{'source'};
524             $self->{'__dest'} = $self->{'__source'};
525              
526             unless ($args->{'overwrite'}) {
527             $self->{'__dest'} = "$self->{'__tmpdir'}/".&basename($args->{'source'});
528             }
529              
530             #
531              
532             if (! $self->_process($args)) {
533             return undef;
534             }
535              
536             #
537              
538             my $validation = $self->_validate($args);
539              
540             if ((! $validation->[0]) && (! $validation->[1])) {
541             return undef;
542             }
543              
544             #
545              
546             if (! keys %{$args->{'scale'}}) {
547              
548             my $dest = ($args->{'overwrite'})?
549             __PACKAGE__->converted_name([$self->{'__images'}{'source'}{'path'},$validation->[1]]) :
550             "$self->{'__tmpdir'}/".&basename(__PACKAGE__->converted_name([$self->{'__images'}{'source'}{'path'},
551             $validation->[1]]));
552              
553             my ($x,$y) = $self->_shoehorn({source => $self->{'__images'}{'source'}{'path'},
554             dest => $dest,
555             type => $validation->[1]});
556              
557             if (! $x) {
558             return undef;
559             }
560              
561             return {source=>$self->_ping($dest)};
562             }
563              
564             #
565              
566             foreach my $name (keys %{$args->{'scale'}}) {
567              
568             next if ($name eq "source");
569              
570             if (! $self->_scale({
571             name => $name,
572             scale => $args->{'scale'}->{$name},
573             type => $validation->[1],
574             })) {
575             return undef;
576             }
577             }
578              
579             map { shift; } @{$self->_magick()};
580             return $self->{'__images'};
581             }
582              
583             # =head2 $obj->_process(\%args)
584             #
585             # =cut
586              
587             sub _process {
588             my $self = shift;
589             my $args = shift;
590              
591             $self->{'__images'}{'source'} = $self->_ping($self->{'__source'}) || return 0;
592              
593             #
594              
595             my $validation = $self->_validate($args);
596              
597             if ((! $validation->[0]) && (! $validation->[1])) {
598             return 0;
599             }
600              
601             #
602              
603             if ((! $args->{'max_height'}) && (! $args->{'max_width'})) {
604             return 1;
605             }
606              
607             #
608              
609             my $geometry = undef;
610             my $newtype = undef;
611              
612             #
613              
614             my ($x,$y) = __PACKAGE__->scaled_dimensions([
615             $self->{'__images'}{'source'}{'width'},
616             $self->{'__images'}{'source'}{'height'},
617             $args->{'max_width'},
618             $args->{'max_height'}
619             ]);
620              
621             unless (($x == $self->{'__images'}{'source'}{'width'}) &&
622             ($y == $self->{'__images'}{'source'}{'height'})) {
623              
624             $geometry = join("x",$x,$y);
625             }
626              
627             #
628              
629             $newtype = $validation->[1];
630              
631             #
632              
633             if ((! $newtype) && (! $geometry)) {
634             return 1;
635             }
636              
637             if ($newtype) {
638             $self->{'__dest'} =~ s/^(.*)\.($self->{'__images'}{'source'}{'type'})$/$1\.$newtype/;
639             }
640              
641             #
642              
643             $self->_shoehorn({
644             geometry => $geometry,
645             type => $newtype
646             });
647              
648             if (! $x) { return 0; }
649              
650             #
651              
652             if ($newtype) {
653             $self->{'__images'}{'source'} = $self->_ping($self->{'__dest'});
654             }
655              
656             else {
657             $self->{'__images'}{'source'}{'height'} = $y;
658             $self->{'__images'}{'source'}{'width'} = $x;
659             }
660              
661             return 1;
662             }
663              
664             # =head2 $obj->_validate(\@valid)
665             #
666             # Returns an array ref containing a boolean (is valid type) and a possible
667             # type for conversion
668             #
669             # =cut
670              
671             sub _validate {
672             my $self = shift;
673             my $args = shift;
674              
675             if (exists($self->{'__validation'})) { return $self->{'__validation'}; }
676              
677             unless (ref($args->{'valid'}) eq "ARRAY") {
678             $self->{'__validation'} = [1];
679             return $self->{'__validation'};
680             }
681              
682             if (grep /^($self->{'__images'}{'source'}{'type'})$/,@{$args->{'valid'}}) {
683             $self->{'__validation'} = [1];
684             return $self->{'__validation'};
685             }
686              
687             foreach my $type (@{$args->{'valid'}}) {
688             my $encode = ($self->_magick()->QueryFormat(format=>$type))[4];
689              
690             if ($encode) {
691             $self->{'__validation'} = [1,$type];
692             return $self->{'__validation'};
693             }
694             }
695              
696             $self->{'__validation'} = [0];
697             return $self->{'__validation'};
698             }
699              
700             # =head2 $obj->_scale($name,$scale)
701             #
702             # =cut
703              
704             sub _scale {
705             my $self = shift;
706             my $args = shift;
707              
708             my $scaled = __PACKAGE__->scaled_name([$self->{'__dest'},
709             $args->{'name'}]);
710              
711             $scaled = "$self->{'__tmpdir'}/$scaled";
712              
713             if ($args->{'type'}) {
714             $scaled = __PACKAGE__->converted_name([$scaled,$args->{'type'}]);
715             }
716              
717             my ($width,$height) = __PACKAGE__->dimensions_for_scale(
718             $self->{'__images'}{'source'}->{'width'},
719             $self->{'__images'}{'source'}->{'height'},
720             $args->{'scale'},
721             );
722              
723             if ((! $width) || (! $height)) {
724             $self->last_error("Unable to determine dimensions for '$args->{scale}'");
725             return 0;
726             }
727            
728             my ($x,$y) = $self->_shoehorn({
729             source => $self->{'__images'}{'source'}{'path'},
730             dest => $scaled,
731             geometry => join("x",$width,$height),
732             type => $args->{'type'},
733             });
734              
735             if (! $x) { return 0; }
736              
737             $self->{'__images'}{$args->{'name'}} = $self->_ping($scaled) || return 0;
738              
739             return 1
740             }
741              
742             # =head2 $obj->_shoehorn(\%args)
743             #
744             # =cut
745              
746             sub _shoehorn {
747             my $self = shift;
748             my $args = shift;
749            
750             $args->{'source'} ||= $self->{'__source'};
751             $args->{'dest'} ||= $self->{'__dest'};
752              
753             # my $caller = (caller(1))[3];
754             # print STDERR "Shoehorn ($caller):\n".&Dumper($args);
755              
756             #
757              
758             $self->_read($args->{'source'}) || return 0;
759              
760             #
761              
762             if ($args->{'geometry'}) {
763              
764             if (my $err = $self->_magick()->Scale(geometry=>$args->{'geometry'})) {
765             $self->last_error("Failed to scale $args->{'source'} : $err");
766             return 0;
767             }
768              
769             }
770              
771             #
772              
773             if ($args->{'type'}) {
774             $args->{'dest'} = "$args->{'type'}:$args->{'dest'}";
775             }
776              
777             if (my $err = $self->_magick()->[0]->Write($args->{'dest'})) {
778             $self->last_error("Failed to write '$args->{'dest'}' : $@");
779             return 0;
780             }
781              
782             #
783              
784             return ($self->_magick()->Get("width"),$self->_magick()->Get("height"));
785             }
786              
787             # =head2 $obj->_read($file)
788             #
789             # =cut
790              
791             sub _read {
792             my $self = shift;
793              
794             if (my $err = $self->_magick()->Read($_[0]."[0]")) {
795             $self->last_error("Failed to ping '$_[0]' : $err");
796             return 0;
797             }
798              
799             # Hack. There must be a better way...
800             @{$self->{'__magick'}} = pop @{$self->{'__magick'}};
801             return 1;
802             }
803              
804             # =head2 $obj->_ping($file)
805             #
806             # =cut
807              
808             sub _ping {
809             my $self = shift;
810             my $file = shift;
811              
812             $self->_read($file) || return 0;
813              
814             # Because $magick->Ping() is often unreliable
815             # and fails to return height/width info. Dunno.
816              
817             $file =~ /^(.*)\.([^\.]+)$/;
818             my $extension = $2;
819            
820             return {
821             width => $self->_magick()->Get("width"),
822             height => $self->_magick()->Get("height"),
823             path => $file,
824             format => $self->_magick()->Get("format"),
825             type => $extension,
826             extension => $extension,
827             contenttype => $self->_mmagic()->checktype_filename($file),
828             };
829             }
830              
831             # =head2 $obj->_cleanup()
832             #
833             # =cut
834              
835             sub _cleanup {
836             my $self = shift;
837              
838             delete $self->{'__validation'};
839              
840             if ($self->{'__images'}{'source'}{'path'} eq $self->{'__source'}) {
841             delete $self->{'__images'}{'source'};
842             }
843              
844             if (ref($self->{'__instancecleanup'}) eq "CODE") {
845             my $result = &{ $self->{'__instancecleanup'} }($self->{'__images'});
846              
847             delete $self->{'__instancecleanup'};
848             return $result;
849             }
850              
851             if (ref($self->{'__cleanup'}) eq "CODE") {
852             return &{ $self->{'__cleanup'} }($self->{'__images'});
853             }
854              
855             foreach my $name (keys %{$self->{'__images'}}) {
856             my $file = $self->{'__images'}->{$name}->{'path'};
857             if (-f $file ) { unlink $file; }
858             }
859              
860             return 1;
861             }
862              
863             # =head2 $obj->_mmagic()
864             #
865             # Returns a File::MMagic object
866             #
867             # -cut
868              
869             sub _mmagic {
870             my $self = shift;
871              
872             if (ref($self->{'__mmagic'}) ne "File::MMagic") {
873             $self->{'__mmagic'} = File::MMagic->new();
874             }
875              
876             return $self->{'__mmagic'};
877             }
878              
879             # =head2 $obj->_magick()
880             #
881             # =cut
882              
883             sub _magick {
884             my $self = shift;
885              
886             if (ref($self->{'__magick'}) ne "Image::Magick") {
887             $self->{'__magick'} = Image::Magick->new();
888             }
889              
890             return $self->{'__magick'};
891             }
892              
893             # =head2 $obj->DESTROY()
894             #
895             # =cut
896              
897             sub DESTROY {
898             my $self = shift;
899             $self->_cleanup();
900             return 1;
901             }
902              
903             =head1 VERSION
904              
905             1.42
906              
907             =head1 DATE
908              
909             $Date: 2003/05/30 22:51:06 $
910              
911             =head1 AUTHOR
912              
913             Aaron Straup Cope
914              
915             =head1 TO DO
916              
917             =over 4
918              
919             =item *
920              
921             Modify constructor to accept all the options defined in the I
922             method as defaults.
923              
924             =item *
925              
926             Modify I to accept multiple files.
927              
928             =item *
929              
930             Modify I to accept strings and filehandles.
931              
932             =back
933              
934             =head1 SEE ALSO
935              
936             L
937              
938             L
939              
940             =head1 LICENSE
941              
942             Copyright (c) 2001-2003, Aaron Straup Cope. All Rights Reserved.
943              
944             This is free software, you may use it and distribute it under the same
945             terms as Perl itself.
946              
947             =cut
948              
949             return 1;
950              
951             }