File Coverage

blib/lib/XAO/ImageCache.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             XAO::ImageCache - Images caching by URLs stored in XAO::FS database
4              
5             =head1 SYNOPSIS
6              
7             use XAO::ImageCache;
8              
9             # Making new instance of Image Cache object
10             my $image_Cache = XAO::ImageCache->new(
11             list => $odb->fetch("/Products"),
12             cache_path => "/var/httpd/shop/product/images/",
13             cache_url => "/products/images/",
14             source_url_key => "source_img",
15             ) || die "Can't make Image cache!";
16              
17             # Init new empty Cache
18             $image_cache->init() || die "Can't init new cache!";
19              
20             # Start images checking and downloading to cache
21             $image_cache->check();
22              
23             =head1 DESCRIPTION
24              
25             When we store images links on own database we have no real
26             images on own site. Some time it may be a problem cause images may
27             have no right dimension or may be deleted from source site.
28              
29             XAO::ImageCache made for cache locally images his URL stored
30             in XAO Founsation Server. Also, images may be resized automaticaly.
31              
32             This module provide easy methods to scan XAO Foundation Server data
33             lists, to extract images source URLs from data objects, to download
34             images to local cache, to resize a local copy of the image to fit into
35             given dimensions and to store the new local URL of the image back to
36             the data object.
37              
38             =head1 METHODS
39              
40             =over
41              
42             =cut
43              
44             ###############################################################################
45             package XAO::ImageCache;
46 1     1   532 use strict;
  1         2  
  1         41  
47 1     1   5 use Error qw(:try);
  1         2  
  1         8  
48 1     1   153 use XAO::Utils;
  1         1  
  1         49  
49 1     1   385 use XAO::FS;
  1         57  
  1         23  
50 1     1   4 use XAO::Errors qw(XAO::ImageCache);
  1         1  
  1         7  
51 1     1   280 use Digest::MD5 qw(md5 md5_hex md5_base64);
  1         2  
  1         54  
52 1     1   601 use LWP::UserAgent;
  1         34521  
  1         38  
53 1     1   7 use URI;
  1         2  
  1         24  
54 1     1   257 use Image::Magick;
  0            
  0            
55             use Date::Manip;
56             use File::Path;
57             use File::Copy;
58              
59             use vars qw($VERSION);
60             $VERSION='1.20';
61              
62             ###############################################################################
63              
64             ##
65             # Methods prototypes
66             #
67             sub new ($%);
68             sub init($);
69             sub check($);
70             sub download ($$);
71             sub scale_file($$$$;$);
72             sub scale_large($$$);
73             sub scale_thumbnail($$$);
74             sub remove_cache($);
75              
76             # Special functions
77             sub get_filename($);
78             sub treat_filename($);
79             sub convert_time($);
80             sub throw($$);
81              
82             ###############################################################################
83              
84             sub DESTROY {
85             my $self = shift;
86             dprint "----- XAO Image Cache finished -----";
87             }
88              
89             ###############################################################################
90              
91             =item new($%)
92              
93             The constructor returns a new C object.
94             You can use it to make new images cache or check images
95             of already existent cache.
96              
97             my $image_cache = XAO::ImageCache->new(
98             cache_path => "cache", # set cache directory to './cache/'
99             source_path => "cache/source", # set source directory to './cache/source/'
100             local_path => "images/copy", # (optional) try to resolve local urls
101             cache_url => "images/", # set cached images (relative) path to 'images/'
102             list => $odb->fetch("/Products"),
103             source_url_key => 'source_image_url',
104             dest_url_key => 'dest_image_url',
105             filename_key => 'product_id',
106             min_width => 50, # Source image is ignored if smaller
107             min_height => 50,
108             size => {
109             width => 320,
110             height => 200,
111             save_aspect_ratio => 1,
112             },
113             thumbnails => {
114             path => '/var/httpd/shop/product/images/tbn',
115             url => '/products/images/tbn/'
116             geometry => "25%",
117             url_key => 'thumbnail_url',
118             },
119             autocreate => 1,
120             useragent => {
121             agent => 'My Lovely Browser/v13.01',
122             timeout => 30,
123             },
124             force_download => 1, # to enforce re-downloads
125             force_scale => 1, # to enforce re-scaling
126             clear_on_error => 0, # don't clear DB properties even on permanent errors
127             ) || die "Image cache creation failure!";
128              
129             A number of configuration parameters can be passed to
130             XAO::ImageCache to tune functionality.
131              
132             =over
133              
134             =item autocreate
135              
136             =item cache_path
137              
138             =item cache_url
139              
140             =item clear_on_error
141              
142             =item dest_url_key
143              
144             =item force_download
145              
146             =item force_scale
147              
148             =item list
149              
150             =item source_url_key
151              
152             =item size
153              
154             =item thumbnails
155              
156             =item useragent
157              
158             =back
159              
160             Follow to L
161             section to see what each parameter does.
162              
163             If any required parameter is not present an error will be thrown.
164              
165             =cut
166              
167             #
168             # Creating new instance of Image Cache object.
169             #
170             # Parameters hash should contain following keys:
171             #
172             # cache_path => path to image cache directory
173             # source_path => path to source image directory
174             # cache_url => relative URL pointing to cached images
175             # list => reference to XAO::DO::FS::List object
176             # containing data elements with images data
177             # source_url_key => source image url key name
178             # filename_key => key name image and thumbnail file names will
179             # be based on
180             #
181             # Optional parameters:
182             #
183             # dest_url_key => key name containing destination url name
184             # size => {
185             # width =>
186             # height =>
187             # }
188             # autocreate =>
189              
190             sub new ($%) {
191              
192             my $class = shift;
193             my $self = {};
194             bless $self,$class;
195              
196             # Predefined parameters
197             $self->{useragent} = { 'agent' => "XAO-ImageCache/$VERSION", };
198              
199             # Passed parameters
200             if (@_) {
201             my %extra = @_;
202             @$self{keys %extra} = values %extra;
203             }
204              
205             #
206             # Check required parameters
207             #
208              
209             unless (defined($self->{source_path})) {
210             $self->throw("- missing 'source_path' parameter!");
211             }
212              
213             unless (defined($self->{cache_path})) {
214             $self->throw("- missing 'cache_path' parameter!");
215             }
216              
217             unless (defined($self->{list})) {
218             $self->throw("- missing 'list' parameter!");
219             }
220              
221             unless (defined($self->{cache_url})){
222             $self->throw("- missing 'cache_url' parameter!") ;
223             }
224              
225             unless (defined($self->{source_url_key})) {
226             $self->throw("- missing 'source_url_key' parameter!");
227             }
228              
229             unless (defined($self->{dest_url_key})) {
230             $self->throw("- missing 'dest_url_key' parameter!");
231             }
232              
233             #
234             # Make sure paths end with /
235             #
236             $self->{'source_path'} .= '/' if $self->{'source_path'} !~ /\/$/;
237             $self->{'cache_path'} .= '/' if $self->{'cache_path'} !~ /\/$/;
238             $self->{'cache_url'} .= '/' if $self->{'cache_url'} !~ /\/$/;
239             $self->{'thumbnails'}->{'cache_path'}.= '/' if $self->{'thumbnails'}->{'cache_path'}
240             && $self->{'thumbnails'}->{'cache_path'} !~ /\/$/;
241             $self->{'thumbnails'}->{'cache_url'} .= '/' if $self->{'thumbnails'}->{'cache_url'}
242             && $self->{'thumbnails'}->{'cache_url'} !~ /\/$/;
243              
244             if (defined($self->{'min_period'}) && $self->{'min_period'}) {
245              
246             unless ($self->{'min_period'} =~ /\d+[smhd]/) {
247             $self->throw("- incorrectly formatted 'min_period' parameter!");
248             }
249              
250             # Make sure 'min_period' parameters is in seconds
251             $self->{'min_period'} =~ s/(\d+)([smhd])/$1/;
252             if ($2 eq 'm') {
253             $self->{'min_period'} *= 60;
254             }
255             elsif ($2 eq 'h') {
256             $self->{'min_period'} *= 3600;
257             }
258             elsif ($2 eq 'd') {
259             $self->{'min_period'} *= 86400;
260             }
261             }
262             else {
263             $self->{min_period} = 0;
264             }
265              
266             $self->{'reload'}=1 if $self->{'force_download'};
267              
268             $self->{'size'}->{'quality'}||=88;
269             $self->{'thumbnails'}->{'quality'}||=88;
270              
271             # Make LWP::UserAgent instance
272             my $hash_ref = $self->{'useragent'};
273             $self->{ua} = LWP::UserAgent->new( %$hash_ref)
274             || $self->throw("- LWP::UserAgent creation failure!");
275              
276             $self->init() if defined($self->{autocreate} && $self->{autocreate});
277             $self;
278             }
279              
280             ###############################################################################
281              
282             =item init($)
283              
284             Cache structure initialization.
285              
286             Executed automaticaly if C parameter present.
287              
288             Create image cache directory if non existent and thumbnail cache
289             directory if non existent and defined as initialization parameter.
290              
291             =cut
292              
293             sub init($) {
294             my $self = shift;
295              
296             dprint "----- XAO Image Cache Initialization started -----";
297              
298             my $source_path = $self->{source_path};
299             my $img_cache_path = $self->{cache_path};
300             my $thm_cache_path = $self->{thumbnails}->{cache_path};
301              
302             #
303             # Create directories if non existent
304             #
305              
306             unless (-d $source_path) {
307             mkdir($source_path,0777)
308             || $self->throw("- cache directory can't be created! $!");
309             dprint "Image Cache directory '$source_path' created.";
310             }
311              
312             unless (-d $img_cache_path) {
313             mkdir($img_cache_path,0777)
314             || $self->throw("- cache directory can't be created! $!");
315             dprint "Image Cache directory '$img_cache_path' created.";
316             }
317              
318             if ($thm_cache_path) {
319             unless (-d $thm_cache_path) {
320             mkdir($thm_cache_path, 0777)
321             || $self->throw("- can't create thumbnails cache directory ($thm_cache_path)! $!");
322             dprint "Thumbnail Cache directory '$thm_cache_path' created.";
323             }
324             }
325              
326             $self->check() if defined($self->{autocreate} && $self->{autocreate});
327              
328             return 1;
329             }
330              
331             ###############################################################################
332              
333             =item check($)
334              
335             Goes through given XAO FS data list, downloads images from source url
336             to cache and puts cache url into destination url key and thumbnail url
337             key (where applicable).
338              
339             XAO::ImageCache->download() will be executed for downloading each image.
340              
341             =cut
342              
343             sub check($) {
344              
345             my $self = shift;
346              
347             my $img_src_url_key = $self->{'source_url_key'};
348             my $img_dest_url_key = $self->{'dest_url_key'};
349             my $img_cache_url = $self->{'cache_url'};
350             my $thm_src_url_key = $self->{'thumbnails'}->{'source_url_key'} || '';
351             my $thm_dest_url_key = $self->{'thumbnails'}->{'dest_url_key'} || '';
352             my $thm_cache_url = $self->{'thumbnails'}->{'cache_url'} || '';
353             my $thm_cache_path = $self->{'thumbnails'}->{'cache_path'} || '';
354              
355             my $getter=$self->{'get_property_sub'} || sub ($$) {
356             my ($obj,$prop)=@_;
357             return $prop ? $obj->get($prop) : '';
358             };
359              
360             my $checked = 0;
361             my $list = $self->{'list'};
362             my $list_keys = $self->{'list_keys'} || [ $list->keys ];
363              
364             my $count=0;
365             my $total=scalar(@$list_keys);
366             foreach my $item_id (@$list_keys) {
367              
368             dprint "Checking ID='$item_id', count=".$count++."/$total";
369              
370             my $item = $list->get($item_id);
371             my $img_src_url = $getter->($item,$img_src_url_key);
372             my $thm_src_url = $getter->($item,$thm_src_url_key);
373              
374             # Skipping products without images
375             #
376             next unless $img_src_url || $thm_src_url;
377              
378             # Download source image and create cache image and thumbnail
379             #
380             try {
381             my ($img_cache_file, $thm_cache_file)=$self->download(
382             $item,
383             $img_src_url,
384             $thm_src_url,
385             );
386              
387             dprint ".storing('$img_cache_file','$thm_cache_file')";
388             my %d;
389             $d{$img_dest_url_key}=$img_cache_url.$img_cache_file if $img_dest_url_key;
390             $d{$thm_dest_url_key}=$thm_cache_url.$thm_cache_file if $thm_dest_url_key;
391             $item->put(\%d);
392             }
393             otherwise {
394             my $e=shift;
395             eprint "$e";
396              
397             # We don't update the item unless this is a permanent error
398             # and we have a 'clear_on_error' argument.
399             #
400             if("$e" =~ /PERMANENT/) {
401             if($self->{'clear_on_error'}) {
402             dprint ".permanent error - clearing existing image/thumbnail urls";
403             my %d;
404             $d{$img_dest_url_key}='' if $img_dest_url_key;
405             $d{$thm_dest_url_key}='' if $thm_dest_url_key;
406             $item->put(\%d);
407             }
408             }
409             };
410              
411             $checked++;
412             }
413              
414             return $checked;
415             }
416              
417             ###############################################################################
418              
419             =item download($$)
420              
421             Downloads image into cache directory.
422              
423             If C contains C parameter, thumbnail is either
424             downloaded into thumbnail cache directory or created from downloaded
425             image.
426              
427             Source image URL should be passed as parameter. Source thumbnail URL is
428             an optional parameter:
429              
430             $img_cache->download($image_source_url, $thumbnail_source_url);
431              
432             Downloaded image is resized if C parameter present. Thumbnail is
433             resized as specified by C C parameter.
434              
435             When C configuration parameter is not set to True value, image
436             will be downloaded into cache only if image is not already cached or if
437             cached image has a later modification date than source image.
438              
439             =cut
440              
441             sub download ($$) {
442             my ($self, $item, $img_src_url, $thm_src_url) = @_;
443              
444             my $fnm_key = $self->{filename_key} || '';
445             my $base_fnm = $fnm_key ? treat_filename($item->get($fnm_key))
446             : get_filename($img_src_url);
447             my $img_fnm = $img_src_url ? $base_fnm.'_img.jpeg' : '';
448             my $img_src_fnm = $img_src_url ? $base_fnm.'_src.jpeg' : '';
449              
450             my $user_agent = $self->{ua};
451             my $source_path = $self->{source_path};
452             my $img_cache_path = $self->{cache_path};
453             my $thm_cache_path = $self->{thumbnails}->{cache_path} || '';
454              
455             ##
456             # Local path can be a reference to an array of paths
457             #
458             my $local_path=$self->{'local_path'} || '';
459             $local_path=[ $local_path ] unless ref($local_path);
460              
461             my $img_src_file = $source_path.$img_src_fnm;
462             my $img_cache_file = $img_cache_path.$img_fnm;
463              
464             my ($thm_fnm, $thm_cache_file, $thm_src_file);
465             if ($thm_cache_path) {
466             $thm_fnm = $thm_src_url ? get_filename($thm_src_url) : $base_fnm;
467             my $thm_src_fnm = $thm_fnm.'_thmsrc.jpeg';
468             $thm_fnm .= '_thm.jpeg';
469             $thm_cache_file = $thm_cache_path.$thm_fnm;
470             $thm_src_file = $source_path.$thm_src_fnm;
471             }
472              
473             # Download thumbnail if specified and resize (keep source and
474             # resized images). If the file is missed in the cache, but it
475             # exists in the sources and is actual -- then resizing it without
476             # downloading.
477             #
478             my $time_now = time;
479             if($thm_cache_path && $thm_src_url) {
480             my $mtime_src = (stat($thm_src_file))[9];
481             my $period = $time_now - $mtime_src;
482             if($period > $self->{'min_period'}) {
483             if($thm_src_url !~ m/^(https?|ftp):\/\//i) {
484             my $lfound;
485             if($thm_src_url=~/^\// && -r $thm_src_url) {
486             $lfound=1;
487             copy($thm_src_url,$thm_src_file);
488             }
489             else {
490             foreach my $lpath (@$local_path) {
491             if(-r "$lpath/$thm_src_url") {
492             copy("$lpath/$thm_src_url",$thm_src_file);
493             $lfound=1;
494             last;
495             }
496             }
497             }
498             if(!$lfound) {
499             $self->throw("- seems to be a local URL and no local file ($thm_src_url)");
500             }
501             }
502             else {
503             my $response = $user_agent->head($thm_src_url);
504             if ($response->is_success) {
505             my $mtime_web = convert_time($response->header('Last-Modified'));
506             if ((!-r $thm_src_file) || ($mtime_src < $mtime_web) || $self->{'reload'} || $self->{'force_download'}) {
507             $self->download_file($thm_src_url, $thm_src_file);
508             }
509             }
510             else {
511             if(-r $thm_src_file) {
512             dprint "...error downloading thumbnail (".$response->status_line."), keeping existing source $thm_src_file";
513             }
514             else {
515             $self->throw("- can't get thumbnail header '$thm_src_url' - ".$response->status_line." (NETWORK)");
516             $thm_src_file = '';
517             }
518             }
519             }
520             }
521             else {
522             dprint ".thumbnail source file current: $thm_src_url";
523             }
524              
525             # Resizing if required
526             #
527             if($thm_src_file) {
528             my $mtime_cache=(stat($thm_cache_file))[9];
529             $mtime_src=(stat($thm_src_file))[9];
530             if($mtime_cache<=$mtime_src) {
531             $self->scale_thumbnail($thm_src_file, $thm_cache_file);
532             }
533             }
534             }
535              
536             # Download source image and resize (keep source and resized
537             # images). Only download source image if cached image not present or
538             # older than source.
539             #
540             if($img_src_url) {
541             my $mtime_src = (stat($img_src_file))[9] || 0;
542             my $period = $time_now - $mtime_src;
543             if($self->{'force_download'} || $period>$self->{'min_period'}) {
544             if($img_src_url !~ m/^(https?|ftp):\/\//i) {
545             my $lfound;
546             if($img_src_url=~/^\// && -r $img_src_url) {
547             $lfound=1;
548             copy($img_src_url,$img_src_file);
549             }
550             else {
551             foreach my $lpath (@$local_path) {
552             if(-r "$lpath/$img_src_url") {
553             copy("$lpath/$img_src_url",$img_src_file);
554             $lfound=1;
555             last;
556             }
557             }
558             }
559             if(!$lfound) {
560             $self->throw("- seems to be a local URL and no local file ($img_src_url)");
561             }
562             }
563             else {
564             my $response = $user_agent->head($img_src_url);
565             if ($response->is_success) {
566             my $mtime_web = convert_time($response->header('Last-Modified'));
567             if ((!-r $img_src_file) || ($mtime_src < $mtime_web) || $self->{'reload'} || $self->{'force_download'}) {
568             $self->download_file($img_src_url, $img_src_file);
569             }
570             else {
571             dprint ".image source file current: $img_src_url";
572             }
573             }
574             else {
575              
576             # This is here mainly for situations like this: a
577             # superseded item content gets into a new item, but
578             # the image URL is long gone, not accessible; we
579             # have a cached copy and we use it.
580             #
581             if(-r $img_src_file) {
582             dprint "...error downloading image (".$response->status_line."), keeping existing source $thm_src_file";
583             }
584             else {
585             $self->throw("- can't get image header for $img_src_url - ".$response->status_line." (NETWORK)");
586             $img_src_file = '';
587             }
588             }
589             }
590             $mtime_src=(stat($img_src_file))[9] if $img_src_file;
591             }
592              
593             if($img_src_file) {
594              
595             # Now checking if the source file we have is newer then what's in
596             # the cache and updating the cache in that case.
597             #
598             my $mtime_cache=(stat($img_cache_file))[9] || 0;
599             if($self->{'force_scale'} || $mtime_cache < $mtime_src) {
600             $self->scale_large($img_src_file, $img_cache_file);
601             }
602              
603             # Create thumbnail from the image source file if necessary
604             #
605             if($thm_cache_file && (!$thm_src_url || !$thm_src_file)) {
606             $mtime_cache=(stat($thm_cache_file))[9] || 0;
607             if($self->{'force_scale'} || $mtime_cache < $mtime_src) {
608             $self->scale_thumbnail($img_src_file,$thm_cache_file);
609             }
610             $thm_src_file=1; # Just to mark that we have it
611             }
612             }
613             }
614              
615             $img_fnm='' unless $img_src_file;
616             $thm_fnm='' unless $thm_src_file;
617              
618             ### dprint "...scaled->('$img_fnm','$thm_fnm')";
619              
620             return ($img_fnm, $thm_fnm);
621             }
622              
623             ###############################################################################
624              
625             sub download_file {
626             my $self = shift;
627             my $source_url = shift;
628             my $source_file = shift;
629              
630             dprint "DOWNLOAD ($source_url)->($source_file)";
631              
632             my $response=$self->{'ua'}->get($source_url);
633             my $errstr;
634             if($response->is_success) {
635             if($response->content_type =~ /^image\//) {
636             open(F,"> $source_file.tmp") || $self->throw("- unable to save file '$source_file': $!");
637             binmode(F);
638             print F $response->content;
639             close(F);
640             rename("$source_file.tmp",$source_file);
641             }
642             else {
643             $errstr="downloaded '$source_url' is not an image (".$response->content_type.") (PERMANENT)";
644             }
645             }
646             else {
647             $errstr="can't download '$source_url' - ".$response->status_line." (NETWORK)";
648             }
649              
650             return unless $errstr;
651              
652             # If the file is already there (from previous downloads then just
653             # printing the error, but returning normally to let the file be scaled and
654             # stored as image/thumbnail
655             #
656             if(-f $source_file && !$self->{'clear_on_error'}) {
657             eprint "download_file: $errstr -- ignoring and keeping existing file";
658             return;
659             }
660             else {
661             $self->throw("- $errstr");
662             }
663             }
664              
665             ###############################################################################
666              
667             sub scale_file ($$$$;$) {
668             my ($self,$infile,$outfile,$params,$label)=@_;
669              
670             $label||='unknown';
671              
672             if(!$params) {
673             dprint "Copying $infile to $outfile as is ($label)";
674             copy($infile,$outfile);
675             return;
676             }
677              
678             my $geometry = ''; # image dimensions in ImageMagick geometry format
679              
680             my $image = Image::Magick->new() || $self->throw("- Image::Magick creation failure!");
681             my $err = $image->ReadImage($infile);
682             ### dprint ".source in '$infile'";
683              
684             # Only throwing an error if no image was read. If we consider all
685             # warnings as errors then some 3M tiff files don't parse.
686             #
687             # From http://www.imagemagick.org/script/perl-magick.php:
688             #
689             # $x = $image->Read(...);
690             # warn "$x" if "$x"; # print the error message
691             # $x =~ /(\d+)/;
692             # print $1; # print the error number
693             # print 0+$x; # print the number of images read
694             #
695             ### $self->throw("- parsing error ($err) (PERMANENT)") if $err;
696             (0 + $err)>0 ||
697             $self->throw("- parsing error ($err) (PERMANENT)");
698              
699             # We only deal with image/* types -- otherwise ImageMagick can
700             # sometimes successfully open and convert HTML or text messages into
701             # images.
702             #
703             $image->Get('mime')=~/^image\// || $self->throw("- not an image file '$infile' (".$image->Get('mime').")");
704              
705             # Get source image dimensions
706             #
707             my ($src_width, $src_height) = $image->Get('columns','rows');
708              
709             my $min_width=$self->{'min_width'} || 10;
710             my $min_height=$self->{'min_height'} || $min_width;
711              
712             if($src_height<=1 || $src_width<=1 || ($src_height<$min_height && $src_width<$min_width)) {
713             $self->throw("- image ($src_width,$src_height) is smaller than the minimum ($min_width,$min_height) for '$label' (PERMANENT)");
714             }
715              
716             # Getting target image width and height
717             #
718             if($params->{'geometry'}){
719             $geometry = $params->{geometry}; # size was set as geometry string
720             }
721             elsif($params->{'save_aspect_ratio'}) {
722             my $src_aspect=$src_width/$src_height;
723              
724             my $target_width=$params->{'width'} || $src_width;
725             my $target_height=$params->{'height'} || $src_height;
726             my $target_aspect=$target_width/$target_height;
727              
728             my ($width,$height);
729              
730             if($src_width<=$target_width && $src_height<=$target_height) {
731             if(lc($image->Get('mime')) eq 'image/jpeg' && ($image->get('colorspace') || '') =~ /^s?RGB$/i) {
732             copy($infile,$outfile);
733             dprint "..copied ${src_width}x${src_height} as is for '$label' (jpeg, fits into ${target_width}x${target_height})";
734             return;
735             }
736             else {
737             $width=$src_width;
738             $height=$src_height;
739             }
740             }
741             elsif($target_aspect>$src_aspect) {
742             $height=$target_height;
743             $width=int($height*$src_aspect+0.5);
744             }
745             else {
746             $width=$target_width;
747             $height=int($width/$src_aspect+0.5);
748             }
749              
750             $geometry=$width.'x'.$height.'!';
751             }
752             else {
753             # Use given width & height as is (or image size if not set)
754             $geometry = ($params->{'width'} || $src_width) .'x'.
755             ($params->{'height'} || $src_height).'!';
756             }
757              
758             # We don't support transparency and by default transparent regions
759             # sometimes get translated to black. Forcing them into white.
760             #
761             $image->Set(background => 'white');
762             $image=$image->Flatten();
763              
764             # This is required, otherwise new ImageMagick sometimes converts
765             # images to CMYK colorspace for whatever reason.
766             #
767             # sRGB is the standard colorspace for monitors and printing, RGB
768             # is an unspecified Red/Green/Blue encoding. Some images come out
769             # darker when converted to RGB.
770             #
771             $image->Set(
772             colorspace => 'sRGB',
773             depth => 8,
774             );
775              
776             # Scaling
777             #
778             $image->Scale(geometry => $geometry);
779              
780             # Stripping embedded profiles & comments. They sometimes take
781             # upwards of 500kb
782             #
783             $image->Strip;
784              
785             # Writing the results
786             #
787             $image->Set(magick => 'JPEG');
788             $image->Set(quality => ($params->{'quality'} || 88));
789              
790             my $rc=$image->Write($outfile);
791              
792             !"$rc" || die "Error writing scaled image: $rc";
793              
794             dprint "..resized ${src_width}x${src_height} to $geometry for '$label'";
795             }
796              
797             ###############################################################################
798              
799             =item scale_large($$$)
800              
801             Scaling image to given size.
802              
803             =cut
804              
805             sub scale_large($$$) {
806             my ($self,$infile,$outfile)=@_;
807              
808             return $self->scale_file($infile,$outfile,$self->{'size'},'large');
809             }
810              
811             ###############################################################################
812              
813             =item scale_thumbnail($$$)
814              
815             Creates thumbnail image from given source image.
816              
817             =cut
818              
819             sub scale_thumbnail($$$) {
820             my ($self,$infile,$outfile)=@_;
821              
822             return $self->scale_file($infile,$outfile,$self->{'thumbnails'},'thumbnail');
823             }
824              
825             ###############################################################################
826              
827             =item remove_cache($)
828              
829             Removing the ENTIRE cache directory from disk.
830              
831             Be carefully to use this method!!!
832              
833             Cache structure will be removed from disk completely!
834             Set C parameter to True value to download
835             images to cache without any conditions.
836              
837             =cut
838              
839             sub remove_cache($) {
840             my $self = shift;
841             rmtree($self->{'cache_path'});
842             }
843              
844             ###############################################################################
845              
846             sub throw($$) {
847             my ($self,$message)=@_;
848              
849             if($message=~/^-/) {
850             (my $fname=(caller(1))[3])=~s/^.*://;
851             $message=$fname." ".$message;
852             }
853              
854             throw XAO::E::ImageCache $message;
855             }
856              
857             ###############################################################################
858              
859             =item get_filename($)
860              
861             File name generation for cached images.
862              
863             Source image URL should be passed. Returned file name is an MD5 digest
864             of the source URL converted to Base64 string with all non alpha numeric
865             characters are converted to C<_>.
866              
867             Example.
868              
869             Location:
870             http://localhost/icons/medbutton.jpeg
871              
872             provide file name:
873             4aFNA1utpmCNG2wEIF69mg.jpeg
874              
875             =cut
876              
877             # Return MD5 digest of given URL converted to Base64
878             # string with extension and
879             sub get_filename($) {
880             my $source = shift;
881             my $url = URI->new($source);
882             my $path= $url->path();
883             $path =~ /\./;
884             my $file = md5_base64($source);
885             return treat_filename($file);
886             }
887              
888             ###############################################################################
889              
890             =item treat_filename($)
891              
892             Makes sure only file name friendly characters are present: all non alpha
893             numeric characters are converted to C<_>.
894              
895             =cut
896              
897             sub treat_filename($) {
898             my $fnm = shift;
899             $fnm =~ s/\W/_/gm;
900             #$fnm =~ s/\//_/gm;
901             #$fnm =~ s/\+/\-/gm;
902             #$fnm =~ s/=/\-/gm;
903             return $fnm;
904             }
905              
906             ###############################################################################
907             # Convert 'Last-Modified' Date/Time in internet format
908             # to seconds since epoch format
909             # Wed, 21 Jan 2001 24:55:55 GMT
910             sub convert_time($) {
911             my $date_str = shift;
912             #print "Last Modified: $date_str\n";
913             $date_str =~ s/,//;
914             my @date_arr = split(/[\s+|:]/,$date_str);
915             my %month = (
916             Jan => 0, Feb => 1, Mar => 2, Apr => 3,
917             May => 4, Jun => 5, Jul => 6, Aug => 7,
918             Sep => 8, Oct => 9, Nov => 10, Dec => 11,
919             );
920             my %wday = (Sun => 0, Mon => 1, Tue => 2, Wed => 3, Thu => 4, Fri => 5, Sat => 6);
921             # Clearing leading zero
922             $date_arr[1] =~ s/^0//; # Month days
923             $date_arr[4] =~ s/^0//; # Hours
924             $date_arr[5] =~ s/^0//; # Minutes
925             $date_arr[6] =~ s/^0//; # Seconds
926             my $time=eval {
927             if ($date_arr[7] eq 'GMT') {
928             Time::Local::timegm(
929             $date_arr[6],
930             $date_arr[5],
931             $date_arr[4],
932             $date_arr[1],
933             $month{$date_arr[2]},
934             $date_arr[3],
935             );
936             }
937             else{
938             Time::Local::timelocal(
939             $date_arr[6],
940             $date_arr[5],
941             $date_arr[4],
942             $date_arr[1],
943             $month{$date_arr[2]},
944             $date_arr[3],
945             );
946             }
947             };
948             return $time || 0;
949             }
950              
951             ###############################################################################
952              
953             1;
954              
955             =back
956              
957             =head1 CONFIGURATION PARAMETERS
958              
959             The set of configuration parameters contain required and optional parameters.
960              
961             Required parameters should be defined. Execution will be stoped if required
962             parameter not present.
963              
964             Optional parameters just configure aditional functionality and may not present.
965              
966             =head2 Required parameters
967              
968             =over
969              
970             =item cache_path
971              
972             - Path string where the cache should be placed.
973              
974             May be absolute or relative from current execution directory path.
975              
976             For example. Set it to C<./cache> if you whant to place cache in
977             C subdirectory of your script working directory.
978              
979             =item cache_url
980              
981             - complet URL (or relative location) to cached images.
982              
983             Place here your URL reflection of cache directory in condition with
984             your HTTP server configuration.
985              
986             For example. Set it to C if your HTTP
987             server configured for provide access to your cache directory by
988             hostname C and location C. Cached images names
989             will be added to image URL automaticaly.
990              
991             =item list
992              
993             - reference to C object containing the data objects
994             with Image source URL
995              
996             Meaning, your data look like a XAO Foundation Server list of objects
997             with references to images. This parameter should contain reference to
998             to XAO::DO::FS::List object. This reference may be result of
999             XAO::Objects->fetch() methode.
1000              
1001             XAO::ImageCache will process each record of this list.
1002              
1003             =item source_url_key
1004              
1005             - data key containing the URL of source image.
1006              
1007             Contain the name of key of data object containing the source image reference.
1008              
1009             =back
1010              
1011             =head2 Optional parameters
1012              
1013             =over
1014              
1015             =item dest_url_key
1016              
1017             - data key for storing URL of image in cache.
1018              
1019             Optional parameter cause image name in cache will be a MD5 Base64
1020             digest of source image path where C<=> character removed, C<\> and C<+>
1021             translated to C<_> and C<-> simultaniosely.
1022              
1023             To get cached image name
1024              
1025             =item size
1026              
1027             - Prefered image size may set as C equal to C parameter
1028             of Image::Magick module to pass it dirrectly to Image::Magick Scale function.
1029              
1030             Other way to set the image size is set a width and height keys to preffered
1031             values.
1032              
1033             If one of image dimension is not defined then corresponding parameter of
1034             original image will be used.
1035              
1036             This way, image will be resized with same aspect ratio (same proportions) to
1037             the original image if C parameter present.
1038              
1039             Image width and height will be resized exactly to given size if
1040             C parameter not present.
1041              
1042             Parameter C has higher priority and other parameters has no effects
1043             if C peresent.
1044              
1045             For example.
1046              
1047             # Size 320x200 as geometry settings
1048             %params = (size => {geometry => "320x200!"} );
1049              
1050             # Size 320x200 as dimensions settings
1051             %params = (size => {width => 320, height => 200} );
1052              
1053             # Fit size into 320x200 with saving image proportions
1054             %params = (
1055             size => {
1056             width => 320,
1057             height => 200,
1058             save_aspect_ratio => 1,
1059             }
1060             );
1061              
1062             =item autocreate
1063              
1064             - create or check cache content automaticaly.
1065              
1066             If non zero value present, cache directory will be created
1067             and images checking will be runned. Otherwithe you should run
1068             init() and check() methodes manualy.
1069              
1070             Existent cache directory will not be removed. You may do it
1071             manualy using remove_cache() methode.
1072              
1073             =item force_download
1074              
1075             - each image should be reloaded to cache and processed without
1076             dependance of source image modification time. Any conditions
1077             ignored.
1078              
1079             =item thumbnail
1080              
1081             - thumbnails creator configuration
1082              
1083             Some thubnails configuration parameters may be set for
1084             automatic thumbnails creation. This parameter should contain
1085             the reference to hash with thumbnails configuration parameters.
1086              
1087             Only C parameter is required. Other parameters are
1088             optional.
1089              
1090             =over
1091              
1092             =item path
1093              
1094             path where thumbnail images should be placed.
1095              
1096             =item url
1097              
1098             URL for access to thumbnails directory. Same way as C.
1099              
1100             =item url_key
1101              
1102             Data object key name where thumbnail URL should be stored.
1103              
1104             =item geometry
1105              
1106             Geometry string to set thumbnail images size in Image Magick geometry
1107             format. May be set as dimension ("320x200!") or as persent of actual
1108             size of cached image ("25%").
1109              
1110             Default value is "50%" the half of actual image size.
1111              
1112             =back
1113              
1114             =item useragent
1115              
1116             - configuration parameters hash for LWP::UserAgent
1117              
1118             =over
1119              
1120             =item agent
1121              
1122             Default value C
1123              
1124             =item env_proxy
1125              
1126             Default value 1
1127              
1128             =item keep_alive
1129              
1130             Default value 1
1131              
1132             =item timeout
1133              
1134             Default value 30
1135              
1136             =back
1137              
1138             For more information please follow to L
1139              
1140             =back
1141              
1142             =head1 SEE ALSO
1143              
1144             Specifics of List API can be found in
1145              
1146             L.
1147              
1148             For additional information please see
1149              
1150             L,
1151              
1152             L,
1153              
1154             L,
1155              
1156             Refer to L documentation for additional
1157             information about setting of image scaling parameters.
1158              
1159             Refer to L documentation for additional
1160             information about user agent parameters.
1161              
1162             =head1 BUGS
1163              
1164             Please, inform me about found bugs.
1165              
1166             =head1 AUTHORS
1167              
1168             The XAO::ImageCache package maintained by
1169             Konstantin Safronov . Specification by
1170             Andrew Maltsew
1171              
1172             =cut