#!/usr/bin/perl -w my $RCS_Id = '$Id: album.pl,v 1.106 2007/06/16 12:37:56 jv Exp $ '; # Author : Johan Vromans # Created On : Tue Sep 15 15:59:04 2002 # Last Modified By: Johan Vromans # Last Modified On: Sun Dec 28 16:07:01 2008 # Update Count : 3266 # Status : Unknown, Use with caution! ################ Common stuff ################ $VERSION = "1.50_08"; use strict; # Package or program libraries, if appropriate. # $LIBDIR = $ENV{'LIBDIR'} || '/usr/local/lib/sample'; # use lib qw($LIBDIR); # require 'common.pl'; # Package name. my $my_package = 'Sciurix'; # Program name and version. my ($my_name, $my_version) = $RCS_Id =~ /: (.+).pl,v ([\d.]+)/; # Tack '*' if it is not checked in into RCS. $my_version .= '*' if length('$Locker: $ ') > 12; my $creator = qq{Created with Album $::VERSION}; ################ Command line parameters ################ use Getopt::Long 2.13; # Command line options. my $import_exif = 0; my $import_dir; my $update = 0; # add new from large/import our $dest_dir = "."; # needs occasional 'local' my $info_file; my $linkthem = 1; # link orig to large, if possible my $clobber = 0; # overwrite medium/thumbnails my $mediumonly = 0; # only medium size (for web export) my $forcemedium = 0; # force medium size if large is smaller my $externalize_css = 0; # create external css files my $externalize_formats = 0; # create external format files my $select = 'default'; # select images my $verbose = 1; # verbose processing # These are left undefined, for set_defaults. Note: our, not my. our $index_columns; our $index_rows; our $thumb; our $medium; # medium size, between large and small our $album_title; our $caption; our $datefmt; our $icon; our $locale; our $lib_common; our $home_link; # These are not command line options. my $journal; # create journal my $encoding; # info_file encoding # Development options (not shown with -help). my $debug = 0; # debugging my $trace = 0; # trace (show process) my $test = 0; # test mode. # Process command line options. app_options(); # Post-processing. $trace |= ($debug || $test); $dest_dir =~ s;^\./;;; $import_dir =~ s;^\./;; if $import_dir; ################ Presets ################ use constant DEFAULTS => { info => "info.dat", title => "Photo Album", medium => 0, mediumsize => 915, thumbsize => 200, indexrows => 3, indexcols => 4, caption => "fct", captionmin => "f", dateformat => '%F', icon => 0, }; my $TMPDIR = $ENV{TMPDIR} || $ENV{TEMP} || '/usr/tmp'; my $picpat = qr{(?i:jpe?g|png|gif|nef)}; my $movpat = qr{(?i:mpe?g|mov|avi)}; my $xtrpat = qw{(?i:html?)}; my $suffixpat = qr{\.$picpat|$movpat}; my $xsuffixpat = qr{\.$picpat|$movpat|$xtrpat}; my %capfun = ('c' => \&c_caption, 'f' => \&f_caption, 's' => \&s_caption, 't' => \&t_caption, ); my $br = br(); # Max.number of clickable index numbers (should be odd). use constant IXLIST => 15; # Stylesheets version. my $css_major = 1; my $css_minor = 0; # Formats version. my $fmt_major = 1; my $fmt_minor = 0; # Helper programs my $prog_jpegtran = findexec("jpegtran"); my $prog_mplayer = findexec("mplayer"); my $prog_mencoder = findexec("mencoder"); ################ The Process ################ use File::Spec; use File::Path; use File::Basename; use Time::Local; use Image::Info; use Image::Magick; use Data::Dumper; use POSIX qw(locale_h strftime); use locale; # The files already there, if any. my $gotlist = new FileList; # The files in the import dir, if any. my $implist = new FileList; # The list of files, in the order to be processed. # This list is initialy filled from info.dat, and (optionally) updated # from the other lists. my $filelist = new FileList; # This is the list of all entries to be journalled (all images, plus # possible interspersed loose annotations). my @journal; # Load cached info, if possible. load_cache(); # Load image names and info from the info file, if any. # This produces the initial file list. load_info(); #print STDERR Data::Dumper->Dump([$filelist],[qw(filelist)]); # Load image names and info for files we already got. load_files() if -d d_large(); #print STDERR Data::Dumper->Dump([$gotlist],[qw(gotlist)]); # Load image names and info for files we can import. load_import() if $import_dir && -d $import_dir; #print STDERR Data::Dumper->Dump([$implist],[qw(implist)]); # Apply defaults to unset parameters. set_defaults(); # warn("date => ", strftime($datefmt, localtime(time)), "\n"); # Verify and update the file list. my $added = update_filelist(); # Perform selection. Normally, hidden entries are ignored. # Option --select=all overrides this. $filelist = $filelist->filter($select); #print STDERR Data::Dumper->Dump([$filelist],[qw(filelist)]); my $num_entries = $filelist->tally; print STDERR ("Number of entries = $num_entries", $added ? " ($added added)" : "", "\n") if $verbose > 1; die("Nothing to do?\n") unless $num_entries > 0; exit(0) if $test; # Clean up and create directories. if ( $clobber ) { rmtree([d_index(), d_medium()], $verbose > 1); rmtree([d_journal()], $verbose > 1); } mkpath([d_index(), d_large(), d_icons()], $verbose > 1); mkpath([d_medium()], $verbose > 1) if $medium; # Copy images in place, rotate if necessary, and create the thumbnails. prepare_images(); # Update cache. update_cache(); my $cache_update = 0; my $entries_per_page = $index_columns*$index_rows; my $num_indexes = int(($num_entries - 1) / $entries_per_page) + 1; my $fn = "img0000"; # Cleanup excess files. for ( 0 ) { my $excess = $fn++ . ".html"; unlink(d_medium($excess)); unlink(d_large($excess)) or last; } # Map file names to html pages. Start with 1 to match "image N of M". my @htmllist; for my $i ( 0 .. $num_entries-1 ) { $htmllist[$i] = $fn++ . ".html"; } # Cleanup excess files. for (my $i = $num_entries ; ; $i++ ) { my $excess = $fn++ . ".html"; unlink(d_medium($excess)); unlink(d_large($excess)) or last; } # Copy the button images over to the target directory. add_button_images(); # Init formats and stylesheets. init_formats(); init_stylesheets(); # Write the individual pages. write_image_pages(); # Write the index pages. write_index_pages(); # Write the journal. write_journal_pages(); # Create index & icon. create_master_index(); create_index_icon(); # Final update, if needed. update_cache() if $cache_update; exit 0; ################ Subroutines ################ # Image types. use constant T_JPG => 1; use constant T_MPG => 2; use constant T_VOICE => 3; # still image + sound # Pseudo types. use constant T_PSEUDO => 0; use constant T_TAG => -1; use constant T_ANN => -2; use constant T_REF => -3; # List of possible subdirs to process. my @subdirs; # Journal tags my %jnltags; # fjoin is used for generating file names. sub fjoin { File::Spec->catfile(@_); } # hjoin is used for generating html paths sub hjoin { join("/", @_); } # $fjoin will be dynamically switched depending on context. our $fjoin; INIT { $fjoin = \&fjoin } sub d_dest { unshift(@_, $dest_dir) unless $dest_dir eq "."; $fjoin->(@_); } sub d_index { unshift(@_, "index"); goto &d_dest; } sub d_large { unshift(@_, "large"); goto &d_dest; } sub d_medium { unshift(@_, "medium"); goto &d_dest; } sub d_journal { unshift(@_, "journal"); goto &d_dest; } sub d_up { unshift(@_, ".."); goto &d_dest; } sub d_destc { unshift(@_, $lib_common) if $lib_common; goto &d_dest; } sub d_icons { unshift(@_, "icons"); goto &d_destc; } sub d_css { unshift(@_, "css"); goto &d_destc; } sub d_fmt { unshift(@_, "formats"); goto &d_destc; } my %optcfg; # option set from config files sub setopt { no strict qw(refs); return if defined(${$_[0]}); print STDERR ("setopt $_[0] -> $_[1]\n") if $trace; ${$_[0]} = $_[1]; $optcfg{$_[0]} = 1; } sub parse_line { local ($_) = (@_); my $err = 0; if ( /^!?\s*(\S.*)/ ) { $_ = $1; if ( /^title\s+(.*)/ ) { setopt("album_title", $1); } elsif ( /^page\s+(\d+)x(\d+)/ ) { setopt("index_rows", $1); setopt("index_columns", $2); } elsif ( /^thumbsize\s*(\d+)/ ) { setopt("thumb", $1); } elsif ( /^mediumsize\s*(\d+)(!)?/ ) { setopt("medium", $1); $forcemedium = defined $2; } elsif ( /^medium\s*(-?\d+)?/ ) { setopt("medium", $1 || DEFAULTS->{mediumsize}); } elsif ( /^dateformat\s*(.*)/ ) { setopt("datefmt", $1); } elsif ( /^caption\s*(.*)/ ) { setopt("caption", $1); } elsif ( /^icon\s*(.*)/ ) { setopt("icon", defined($1) && length($1) ? $1 : 1); } elsif ( /^locale\s*(.*)/ ) { setopt("locale", $1); } elsif ( /^depth\s+(\d+)/ ) { # lib_common is used in the HTML, don't use fjoin. setopt("lib_common", join("/", ("..") x $1)); } elsif ( /^home\s+(.+)/ ) { setopt("home_link", $1); } else { warn("Unknown control: $_[0]\n"); $err++; } } else { warn("Invalid control: $_[0]\n"); $err++; } $err; } sub set_defaults { # Load settings from user files. my $sl; unless ( $sl = $ENV{ALBUMCONFIG} ) { $sl = ".albumrc"; $sl .= ":".$ENV{HOME}."/.albumrc" if $ENV{HOME}; } foreach my $cf ( split(/:/, $sl) ) { unless ( -f $cf ) { warn("$cf: $!\n") if $ENV{ALBUMCONFIG}; next; } open(my $fh, "<", $cf) || next; warn("parsing: $cf\n") if $trace; my $err = 0; while ( <$fh> ) { next if /^\s*#/; next unless /\S/; $err += parse_line($_); } close($fh); die("Errors in config file $cf, aborted\n") if $err; } # Finally, apply defaults if necessary. warn("apply defaults\n") if $trace; setopt("album_title", DEFAULTS->{title}); setopt("index_rows", DEFAULTS->{indexrows}); setopt("index_columns", DEFAULTS->{indexcols}); setopt("thumb", DEFAULTS->{thumbsize}); setopt("datefmt", DEFAULTS->{dateformat}); setopt("icon", DEFAULTS->{icon}); $medium = DEFAULTS->{mediumsize} if defined($medium) && !$medium || $mediumonly; $medium = 0 if defined($medium) && $medium < 0; # Caption values. setopt("caption", DEFAULTS->{( -s $info_file || $import_dir) ? "caption" : "captionmin" }); die("Invalid value for caption: $caption\n") unless $caption =~ /^[fsct]*$/i; $caption = lc($caption); if ( $locale ) { setlocale(LC_TIME, $locale); setlocale(LC_COLLATE, $locale); } if ( defined($lib_common) ) { $lib_common =~ s;/+$;;; } $lib_common ||= ""; } sub load_info { my %typemap = ( 'p' => T_JPG, 'm' => T_MPG, 'v' => T_VOICE ); # If an info has been supplied, it'd better exist. if ( $info_file ) { die("$info_file: $!\n") unless -s $info_file; } else { # Try default. $info_file = d_dest(DEFAULTS->{info}); unless ( -s $info_file ) { my $add_new; $add_new++ if $import_dir; my $add_src; $add_src++ if -d d_large(); print STDERR ("No ", d_dest(DEFAULTS->{info})); print STDERR (", adding images from ") if $add_src || $add_new; print STDERR (d_large()) if $add_src; print STDERR (" and ") if $add_src && $add_new; print STDERR ($import_dir) if $add_new; print STDERR ("\n"); return; } } my $err = 0; my $file; my $tag; my $fh = do { local *FH; *FH }; die("$info_file: $!\n") unless open($fh, "<", $info_file); warn("parsing: $info_file\n") if $trace; my $el; my %dirs; while ( <$fh> ) { chomp; # Detection of condig system for info_file. # Uses GNU Emacs syntax, e.g., # # blah -*- mode: album; coding: utf-8 -*- if ( $. == 1 && m/^\s*\# # start with # .* # arb -\*- # -*- (?:.*?;)* # things, must be ; terminated \s* # ws coding\s*:\s*([\w\d-]+) # coding: utf-8 \s* # ws (?:;.*)* # things, must be ; started -\*- # -*- /x ) { $encoding = $1; warn("using encoding $encoding for $info_file\n") if $trace; # Remember position, reopen and restart IO. my $pos = tell($fh); close($fh); open($fh, "<:encoding($encoding)", $info_file) or die("$info_file: $!\n"); seek($fh, $pos, 0); next; } next if /^\s*#/; next unless /\S/; if ( /^\s+/ && $el ) { $el->description($el->description . "\n" . $_); next; } if ( /^!\s*(\S.*)/ ) { $_ = $1; if ( /^tag\s*(.*)/ ) { $tag = $1; $tag =~ s/\s$//; $tag =~ s/\s+/ /g; } elsif ( /^subdirs\s*(.*)/ ) { foreach ( split(' ', $1)) { $dirs{$_}++; } } elsif ( /^journal\s*(.*)/ ) { if ( $filelist->tally ) { warn("\"!journal\" must precede image info\n"); $err++; } load_info_journal($err, $fh); return; } else { $err += parse_line("!".$_); } next; } ($file, $a) = $_ =~ /^(.+?$xsuffixpat)\s*(.*)/; ($file, $a) = $_ =~ /^([^\s]+)\s+(.*)/ unless defined($file); my $rotate; my $type = T_JPG; my $assc; while ( $a && $a =~ /^-(\w):(\S+)\s*(.*)/ ) { if ( lc($1) eq 'o' ) { $rotate = 90 * ($2 % 4); } elsif ( lc($1) eq 'i' ) { $assc = fjoin(basename($file), $2); unless ( -s $assc && -r _ ) { warn("$file (info): $assc [$!]\n"); undef $assc; } } elsif ( lc($1) eq 't' ) { $type = $typemap{lc($2)} or warn("$file (info): Illegal type: $2\n"), $err++; } $a = $3; } $el = new ImageInfo($file); $el->type($type); $el->description($a) if $a; $el->tag($tag) if $tag; $el->_rotation($rotate) if defined($rotate); if ( $file =~ /^(.+)\.$movpat$/i ) { $el->type(T_MPG); $el->assoc_name($1."s.jpg"); # associates still image } elsif ( $type == T_VOICE ) { (my $t = $file) =~ s/\.jpg$/.mp3/i; $el->assoc_name($t); } elsif ( $file =~ /.\.html?$/i ) { $type = T_REF; } elsif ( -d $file ) { $type = T_REF; my $f = $file; $file = fjoin($f, "index", "index0001.html"); $file = fjoin($f, "index.html") unless -s $file; } if ( $type == T_REF ) { for ( fjoin(dirname($file), "icon.jpg") ) { $assc = $_ if !defined $assc && -f $_; } $assc = d_icons("extern.jpg") unless defined $assc; $el->assoc_name($assc); $el->dest_name($file); $el->type($type); } $filelist->add($el); $dirs{$1} = 1 if $type != T_REF && $file =~ m;^(.+)[/\\][^/\\]+$;; } close($fh); die("Aborted\n") if $err; @subdirs = sort(keys(%dirs)); } sub load_info_journal { my $err = shift; my $fh = shift; #### WARNING: EXPERIMENTAL #### warn("parsing (journal mode)\n") if $trace; my %typemap = ( 'p' => T_JPG, 'm' => T_MPG, 'v' => T_VOICE ); my $tag; my $nexttag = 0; my $annotation = ""; my $tags = 0; my %dirs; local($/) = ""; # para mode while ( <$fh> ) { chomp; next if /^\s*#/; next unless /\S/; # Handle controls. if ( /^!\s*(\S.*)/ ) { $_ = $1; if ( /^tag\s*(.*)/ ) { $tag = $1; $tag =~ s/\s$//; $tag =~ s/\s+/ /g; if ( $tag !~ /\S/ ) { warn("Tag may not be empty\n"); $err++; next; } if ( exists($jnltags{$tag}) ) { warn("Tag \"$tag\" is not unique\n"); $err++; } $jnltags{$tag} = sprintf("%04d", ++$nexttag); my $el = new ImageInfo; $el->tag($tag); $el->type(T_TAG); push(@journal, $el); $tags++; } elsif ( /^subdirs\s*(.*)/ ) { foreach ( split(' ', $1)) { $dirs{$_}++; } } elsif ( /^journal\s*(.*)/ ) { if ( $filelist->tally ) { warn("\"!journal\" must precede image info\n"); $err++; } # Ignore. } else { $err += parse_line("!".$_); } next; } if ( /^\*\s*(.*)/s ) { $_ = $1; } else { my $el = new ImageInfo; $el->annotation($_); $el->tag($tag); $el->type(T_ANN); push(@journal, $el); next; } s/\s*\n\s+/ /g; my @a = split(/\n/, $_); $_ = shift(@a); my $annotation = join(" ", @a); my ($file, $a) = $_ =~ /^(.+?$xsuffixpat)\s*(.*)/; my $rotate; my $type = T_JPG; my $assc; while ( $a && $a =~ /^-(\w):(\S+)\s*(.*)/ ) { if ( lc($1) eq 'o' ) { $rotate = 90 * ($2 % 4); } elsif ( lc($1) eq 'i' ) { $assc = fjoin(basename($file), $2); unless ( -s $assc && -r _ ) { warn("$file (info): $assc [$!]\n"); undef $assc; } } elsif ( lc($1) eq 't' ) { $type = $typemap{lc($2)} or warn("$file (info): Illegal type: $2\n"), $err++; } $a = $3; } my $el = new ImageInfo($file); $el->type($type); $el->description($a) if $a; $el->tag($tag) if $tag; # $annotation ||= $a; if ( $annotation ) { $annotation =~ s/^\s+//; $annotation =~ s/\s+$//; $annotation =~ s/\s+/ /g; $el->annotation($annotation); } $el->_rotation($rotate) if defined($rotate); if ( $file =~ /^(.+)\.$movpat$/i ) { $el->type(T_MPG); $el->assoc_name($1."s.jpg"); # associates still image } elsif ( $type == T_VOICE ) { (my $t = $file) =~ s/\.jpg$/.mp3/i; $el->assoc_name($t); } elsif ( -d $file ) { $type = T_REF; my $f = $file; $file = fjoin($f, "index", "index0001.html"); $file = fjoin($f, "index.html") unless -s $file; } elsif ( $file =~ /.\.html?$/i ) { $type = T_REF; } if ( $type == T_REF ) { for ( fjoin(dirname($file), "icon.jpg") ) { $assc = $_ if !defined $assc && -f $_; } $assc = d_icons("extern.jpg") unless defined $assc; $el->assoc_name($assc); $el->dest_name($file); $el->type($type); } if ( $type > T_PSEUDO ) { my @a = ($annotation); my $pi = scalar(@journal) - 1; while ( $pi >= 0 ) { my $e = $journal[$pi]; last if $e->type != T_ANN; push(@a, $e->annotation); $pi--; } $el->annotation([@a]) if @a; } $filelist->add($el); push(@journal, $el) if !$a || $a !~ /^--/; $dirs{$1} = 1 if $type != T_REF && $file =~ m;^(.+)[/\\][^/\\]+$;; } close($fh); die("Aborted\n") if $err; @subdirs = sort(keys(%dirs)); $journal = $tags; # no tags -- no journal... } sub load_files { my $dh = do { local *DH; *DH; }; opendir($dh, d_large()) or die("Cannot opendir " . d_large() . ": $!\n"); my @files = sort grep { !/^\./ && /$suffixpat$/ } readdir($dh); closedir($dh); foreach my $dir ( @subdirs ) { opendir($dh, d_large($dir)) or die("Cannot opendir " . d_large($dir) . ": $!\n"); push(@files, map { "$dir/$_" } sort grep { !/^\./ && /$suffixpat$/ } readdir($dh)); closedir($dh); } while ( @files ) { my $f = shift(@files); next unless -f d_large($f); my $el = new ImageInfo(d_large($f)); $el->type(T_JPG); if ( $f =~ /^(.+)\.$picpat$/ ) { my $m = "$1.mp3"; if ( -s d_large($m) ) { $el->type(T_VOICE); $el->assoc_name($m); warn(d_large($f).": Changed to VOICE\n") if $verbose; } } elsif ( $f =~ /^(.+)\.$movpat$/i ) { $el->type(T_MPG); my $assoc = $1."s.jpg"; $el->assoc_name($assoc); if ( @files && $files[0] eq $assoc ) { shift(@files); warn(d_large($assoc).": Skipped still\n") if $verbose; } } $gotlist->add($el, $f); } } sub load_import { my $dh = do { local *DH; *DH; }; opendir($dh, $import_dir) or die("Cannot opendir $import_dir: $!\n"); my @files = sort grep { !/^\./ && /$suffixpat$/ } readdir($dh); closedir($dh); while ( @files ) { my $f = shift(@files); next unless -f fjoin($import_dir, $f); my $el = new ImageInfo(fjoin($import_dir, $f)); if ( $import_exif ) { shift(@files) if handle_exif($f, $files[0], $el); } else { $el->type(T_JPG); if ( $f =~ /^(.+)\.$movpat$/i ) { $el->type(T_MPG); $el->assoc_name($1."s.jpg"); } $implist->add($el, $f); } } } sub handle_exif { my ($file, $next, $el) = @_; # Sony DSC-V1 produces the following files: # DSC0nnnn.JPG still image # DSC0nnnn.JPE mail mode image* # DSC0nnnn.MPG voice mode image* # DSC0nnnn.TIF uncompressed image* # CLP0nnnn.GIF clip motion file # CLP0nnnn.HTM clip motion file index # MBL0nnnn.GIF clip motion file, mobile mode # MBL0nnnn.HTM clip motion file index, mobile mode # MOV0nnnn.MPG movie # Files marked with * have a normal still image associated. # Normal still image. if ( $file =~ /^(.{4})(\d{4})\.($picpat)$/i ) { my ($type, $seq, $ext) = ($1, $2, $3); my $fd = $el->DateTime || ""; if ( $fd =~ /(\d\d\d\d):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)/ && $2 != 0) { my $time = timelocal($6,$5,$4,$3,$2-1,$1); my $new = "$1$2$3$4$5$6$seq"; my $ii = cache_entry("$new.$ext"); if ( $ii && !$ii->orig_name ) { $ii->orig_name(fjoin($import_dir, $file)); } $el->type(T_JPG); $el->dest_name("$new.$ext"); $el->timestamp($time); $file = "$new.$ext"; cache_entry($file, $el) unless $ii; } else { warn(fjoin($import_dir, $file).": Missing or unparsable file date [$fd]\n") if $verbose; $el->type(T_JPG); } if ( $next && $next eq "$type$seq.mpg" ) { warn(fjoin($import_dir, $file).": Changed to VOICE\n") if $verbose; $el->type(T_VOICE); (my $t = $file) =~ s/\.jpg$/.mp3/i; $el->assoc_name($t); $implist->add($el); return 1; } $implist->add($el); } # MPEG movie. elsif ( $file =~ /^(.{4})(\d{4})\.($movpat)$/i ) { my ($type, $seq, $ext) = ($1, $2, $3); # We have to trust the file date... my $time = $el->timestamp; my @tm = localtime($time); my $new = sprintf("%04d%02d%02d%02d%02d%02d$seq", 1900+$tm[5], 1+$tm[4], @tm[3,2,1,0]); my $ii = cache_entry("$new.$ext"); if ( $ii && !$ii->orig_name ) { $ii->orig_name(fjoin($import_dir, $file)); } $el->type(T_MPG); $el->dest_name("$new.$ext"); $el->assoc_name($new."s.jpg"); $implist->add($el, "$new.$ext"); $file = "$new.$ext"; cache_entry($file, $el) unless $ii; } # Assume ordinary JPEG or some picture. elsif ( $file =~ /^.*$picpat$/) { $el->type(T_JPG); $el->orig_name(fjoin($import_dir, $file)); $el->dest_name($file); $implist->add($el, $file); } # Assume ordinary MPEG or some movie. elsif ( $file =~ /^(.*)($movpat)$/) { $el->type(T_MPG); $el->orig_name(fjoin($import_dir, $file)); $el->dest_name($file); $el->assoc_name($1."s.jpg"); $implist->add($el, $file); } return 0; } sub update_filelist { my $todo = new FileList; my $el; my %seen; my $missing; my $prev; foreach $el ( $filelist->entries ) { my $f = $el->dest_name; $seen{$f}++; print STDERR ("todo[inf]: $f") if $trace; my $entry = $gotlist->byname($f); if ( $entry ) { print STDERR (" -- got") if $trace; } elsif ( $entry = $implist->byname($f) ) { print STDERR (" -- imp") if $trace; } elsif ( $el->type == T_REF ) { $entry = $el; print STDERR (" -- ref") if $trace; } if ( $entry ) { if ( $el->description =~ /^--(?:$|\s+)(.*)/ ) { $entry->description($1); $entry->hidden(1); print STDERR (" (hidden)") if $trace; } else { $entry->description($el->description); } # Copy properties from info. $entry->tag($el->tag); $entry->annotation($el->annotation); $entry->_rotation($el->_rotation); # Add and create prev/next links. $entry->prev($prev->seq) if $prev; $todo->add($entry); $prev->next($entry->seq) if $prev; print STDERR ("\n") if $trace; } else { if ( $trace ) { print STDERR ("\n"); } else { unless ( $el->description =~ /^--($|\s)/ ) { print STDERR ("todo[inf]: $f -- missing\n"); } } unless ( $el->description =~ /^--($|\s)/ ) { $missing++; } } $prev = $entry if $entry && $entry->type != T_REF; } die("Aborted!\n") if $missing; unless ( $filelist->tally == 0 || $update ) { $filelist = $todo; return 0; } my $newinfo = ""; my $date; my $new; foreach $el ( $gotlist->entries ) { my $f = $el->dest_name; print STDERR ("todo[got]: $f") if $trace; if ( $seen{$f}++ ) { print STDERR (" -- seen\n") if $trace; next; } print STDERR (" -- added\n") if $trace; my $nd = ""; if ( $f =~ /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ ) { my $tl = timelocal($6,$5,$4,$3,$2-1,$1); $nd = strftime($datefmt, localtime($tl)); $el->timestamp($tl); } if ( !defined($date) || $nd ne $date ) { $newinfo .= "\n!tag $nd\n"; $newinfo .= "\n" if $journal; $date = $nd; } $newinfo .= defined $journal ? "* $f\n\n" : "$f\n"; $el->tag($date) if $date; $el->prev($prev->seq) if $prev; $todo->add($el); $prev->next($el->seq) if $prev; $prev = $el unless $el->type == T_REF; push(@journal, $el) if $journal; $new++; } foreach $el ( $implist->entries ) { my $f = $el->dest_name; print STDERR ("todo[imp]: $f") if $trace; if ( $seen{$f}++ ) { print STDERR (" -- seen\n") if $trace; next; } print STDERR (" -- added\n") if $trace; my $nd = ""; my $time = $el->timestamp; if ( $time ) { $nd = strftime($datefmt, localtime($time)); } if ( !defined($date) || $nd ne $date ) { $newinfo .= "\n!tag $nd\n"; $newinfo .= "\n" if $journal; $date = $nd; } $newinfo .= (defined $journal ? "* " : "") . "$f " . ($el->rotation ? ("-O:".int($el->rotation/90)." ") : "") . ($el->type == T_VOICE ? "-T:V " : "") . ($journal ? " \n\n" : " \n"); $el->tag($date) if $date; $el->prev($prev->seq) if $prev; $todo->add($el); $prev->next($el->seq) if $prev; $prev = $el unless $el->type == T_REF; push(@journal, $el) if $journal; $new++; } $filelist = $todo; unless ( $new ) { # nothing to add warn("No new images imported\n") if $verbose > 1; return 0; } return $new if $test; unless ( -w $info_file ) { warn("$info_file: Cannot update (". (-e _ ? "no write access" : "does not exist") . ")\n") if $verbose; return $new; } my $infosize = -s $info_file; # Append new info. warn("Updating $info_file\n") if $verbose > 1; my $fh = do { local *F; *F }; open($fh, $encoding ? ">>:encoding($encoding)" : ">>", $info_file) || die("$info_file: $!\n"); unless ( $infosize ) { print $fh ("# album control file created by Album $::VERSION, ". localtime(time), "\n\n"); print $fh ("!title $album_title\n") if $album_title; if ( $medium && !$optcfg{"medium"} ) { print $fh ($medium != DEFAULTS->{mediumsize} ? "!mediumsize $medium\n" : "!medium\n"); } print $fh ("!thumbsize $thumb\n") if !$optcfg{"thumb"} && $thumb != DEFAULTS->{thumbsize}; print $fh ("!page ${index_rows}x${index_columns}\n") if !$optcfg{index_rows} && $index_rows != DEFAULTS->{indexrows} || !$optcfg{index_columns} && $index_columns != DEFAULTS->{indexcols}; print $fh ("!caption $caption\n") if !$optcfg{"caption"} && $caption ne DEFAULTS->{caption}; } print $fh ("\n# New entries added by Album $::VERSION, ". localtime(time), "\n", $newinfo, "\n"); close($fh); $new; } sub prepare_images { my $ddot = 0; my $tdot = 0; my $fmt = "[%" . length($filelist->tally) . "d]\n"; my $msgfile; my $msg = sub { return unless $verbose > 1; if ( $verbose > 2 ) { if ( $msgfile ) { print STDERR ("$msgfile: "); $msgfile = ""; } print STDERR (@_ ? @_ : "OK\n"); } unless ( @_ ) { unless ( $msgfile ) { print STDERR ("OK\n"); return; } print STDERR ("."); $tdot++; if ( ++$ddot >= 50 ) { printf STDERR ($fmt, $tdot); $ddot = 0; } return; } printf STDERR ($fmt, $tdot) if $ddot; $ddot = 0; if ( $msgfile ) { print STDERR ("$msgfile: "); $msgfile = ""; $tdot++; } print STDERR (@_); }; my $image; my $i_large; my $readimage = sub { my ($file) = (@_, $i_large); $image = new Image::Magick; my $t = $image->Read($file); warn("read($file): $t\n") if $t; #$image->Profile(name => "*", profile => undef); }; my $resize = sub { my ($n) = @_; my ($origx, $origy) = $image->Get(qw(width height)); return unless $forcemedium || $origx > $n || $origy > $n; my $ratio = $origx > $origy ? $origx / $n : $origy / $n; my $t = $image->Resize(width => $origx/$ratio, height => $origy/$ratio); warn("resize: $t\n") if $t; }; foreach my $el ( $filelist->entries ) { $msg->(), next unless $el->type > 0; my $file = $el->dest_name; $msgfile = $file; $image = undef; # Check for directory names, e.g. f01/p01.jpg. my $dn = dirname($file); if ( $dn && $dn ne "." ) { # we have a dir name. mkpath([d_index($dn), d_large($dn)], 1); mkpath([d_medium($dn)], 1) if $medium; } $i_large = d_large($file); my $movie = $el->type == T_MPG; # Copy the file into place. if ( ! -s $i_large && $el->orig_name ) { my $i_src = $el->orig_name; my $time = $el->timestamp; if ( $movie ) { # Need copy? my $copyit = !$linkthem || (($el->rotation || $el->mirror) && $prog_mencoder); # Try to link. if ( !$copyit ) { $msg->("link "); if ( link($i_src, $i_large) == 1 ) { # Ok, done. } else { # Need copy. unlink($i_large); # just in case $msg->("[copy] "); $copyit = 1; } } else { $msg->("copy"); } # Need copy? if ( $copyit ) { if ( $prog_mencoder ) { $msg->("/rotate (be patient)") if $el->rotation; $msg->(" "); # Currently. movies have a bad ugly copy routine... copy_mpg($i_src, $i_large, $time, $el->rotation, $el->mirror); } else { $msg->(" [no rotation]") if $el->rotation; $msg->(" "); copy($i_src, $i_large, $time); } } } elsif ( $el->rotation || $el->mirror ) { $msg->("copy"); $msg->("/rotate") if $el->rotation; $msg->("/mirror") if $el->mirror; $msg->(" "); # Use jpegtran to rotate jpg files. if ( ($el->file_ext || "") eq "jpg" && $prog_jpegtran ) { my $cmd = "$prog_jpegtran -copy all -rotate " . $el->rotation . " "; $cmd .= $el->mirror eq 'h' ? "-transpose " : "-transverse " if $el->mirror; $cmd .= "-outfile " . squote($i_large) . " " . squote($i_src); my $t = `$cmd 2>&1`; $msg->($t) if $t; utime($time, $time, $i_large); } # Otherwise, let Image::Magick handle it. else { $readimage->($i_src); $image->Rotate(); if ( $el->mirror ) { $image->Flip if $el->mirror eq 'h'; $image->Flop if $el->mirror eq 'v'; } my $t = $image->Write($i_large); $msg->($t) if $t; utime($time, $time, $i_large); } } elsif ( $linkthem ) { $msg->("link "); unless ( link($i_src, $i_large) == 1 ) { unlink($i_large); # just in case $msg->("[copy] "); copy($i_src, $i_large, $time); } } else { $msg->("copy "); copy($i_src, $i_large, $time); } if ( $el->type == T_VOICE ) { $msg->("sound "); copy_voice($i_src, d_large($el->assoc_name), $time); } } if ( $movie ) { $movie = $file; $file = $el->assoc_name; $i_large = d_large($file); unless ( -s $i_large ) { $msg->("still "); $image = still($el); } } my $i_medium = d_medium($file); my $i_small = d_index($file); if ( $medium && ! -s $i_medium ) { $readimage->() unless $image; $msg->("medium "); $resize->($medium); my $t = $image->Write($i_medium); $msg->($t) if $t; } $el->medium_size(-s $i_medium) if $medium && !$movie; if ( ! -s $i_small ) { $readimage->() unless $image; $msg->("thumbnail "); $resize->($thumb); my $t = $image->Write($i_small); $msg->($t) if $t; } $msg->(); # flush } printf STDERR ($fmt, $tdot) if $ddot && $tdot; } ################ Formats ################ my %format_for; # <; close($fh); } elsif ( $externalize_formats ) { unless ( $did ) { my $fdir = d_fmt(""); $fdir =~ s/\/+$//; unless ( -d $fdir ) { print STDERR ("mkdir $fdir\n"); mkdir(d_fmt("")); } } print STDERR ("Creating formats: ") if $verbose > 1 && !$did++; print STDERR ("$req ") if $verbose > 1; open (my $fh, '>', $fmt) || die("$fmt: $!\n"); print {$fh} $data; close($fh); } $data =~ s/^([ \t]+)/detab($1)/gem; $data; }; # Format for main index page. # # Variables: # # $link # $title # $index # $version $format_for{main} = $load->("main.fmt", heredoc(<<' EOD', 4)); $title $css

$title

Created with Album $version

EOD # Format for index pages (mostly). # # Variables: # # $title # $ltop # $rtop # $vbuttons / $hbuttons # $jscript # $contents $format_for{index} = $load->("index.fmt", heredoc(<<' EOD', 4)); $title $css $jscript

$ltop

$rtop

$vbuttons $contents
EOD # Format for image pages (mostly). # # Variables: # # $title # $ltop # $rtop # $vbuttons / $hbuttons # $jscript # $image # $lbot # $rbot $format_for{image} = $load->("image.fmt", heredoc(<<' EOD', 4)); $title $css $jscript

$ltop

$rtop

$vbuttons $image

$lbot

$rbot

EOD $format_for{large} = $load->("large.fmt", $format_for{image}); $format_for{medium} = $load->("medium.fmt", $format_for{image}); # Format for journal pages (mostly). # # Variables: # # $title # $tag # $vbuttons / $hbuttons # $journal # $jscript $format_for{journal} = $load->("journal.fmt", heredoc(<<' EOD', 4)); $title $css $jscript $journal

$tag

$hbuttons
$hbuttons
EOD print STDERR ("\n") if $did; } sub process_fmt { my ($fmt, %map) = @_; # THIS DOES ONLY ONE SUBST PER LINE $fmt =~ s/^(.*?)\$(\w+)\b/$1.indent($map{$2}, length($1))/gme; $fmt; } ################ Style Sheets ################ my %css_for; sub init_stylesheets { my $css_fontfam = "font-family: Verdana, Arial, Helvetica"; my $WHITE = "#FFFFFF"; my $BLACK = "#000000"; my $RED = "#FF0000"; my $LGREY = "#E0E0E0"; my $MGREY = "#D0D0D0"; my $DGREY = "#C0C0C0"; my $DDGREY = "#B0B0B0"; my $BLUE = "#0000FF"; # Grey variants for index table borders. my $GR245 = "#F5F5F5"; my $GR232 = "#E8E8E8"; my $GR124 = "#7C7C7C"; my $GR114 = "#727272"; my $helper = $thumb + 4; my $did = 0; my $load = sub { my ($req, $data) = @_; my $css = d_css($req.".css"); if ( -r $css ) { my $major = $css_major; my $minor = $css_minor; my $orig; if ( open(my $orig, "<", d_fmt("$req.fmt")) ) { my $line = <$orig>; close($orig); if ( $line =~ m;/\*\s*album-fmt-version:\s*(\d+)\.(\d+)\s*\*/;i ) { ($major, $minor) = ($1, $2); } } # Check stylesheet compatibility. open($orig, "<", $css); my $line = <$orig>; close($orig); if ( $line =~ m;/\*\s*album-css-version:\s*(\d+)\.(\d+)\s*\*/;i ) { if ( $1 == $major ) { return ""; } } print STDERR "\n" if $did; die(heredoc(<<" EOD", 8)); ************************************************************************* Existing style sheet $req.css is not compatible with this version. It has probably been created by an older version of this program, or it has been modified manually. If you did not change any style sheets, just remove the css directory and try again. If you did modify the style sheets move them away to a backup location, run the program with '--extcss', and apply your changes to the new style sheets. ************************************************************************* EOD } elsif ( $externalize_css ) { unless ( $did ) { my $fdir = d_css(""); $fdir =~ s/\/+$//; unless ( -d $fdir ) { print STDERR ("mkdir $fdir\n"); mkdir(d_css("")); } } print STDERR ("Creating stylesheets: ") if $verbose > 1 && !$did++; print STDERR ("$req ") if $verbose > 1; open (my $fh, '>', $css) || die("$css: $!\n"); print { $fh } $data; close($fh); } $data =~ s/^([ \t]+)/detab($1)/gem; $data; }; my $css_for_common = heredoc(<<" EOD", 0); body { $css_fontfam; font-size: 80%; text: $BLACK; } a:link { color: $BLACK; text-decoration: none; } a:visited { color: $BLACK; text-decoration: none; } a:active { color: $RED; text-decoration: none; } img.image { border: 2px solid $BLACK; } img.button { border: 0; vertical-align: top; } table.vb { border: 0; border-spacing: 0 0; } table.vb td { padding: 0 0 0 0; } table.hb { border: 0; border-spacing: 0 0; } table.hb td { padding: 0 0 0 0; } EOD my $css_for_ipage = heredoc(<<" EOD", 0); $css_for_common body { background: $DGREY; } td { font-size: 80%; } p.hdl, p.hdr { font-size: 140%; font-weight: bold; margin-top: 0; margin-bottom: 0; } p.ftl, p.ftr { font-size: 80%; margin-top: 0; margin-bottom: 0; } td.topleft { text-align: left; vertical-align: top; } td.topright { text-align: right; vertical-align: top; } td.image { text-align: center; vertical-align: top; } td.botleft { text-align: left; vertical-align: top; } td.botright { text-align: right; vertical-align: top; } td.vbuttons { vertical-align: top; } EOD $css_for{index} = $load->("index", heredoc(<<" EOD", 4)); /* ALBUM-CSS-VERSION: ${css_major}.${css_minor} */ $css_for_ipage a.info { position: relative; z-index: 24; background-color: $LGREY; color: $BLACK; text-decoration:none; } a.info:hover { z-index: 25; background-color: $LGREY; } a.info span { display: none; } a.info:hover span { display: block; position: absolute; top: 2em; left: 2em; width: 25em; border: 0px; background-color: $MGREY; color: $BLACK; text-align: center; } table.outer { background: $MGREY; border-collapse: separate; border-width: 2px; /* border=2 */ border-style: solid; border-color: $GR232 $GR114 $GR114 $GR232; border-spacing: 3px; /* cellspacing = 3 */ } table.outer tr { background: $LGREY; } table.outer td { border-width: 1px; border-style: solid; border-color: $GR124 $GR245 $GR245 $GR124; } table.inner { /* need a width otherwise we cannot center it */ width: ${helper}px; border: outset 0px; } table.inner td { border: inset 0px; padding: 0 0 0 0; } p.hdr { font-size: 140%; font-weight: bold; margin-top: 0; margin-bottom: 0; } p.hdr a:link { color: $BLACK; text-decoration: underline; } p.hdr a:visited { color: $BLACK; text-decoration: underline; } p.hdr a:hover { color: $RED; text-decoration: underline; } td.vimage { vertical-align: top; } td.oimg { text-align: center; vertical-align: bottom; } td.iimg { text-align: center; } td.itxt { text-align: center; } img.thumb { border: 0; } EOD my $css_for_image = heredoc(<<" EOD", 4); /* ALBUM-CSS-VERSION: ${css_major}.${css_minor} */ $css_for_ipage a.info { position: relative; z-index: 24; background-color: $DGREY; color:$BLACK; text-decoration:none; } a.info:hover { z-index: 25; background-color: $DGREY; } a.info span { display: none; } a.info:hover span { display: block; position: absolute; top:2em; left: 2em; width: 15em; border: 0px; background-color: $MGREY; color: $BLACK; text-align: center; } EOD $css_for{large} = $load->("large", $css_for_image); $css_for{medium} = $load->("medium", $css_for_image); $css_for{journal} = $load->("journal", heredoc(<<" EOD", 4)); /* ALBUM-CSS-VERSION: ${css_major}.${css_minor} */ $css_for_common body { font-size: 100%; background: $WHITE; } td { font-size: 100%; } p.hdl { font-size: 140%; font-weight: bold; margin-left: 0.1in; margin-top: 0.1in; margin-bottom: 0.1in; } table.outer { width: 600px; border-spacing: 10px; } tr.grey { background: $DGREY; } table.outer td.twocol { vertical-align: top; text-align: left; } table.outer td.jl { vertical-align: top; text-align: left; } table.outer td.jr { width: ${thumb}px; vertical-align: top; text-align: center; background: $LGREY; } table.outer td.buttons { vertical-align: middle; text-align: right; padding-right: 0.1in; } EOD $css_for{main} = $load->("main", heredoc(<<" EOD", 4)); /* ALBUM-CSS-VERSION: ${css_major}.${css_minor} */ body { $css_fontfam; font-size: 80%; background: $LGREY; background-image: url("icons/bg.jpg"); background-repeat: no-repeat; background-position: 10% 60%; } p.ftr { padding-left: 10%; padding-top: 40%; font-size: 80%; text-align: left; color: $DDGREY; } p.indextitle { padding-left: 10%; font-size: 500%; font-weight: bold; color: $WHITE; } p.indextitle a { text-decoration: none; color: $WHITE; } EOD } sub css_for { my ($type) = shift(@_); defined(my $css = $css_for{$type}) or die("PROGRAM ERROR: css_for($type)"); return qq{} unless $css; qq{}; } ################ Helpers for Image/Index/Journal pages ################ sub jscript { my (%nav) = @_; my $next = $nav{next}; my $prev = $nav{prev}; my $up = $nav{up}; my $down = $nav{down}; my $idx = $nav{idx}; my $jnl = $nav{jnl}; my $js = heredoc(<<" EOD", 4); EOD $js; } sub button($$;$$) { my ($tag, $link, $level, $active) = @_; my $Tag = ucfirst($tag); local $fjoin = \&hjoin; $level = 0 unless defined $level; $active = 1 unless defined $active; $tag .= "-gr" unless $active; my @path; push(@path, ("..") x $level) if $level; push(@path, d_icons("$tag.png")); my $path = $fjoin->(@path); my $b = img($path, class => "button", alt => "[$Tag]"); $active ? "$b" : $b; } sub hbuttons { my (@b) = @_; # When using a , it seems to be impossible to get it # aligned properly on the journal pages. Apparently, # ". "\n"; }; $app->("Date", $v) if $v = $el->DateTime; my $t = $el->ExposureTime || 0; if ( $t && $t <= 0.5 ) { $t = "1/".int(0.5 + 1/$t)."s"; } $app->("Exposure", join(" ", $el->ExposureMode || "", $el->ExposureProgram || "", $t)); $app->("Aperture", sprintf("%.1f", $v)) if $v = $el->FNumber; if ( $v = $el->FocalLength ) { if ( $el->Model eq "DSC-V1" ) { $v .= sprintf("mm (%.1fmm equiv.)", $v*4.857); } else { $v .= "mm"; } $app->("Focal length", $v); } $app->("ISO", $v) if $v = $el->ISOSpeedRatings; $app->("Flash", $v) if ($v = $el->Flash) && $v ne "Flash did not fire"; $app->("Metering", $v) if $v = $el->MeteringMode; $app->("Scene", $v) if $v = $el->SceneCaptureType; $app->("Camera", join(" ", $v, $el->Model)) if $v = $el->Make; } #### Caption helpers. sub f_caption($) { my ($el) = @_; my $s = htmln($el->type == T_REF ? $el->orig_name : $el->dest_name); if ( $el->Make ) { $s = " $s ". "
semantics cannot be implemented using CSS. "
". join("", map { $_ } @b). "
"; } sub vbuttons { my (@b) = @_; "". join("", map { ""} @b). "
$_
"; } sub ixname($;$) { my ($n, $rel) = @_; my $f = sprintf("index%04d.html", $n + 1); return $f if $rel; d_index($f); } # To aid XHTML compliancy. sub br { "
" } # Pseudo-smart approach to creating paired single/double quotes. # Note that the (s-|s\s|t\s) case is specific to the dutch language, # but probably won't harm other languages... # Yes, you'll get stupid results with input like rock'n'roll. sub fixquotes($) { my ($t) = @_; # HTML::Entities will already have turned " into " -- undo. $t =~ s/\"/"/g; while ( $t =~ /^([^"]*)"([^"]+)"(.*)/s ) { $t = $1 . "“" . $2 . "”" . $3; } $t =~ s/"/"/g; # HTML::Entities will already have turned ' into ' -- undo. $t =~ s/\'/'/g; while ( $t =~ /^(.*?)'(s-|s\s|t\s)(.*)/s ) { $t = $1 . "'" . $2 . $3; } while ( $t =~ /^([^']*)'([^']+)'(.*)/s ) { $t = $1 . "‘" . $2 . "’" . $3; } $t; } # Escape sensitive characters in HTML. # Two variants: one using HTML::Entities, the other a dumber stub. # If HTML::Entities is available, it will be used. sub html($) { eval { require HTML::Entities; if ( !$encoding or $encoding =~ /^(iso-?8859-?15|latin-?9)$/i ) { # Apply Latin-9 instead of Latin-1. no warnings 'once'; for ( \%HTML::Entities::char2entity ) { $_->{chr(0204)} = '€'; $_->{chr(0246)} = 'Š'; $_->{chr(0250)} = 'š'; $_->{chr(0264)} = 'Ž'; $_->{chr(0270)} = 'ž'; $_->{chr(0274)} = '&OE;'; $_->{chr(0275)} = '&oe;'; $_->{chr(0276)} = 'Ÿ'; } } no warnings 'redefine'; *html = sub($) { my ($t) = @_; return '' unless $t; $t = HTML::Entities::encode($t); fixquotes($t); }; }; if ( $@ ) { if ( $encoding ) { warn("WARNING: Module HTML::Entities not found.\n". "Encoding of the HTML files may be incorrect!\n"); } no warnings 'redefine'; *html = sub($) { my ($t) = @_; return '' unless $t; $t =~ s/&/&/g; $t =~ s//>/g; fixquotes($t); }; } goto &html; } sub htmln($) { # Escape HTML sensitive characters, and turn newlines into
. my $t = html(shift); return '' unless $t; $t =~ s/\n+/$br/go; $t; } sub indent($$) { # Shift contents to the right so it fits pretty. my ($t, $n) = @_; $n = " " x $n; return $n unless $t; $t = detab($t); $t =~ s/\n+$//; $t =~ s/\n/\n$n/g; $t; } sub img($%) { my ($file, %atts) = @_; my $ret = ""; } #### Size helpers. sub bytes($) { my $t = shift; return $t . "b" if $t < 10*1024; return ($t >> 10) . "kb" if $t < 10*1024*1024; ($t >> 20) . "Mb"; } sub size_info($;$) { my ($el, $med) = @_; return unless $el->width; my $ret = ""; $ret .= $el->width . "x" . $el->height if $el->width; for ( $med ? $el->medium_size : $el->file_size ) { next unless $_; $ret .= "," if $ret; $ret .= bytes($_); } $ret; } #### EXIF helpers. sub restyle_exif($) { my ($el) = @_; my $ret = ""; my $v; my $app = sub { $ret .= "
".htmln($_[0])."".htmln($_[1])."
\n". restyle_exif($el) . "
\n". ""; } $s; } sub s_caption($) { my ($el) = @_; size_info($el, $medium); } sub t_caption($) { my ($el) = @_; $el->tag ? htmln($el->tag) : ""; } sub c_caption($) { my ($el) = @_; my $t = $el->description || ""; $t =~ s/\n.*//; htmln($t); } #### Misc. sub update_if_needed($$) { my ($fname, $new) = @_; # Do not overwrite unless modified. if ( -s $fname && -s _ == length($new) ) { local($/); my $hh = do { local *F; *F }; my $old; open($hh, "<", $fname) && ($old = <$hh>) && close($hh); if ( $old eq $new ) { return 0; } } my $fh = do { local *F; *F }; open($fh, ">", $fname) or die("$fname (create): $!\n"); print $fh $new; close($fh); 1; } sub uptodate($$) { my ($type, $mod) = @_; if ( $mod ) { print STDERR ("(Needed to write ", $mod, " $type page", $mod == 1 ? "" : "s", ")\n"); } else { print STDERR ("(No $type pages needed updating)\n"); } } ################ Image Pages ################ sub write_image_pages { print STDERR ("Creating ", $num_entries, " image page", $num_entries == 1 ? "" : "s", "\n") if $verbose > 1; my $mod = 0; for my $el ( $filelist->entries ) { write_image_page($el, "large") && $mod++; write_image_page($el, "medium") && $mod++ if $medium; } uptodate("image", $mod) if $verbose > 1; } sub write_image_page { my ($el, $dir) = @_; if ( $el->type <= T_PSEUDO ) { warn("PSEUDO: ", Dumper($el)) unless $el->type == T_REF; return; } # Local scope... my $orig_dd = $dest_dir; local $dest_dir = "."; local $fjoin = \&hjoin; my $i = $el->seq - 1; my $file = $el->dest_name; my $rf = $file; # Try movie. my $movie = $el->type == T_MPG; if ( $movie ) { $file = $el->assoc_name; } my $tt = "$album_title: Image " . ($i+1); $tt .= " of " . $num_entries if $num_entries > 1; $tt = htmln($tt); my $it = htmln($el->description) || ""; my $next = ($el->next || $num_entries+1) - 1; my $prev = ($el->prev || 0) - 1; my %nav = (next => $next < $num_entries ? $htmllist[$next] : "", prev => $prev >= 0 ? $htmllist[$prev] : "", idx => d_up(ixname(int($i/$entries_per_page))), up => d_up(ixname(int($i/$entries_per_page)))); my @b = ( ($dir eq "large" && $medium) ? button("medium", d_up(d_medium($htmllist[$i])), 1, 1) : button("index", d_up(ixname(int($i/$entries_per_page))), 1, 1), button("first", $htmllist[0], 1, $i > 0), button("prev", $htmllist[$prev] || "", 1, $prev >= 0), button("next", $htmllist[$next] || "", 1, $next < $num_entries), button("last", $htmllist[-1], 1, $i < $num_entries-1)); if ( $journal && exists $jnltags{$el->tag} ) { my $page = d_up(d_journal("jnl" . $jnltags{$el->tag} . ".html#img".sprintf("%04d", $i+1))); push(@b, button("journal", $page, 1, 1)); $nav{jnl} = $page; } if ( $el->type == T_VOICE ) { my $sound = $el->assoc_name; push(@b, button("sound", d_up(d_large($sound)), 1, 1)); } my $imglink; if ( $dir eq "medium" ) { if ( $mediumonly ) { $imglink = img($file, alt => "[Image]", class => "image"); } elsif ( $movie ) { $imglink = "" . img($file, alt => "[Movie]", class => "image") . ""; $nav{down} = d_up(d_large($el->dest_name)); } else { $imglink = "" . img($file, alt => "[Image]", class => "image") . ""; $nav{down} = d_up(d_large($htmllist[$i])); } } else { if ( $movie ) { $imglink = "" . img($file, alt => "[Movie]", class => "image") . ""; } else { $imglink = img($file, alt => "[Image]", class => "image"); } $nav{up} = d_up(d_medium($htmllist[$i])); } my $auxright = htmln($el->dest_name); my $s = size_info($el); $auxright .= " ($s)" if $s; $auxright .= "   $creator" if $creator; my $auxleft = htmln($el->tag || ""); my $it2 = $it; if ( $el->Make ) { # EXIF info $it2 = "" . ($it || " ") . "" . "\n" . restyle_exif($el) . "
\n" . "
"; } my $tt2 = $tt; if ( $dir eq "medium" && $el->annotation ) { my @a = UNIVERSAL::isa($el->annotation, "ARRAY") ? @{$el->annotation} : ($el->annotation); my $t = ""; foreach ( reverse(@{$el->annotation}) ) { next unless $_; my $x = $_; # copy $x = html($x) unless $x =~ /^\n" if $t; $t .= $x; } $tt2 = "" . $tt . "" . "\n" . "" . "
$t
\n" . "
" if $t; } # Restore local scope. $fjoin = \&fjoin; $dest_dir = $orig_dd; update_if_needed(d_dest($dir, $htmllist[$i]), process_fmt($format_for{$dir}, title => $it || $tt, css => css_for($dir), dir => $dir, ltop => $it2, rtop => $tt2, hbuttons => hbuttons(@b), vbuttons => vbuttons(@b), jscript => jscript(%nav), image => $imglink, lbot => $auxleft, rbot => $auxright, )); } ################ Index Pages ################ sub write_index_pages { print STDERR ("Creating ", $num_indexes, " index page", $num_indexes == 1 ? "" : "s", "\n") if $verbose > 1; my $mod = 0; for my $i ( 0 .. $num_indexes-1 ) { write_index_page($i) && $mod++; } uptodate("index", $mod) if $verbose > 1; # Cleanup excess indices. for (my $i = $num_indexes ; ; $i++ ) { unlink(d_dest("index$i.html")) or last; } } sub write_index_page { my ($x) = @_; my $tt = $album_title.": Index"; # left title my $t = ""; # right (index select) my @b; # buttons my %nav; # Local scope... my $orig_dd = $dest_dir; local $dest_dir = "."; local $fjoin = \&hjoin; # Construct buttons and index selector. if ( $num_indexes > 1 ) { $nav{next} = ixname($x+1, 1) if $x < $num_indexes-1; $nav{prev} = ixname($x-1, 1) if $x > 0; if ( $lib_common ne "" ) { $nav{up} = join("/","..",$lib_common,"index.html"); } elsif ( $home_link ) { $nav{up} = join("/","..",$home_link); } push(@b, button("up", $nav{up}, 1, 1)) if $nav{up}; push(@b, button("first", ixname(0, 1), 1, $x > 0 ), button("prev", ixname($x-1, 1), 1, $x > 0 ), button("next", ixname($x+1, 1), 1, $x < $num_indexes-1), button("last", ixname($num_indexes-1, 1), 1, $x < $num_indexes-1)); $tt .= " " . ($x+1) . " of $num_indexes"; my @ixlist = ( 0..$num_indexes-1 ); if ( @ixlist > IXLIST ) { @ixlist = ( $x ); while ( @ixlist < IXLIST ) { push(@ixlist, $ixlist[-1]+1) if $ixlist[-1]+1 < $num_indexes; unshift(@ixlist, $ixlist[0]-1) if @ixlist < IXLIST && $ixlist[0] > 0; } } $t .= "...\n" if $ixlist[0]; foreach ( @ixlist ) { if ( $_ == $x ) { $t .= ($x+1) . "\n"; } else { my $el = $filelist->byseq(($_ * $index_rows * $index_columns) + 1); $t .= "tag ) { $t .= " title=\"$tag\""; } $t .= " href='" . ixname($_, 1) . "'>" . ($_+1) . "\n"; } } $t .= "...\n" if $ixlist[-1] < $num_indexes-1; } elsif ( $lib_common ) { push(@b, button("up", join("/","..",$lib_common,"index.html"), 1, 1)); $nav{up} = join("/","..",$lib_common,"index.html"); } my $first_in_row = $x * $entries_per_page; if ( $journal && exists $jnltags{$filelist->byseq($first_in_row+1)->tag} ) { my $page = d_up(d_journal("jnl".$jnltags{$filelist->byseq($first_in_row+1)->tag} . ".html#img" . sprintf("%04d", $first_in_row+1))); push(@b, button("journal", $page, 1, 1)); $nav{jnl} = $page; } # Construct the actual index part. my $cc = "\n"; for ( my $i = 0; $i < $index_rows; $i++, $first_in_row += $index_columns ) { if ( $first_in_row < $num_entries ) { $cc .= " \n"; for ( my $j = 0; $j < $index_columns; $j++ ) { my $this = $first_in_row + $j; if ( $this < $num_entries ) { my $el = $filelist->byseq($this+1); my $file = $el->dest_name; my $img; my $base; my $target = ""; if ( $el->type == T_REF ) { $img = d_up($el->assoc_name); $base = d_up($el->dest_name); $target = " target=\"_blank\""; } else { $img = $el->type == T_MPG ? $el->assoc_name : $file; # $img = d_up(d_thumbnails($img)); $base = d_up($medium ? d_medium($htmllist[$this]) : d_large($htmllist[$this])); } $cc .= heredoc(<<" EOD", 16); EOD } else { $cc .= " \n"; } } $cc .= " \n"; } } $cc .= "
@{[img($img, alt => "[$img]", class => "thumb")]}

@{[join($br, map { $capfun{$_}->($el) } split(//, $caption))]}

\n"; # Restore local scope. $fjoin = \&fjoin; $dest_dir = $orig_dd; update_if_needed(ixname($x), process_fmt($format_for{index}, title => $tt, css => css_for("index"), ltop => $tt, rtop => $t, hbuttons => hbuttons(@b), vbuttons => vbuttons(@b), jscript => jscript(%nav), contents => $cc, )); } ################ Journal Pages ################ sub write_journal_pages { return unless $journal; print STDERR ("Creating ", $journal, " journal page", $journal == 1 ? "" : "s", "\n") if $verbose > 1; mkpath([d_journal()], $verbose > 1); my $mod = write_journal(); uptodate("journal", $mod) if $verbose > 1; } sub write_journal { my $jname = sub { sprintf("jnl%04d.html", shift) }; my @ann; my $seq = 1; my $x = 0; my $tag; # Local scope... my $orig_dd = $dest_dir; local $dest_dir = "."; local $fjoin = \&hjoin; my $flush = sub { my $jnl = ""; my $ix = int($seq / ($index_rows * $index_columns)); foreach my $e ( @ann ) { my $t = $e->annotation; $t = (UNIVERSAL::isa($t, "ARRAY") ? $t->[0] : $t) || ""; $t = html($t) unless $t =~ /^type == T_ANN ) { $jnl .= "\n". " \n". " " . indent($t, 4) . "\n". " \n". "\n"; next; } # We cannot use $el->seq, since that's the info.dat order # which includes the skipped entries. my $dst = ($e->type == T_REF) ? $e->assoc_name : d_index($e->type == T_MPG ? $e->assoc_name : $e->dest_name); my $img = "type == T_REF ? " target=\"_blank\"" : ""). "href='" . d_up(($e->type == T_REF ? $e->dest_name : d_medium(sprintf("img%04d.html", $seq)))) . # "' border='0" . "'>" . ""; $jnl .= "\n". " \n". " " . indent($t || " ", 4) . "\n". " \n". " \n". " " . indent($img, 4) . "\n". " \n". "\n"; $seq++; } my @b = ( button("first", $jname->(1), 1, $x > 0 ), button("prev", $jname->($x), 1, $x > 0 ), button("next", $jname->($x+2), 1, $x < $journal-1), button("last", $jname->($journal), 1, $x < $journal-1), button("index", d_up(ixname($ix)), 1, 1 ), ); my %nav = ( up => d_up(ixname($ix)), idx => d_up(ixname($ix)) ); $nav{prev} = $jname->($x) if $x > 0; $nav{next} = $jname->($x+2) if $x < $journal-1; $x++; { # Temporary restore local scope. local $fjoin = \&fjoin; local $dest_dir = $orig_dd; update_if_needed(d_journal("jnl" . $jnltags{$tag} . ".html"), process_fmt($format_for{journal}, title => "Journal: " . htmln($tag), css => css_for("journal"), tag => htmln($tag), hbuttons => hbuttons(@b), vbuttons => vbuttons(@b), journal => $jnl, jscript => jscript(%nav), )); } }; my $mod = 0; foreach my $el ( @journal ) { my $t = $el->type; if ( $t == T_TAG ) { $flush->() && $mod++ if @ann; $tag = $el->tag; @ann = (); } else { push(@ann, $el); } } $flush->() && $mod++ if @ann; $mod; } ################ ################ #### Persistent info (cache) helpers. { my $cache; my @stats; INIT{ @stats = (0, 0, 0); } sub load_cache { $cache = new ImageInfoCache ((!$clobber && -s d_dest(".cache")) ? d_dest(".cache") : undef); } sub update_cache { $cache->store(d_dest(".cache")); } sub cache_entry { if ( @_ == 1 ) { $stats[0]++; my $ii = $cache->entry(@_); $stats[1]++ if $ii; warn("Cache miss: $_[0]\n") if !$ii && $trace; return $ii; } $stats[2]++; $cache->entry(@_); } END { print STDERR ("Cache: store = $stats[2], lookup = $stats[0], hits = $stats[1]\n") if $trace; } } #### Miscellaneous. sub findexec { my ($bin) = @_; foreach ( File::Spec->path ) { my $try = File::Spec->catfile($_, $bin); return $try if -x $try; } undef; } sub squote { my ($t) = @_; $t =~ s/([\\\"])/\\$1/g; $t = '"'.$t.'"' if $t =~ /[^-\w.\/]/; $t; } ################ Button Images ################ sub add_button_images { # Extract button images from DATA section. my $out = do { local *OUT; *OUT }; my $name; my $doing = 0; my $did = 0; while ( ) { if ( $doing ) { # uudecoding... if ( /^Xend/ ) { close($out); $doing = 0; # Done next; } # Select lines to process. next if /[a-z]/; next unless /^X(.*)/s; $_ = $1; next unless int((((ord() - 32) & 077) + 2) / 3) == int(length() / 4); # Decode. print $out unpack("u",$_); next; } # Otherwise, search for the uudecode 'begin' line. if ( /^Xbegin\s+\d+\s+(.+)$/ ) { next if !$clobber && -s d_icons($1); print STDERR ("Creating icons: ") if $verbose > 1 && !defined($name); $did++; $name = d_icons($1); print STDERR ("$1 ") if $verbose > 1; open($out, ">", $name); binmode($out); $doing = 1; # Doing next; } } print STDERR ("\n") if $verbose > 1; if ( $doing ) { die("Error in DATA: still processing $name\n"); unlink($name); } } ################ Style Sheets ################ ################ End Style Sheets ################ sub detab { my ($line) = @_; return $line unless $line; my $orig = $line; my (@l) = split(/\t/, $line, -1); # Replace tabs with blanks, retaining layout $line = shift(@l); $line .= " " x (8-length($line)%8) . shift(@l) while @l; $line; } ################ Copying: plain files ################ sub copy { my ($orig, $new, $time) = @_; $time = (stat($orig))[9] unless defined($time); my $in = do { local *F; *F }; open($in, "<", $orig) or die("$orig: $!\n"); binmode($in); my $out = do { local *F; *F }; open($out, ">", $new) or die("$new: $!\n"); binmode($out); my $buf; for (;;) { my ($r, $w, $t); defined($r = sysread($in, $buf, 10240)) or die("$orig: $!\n"); last unless $r; for ( $w = 0; $w < $r; $w += $t ) { $t = syswrite($out, $buf, $r - $w, $w) or die("$new: $!\n"); } } close($in); close($out) or die("$new: $!\n"); utime($time, $time, $new); } ################ Copying: MPG files ################ sub copy_mpg { my ($orig, $new, $time, $rotate, $mirror) = @_; $time = (stat($orig))[9] unless defined($time); # I'm not sure what this does. The resultant file is about 10% of # the original, without missing something... my $cmd = "$prog_mencoder -of mpeg -oac copy -ovc ". ($rotate ? "lavc -lavcopts vcodec=mpeg1video -vf rotate=".int($rotate/90)." " : "copy ") . squote($orig) . " -o ". squote($new); warn("\n+ $cmd\n") if $verbose > 2; my $res = `$cmd 2>&1`; die("${res}Aborted\n") if $?; utime($time, $time, $new); } sub still { my ($el) = @_; my $new = d_large($el->assoc_name); my $still = new Image::Magick; if ( $prog_mplayer ) { my $tmp = "00000001.jpg"; my $tmp2 = "00000002.jpg"; if ( -e $tmp ) { die("ERROR: mplayer needs to create a file $tmp, but it already exists!\n"); } # Sometimes, -frames 1 does not produce anything. Need -frames 2. my $cmd = "$prog_mplayer -really-quiet -nojoystick -nolirc -nosound -frames 2 -vo jpeg " . squote(d_large($el->dest_name)); warn("\n+ $cmd\n") if $verbose > 2; my $t = `$cmd 2>&1`; warn("$t\n") unless -s $tmp; $still->Read($tmp); unlink($tmp, $tmp2); } else { # This may take minutes. $still->Read(d_large($el->dest_name)."[0]"); } # Get still dimensions. my ($hs, $ws) = $still->Get(qw(height width)); unless ( $hs && $ws ) { $still->Read(d_icons("movie.jpg")); $still->Write($new); return $still; } # Scale to 640x480 if needed. my $r = $hs > $ws ? 640 / $hs : 640 / $ws; if ( abs($r - 1) > 0.05 ) { $still->Resize(width => $r*$ws, height => $r*$hs); ($hs, $ws) = $still->Get(qw(height width)); } # Create black canvas. my $canvas = new Image::Magick; $canvas->Set(size => ($ws+240).'x'.($hs+180)); $canvas->ReadImage('xc:black'); my ($hc, $wc) = $canvas->Get(qw(height width)); # Place the still on top of it. # Center image $canvas->Composite(image => $still, compose => 'Atop', x => 120, 'y' => 90); # Bottom slice. $canvas->Composite(image => $still, compose => 'Atop', x => 120, 'y' => $hs+135); # Top slice. Cannot place at negative offsets, so crop the still first. $still->Crop(width => $ws, height => 45, x => 0, 'y' => $hs-45); $canvas->Composite(image => $still, compose => 'Atop', x => 120, 'y' => 0); undef $still; # Drill spocket holes. my $hole = new Image::Magick; $hole->Set(size => '60x40'); $hole->ReadImage("xc:grey90"); $hole->Draw(primitive => 'polygon', fill => "black", points => " 0,0 5,0 0,5"); $hole->Draw(primitive => 'polygon', fill => "black", points => "60,0 55,0 60,5"); $hole->Draw(primitive => 'polygon', fill => "black", points => "60,40 55,40 60,35"); $hole->Draw(primitive => 'polygon', fill => "black", points => " 0,40 5,40 0,35"); for ( my $v = 0; $v < $hc; $v += 80 ) { for my $h ( 30, $wc-90 ) { $canvas->Composite(image => $hole, compose => 'Atop', geometry => "+$h+$v"); } } $canvas->Write($new); my $time = $el->timestamp; utime($time, $time, $new); $canvas; } ################ Copying: Voice files ################ sub copy_voice { my ($orig, $new, $time) = @_; $time = (stat($orig))[9] unless defined($time); $orig =~ s/\.\w+$/.mpg/; return if -s $new; return unless $prog_mplayer; # This will produce an MP2 file. Good enough for now... my $cmd = "$prog_mplayer -nojoystick -nolirc -vo null ". "-dumpaudio -dumpfile " . squote($new) . " " . squote($orig); warn("\n+ $cmd\n") if $trace; my $res = `$cmd 2>&1`; die("${res}Aborted\n") if $?; die("${res}Aborted\n") unless -s $new; utime($time, $time, $new); } ################ Index Icon Maintenance ################ sub create_master_index { my $index = d_dest("index.html"); return if -e $index; update_if_needed($index, process_fmt($format_for{main}, title => html($album_title), css => css_for("main"), version => $::VERSION, link => ixname(0), )); } sub create_index_icon { return unless $icon; print STDERR ("Creating index icon\n") if $verbose > 1; unless ( indexicon() ) { print STDERR ("(Index icon not modified)\n") if $verbose > 1; } } sub indexicon { my @imgs; for ( my $i = 0; $i < $index_rows*$index_columns; $i++ ) { next if $i >= $num_entries; my $el = $filelist->byseq($i+1); my $file = $el->dest_name; my $img; if ( $el->type == T_REF ) { $img = $el->assoc_name; } else { $img = $el->type == T_MPG ? $el->assoc_name : $file; $img = d_index($img); } push(@imgs, $img); } my $iconfile = "icon.jpg"; my $ii = cache_entry(" indexicon "); if ( -f $iconfile && $ii && $ii->dest_name eq "@imgs" ) { return 0; } my $el = new ImageInfo($iconfile); $el->dest_name("@imgs"); cache_entry(" indexicon ", $el); $cache_update++; my $image = new Image::Magick->new; foreach ( @imgs ) { $image->Read($_); } my $width = $thumb; my $height = int($thumb*0.75); $image = $image->Montage(tile=>"${index_columns}x${index_rows}", texture=>"xc:gray90"); $image->Resize(geometry=>"${width}x${height}"); $image->Write($iconfile); 1; } ################ Subroutines ################ sub app_options { my $help = 0; # handled locally my $ident = 0; # handled locally my $sel; # handled locally; if ( !GetOptions( # Run time options. 'clobber' => \$clobber, 'dcim=s' => sub { $import_dir = $_[1]; $import_exif++ }, 'exif' => \$import_exif, 'import=s' => \$import_dir, 'info=s' => \$info_file, 'link!' => \$linkthem, 'update' => \$update, 'mediumonly' => \$mediumonly, 'select=s' => \$sel, 'extcss' => \$externalize_css, 'extformats' => \$externalize_formats, # Album options. Can also be set in info/config files. 'captions=s' => \$caption, 'cols|columns=i' => \$index_columns, 'icon!' => \$icon, 'medium' => sub { $medium = 0 }, 'mediumsize=i' => \$medium, 'rows=i' => \$index_rows, 'thumbsize=i' => \$thumb, 'title=s' => \$album_title, 'home=s' => \$home_link, # Miscellaneous. 'debug' => \$debug, 'help|?' => \$help, 'ident' => \$ident, 'quiet' => sub { $verbose = 0 }, 'test' => \$test, 'trace' => \$trace, 'verbose+' => \$verbose, ) or $help or @ARGV > 1 or @ARGV && ! -d $ARGV[0] ) { app_usage(2); } app_ident() if $ident; $dest_dir = @ARGV ? shift(@ARGV) : "."; $dest_dir =~ s;^\./;;; if ( $import_dir ) { die("$import_dir: Not a directory\n") unless -d $import_dir; $import_dir =~ s;^\./;;; } set_selector($sel); } sub app_ident { print STDERR ("This is $my_package [$my_name $my_version]\n"); } sub app_usage { my ($exit) = @_; app_ident(); print STDERR heredoc(<<" EndOfUsage", 4); Usage: $0 [options] [ directory ] Album: --info XXX description file, default "@{[DEFAULTS->{info}]}" (if it exists) --title XXX album title, default "@{[DEFAULTS->{title}]}" --[no]icon [do not] produce an album icon --home XXX up link for index pages Index: --cols NN number of columns per page, default @{[DEFAULTS->{indexcols}]} --rows NN number of rows per page, default @{[DEFAULTS->{indexrows}]} --thumbsize NNN the max size of thumbnail images, default @{[DEFAULTS->{thumbsize}]} --captions XXX f: filename s: size c: description t: tag Medium: --medium produce medium sized images of size @{[DEFAULTS->{mediumsize}]} --mediumsize NNN the max size of medium sized images, default @{[DEFAULTS->{mediumsize}]} --mediumonly ignore large images and links (for web export) Importing: --import XXX original images --exif use w/ EXIF info, if possible --dcim XXX as --import with --exif --update add new entries from import, if needed --[no]link [do not] link to original, instead of copying. Default is link. Miscellaneous: --clobber recreate everything (except large) --clobbercss recreate (overwrite) style sheets --select=XXX select images (default, all, hidden, tag:...) --test verify only --help this message --ident show identification --verbose verbose information EndOfUsage exit $exit if defined $exit && $exit != 0; } sub set_selector { my $sel = shift || 'default'; my $tag; if ( $sel =~ /^(tag):(.+)/i ) { $sel = 'tag:...'; if ( $2 =~ /^\/(.+?)\/?$/ ) { $tag = $1; } else { $tag = quotemeta($2); $tag =~ s/(\\ )+/\\s+/g; } warn("tag = \"$tag\"\n"); } my %selectors = ( default => sub { ! $_[0]->hidden }, all => sub { 1 }, hidden => sub { $_[0]->hidden }, 'tag:...' => sub { $_[0]->tag =~ $tag } ); die("Unknown selection: $sel\n". "Possible values are: ", join(", ", sort keys %selectors), ".\n") unless $select = $selectors{lc($sel)}; } ################ Modules ################ package ImageInfo; my @std_fields; my @exif_fields; my $exif_rot; INIT { @std_fields = qw(type seq next prev hidden dest_name orig_name assoc_name timestamp file_size medium_size tag description annotation rotation mirror); @exif_fields = qw(DateTime ExifImageLength ExifImageWidth ExposureMode ExposureProgram ExposureTime FNumber Flash FocalLength ISOSpeedRatings ImageDescription Make Model MeteringMode SceneCaptureType Orientation height width file_ext); $exif_rot = { top_left => [ 0, '' ], # 1: no corr. needed top_right => [ 0, 'v' ], # 2: flop (V) bot_right => [ 180, '' ], # 3: 180 bot_left => [ 0, 'h' ], # 4: flip (H) left_top => [ 90, 'h' ], # 5: flip 90 right_top => [ 90, '' ], # 6: 90 right_bot => [ 90, 'v' ], # 7: flop 90 left_bot => [ 270, '' ], # 8: 270 }; } my $largepat; sub basename_nolarge { my ($f) = @_; unless ( $largepat ) { $largepat = quotemeta(::d_large()); $largepat = qr;^$largepat[/\\];; } $f =~ s;$largepat;;; $f; } sub new { my ($pkg, $file) = @_; $pkg = ref($pkg) if ref($pkg); my $self = { $file ? (orig_name => $file, dest_name => basename_nolarge($file)) : (), description => "", annotation => [], tag => "", hidden => 0, }; if ( $file && -f $file ) { my @st = stat(_); my $ii = ::cache_entry($file); if ( $ii ){ $self = $ii; delete($self->{$_}) foreach grep { /^_/ } keys(%$self); } # Else, get image info. else { my $ii = Image::Info::image_info($file); $self->{file_size} = $st[7]; $self->{timestamp} = $st[9]; unless ( exists($ii->{error}) ) { for my $key ( @exif_fields ) { my $val = $ii->{$key}; next unless defined $val; if ( $key eq "Orientation" ) { ($self->{rotation}, $self->{mirror}) = @{$exif_rot->{$val}} if exists $exif_rot->{$val}; } else { $val = $val->as_float if UNIVERSAL::can($val,"as_float"); $self->{$key} = $val; } } ::cache_entry($file, $self); } } # Actualize. $self->{file_size} = $st[7]; $self->{timestamp} = $st[9]; } bless($self, $pkg); } INIT { no strict 'refs'; for my $sub ( @std_fields, @exif_fields ) { $sub = "_".$sub if $sub eq "rotation"; *{"ImageInfo::$sub"} = sub { my ($self, $value) = @_; $self->{$sub} = $value if defined($value); $self->{$sub}; }; } } sub rotation { my ($self) = @_; defined($self->{_rotation}) ? $self->{_rotation} : $self->{rotation}; } sub html_name { my ($self) = @_; sprintf("img%04d.html", $self->seq); } package FileList; use Class::Struct "FileList" => [ _tally => '$', _data => '$', _hash => '$', ]; sub add { my ($self, $el, $name) = @_; my $data = $self->_data; my $hash = $self->_hash; $self->_hash($hash = {}) unless $hash; $self->_data($data = []) unless $data; push(@$data, $el); $hash->{$name || $el->dest_name || ""} = $el; $self->_tally(($self->_tally||0)+1); $el->seq($self->_tally); $self; } sub byname { my ($self, $file) = @_; $self->_hash ? $self->_hash->{$file} : undef; } sub entries { my ($self) = @_; $self->_data([]) unless $self->_data; wantarray ? @{$self->_data} : $self->_data; } sub tally { my ($self) = @_; $self->_tally || 0; } sub byseq { my ($self, $seq) = @_; $self->_data ? $self->_data->[$seq-1] : undef; } sub filter { my ($self, $filter) = @_; my $new = FileList->new; my $prev; foreach my $el ( @{$self->entries} ) { next unless $filter->($el); my $e = bless { %$el }, 'ImageInfo'; # one level copy $e->prev($prev->seq) if $prev; $new->add($e); $prev->next($e->seq) if $prev; $prev = $e; } return $new; } #### Cache maintenance. package ImageInfoCache; use constant CACHE_VERSION => 3; sub new { my ($pkg, $file) = @_; $pkg = ref($pkg) || $pkg; my $self = bless({}, $pkg); if ( defined($file) ) { $self->load($file); if ( ($self->{_version} || 1) != CACHE_VERSION ) { warn("Incompatible cache version " . $self->version . " -- invalidated\n") if $verbose; $self = bless({}, $pkg); } } $self->{_version} = CACHE_VERSION; $self; } sub load { my ($self, $file) = @_; our $info; $info = undef; eval { require $file; }; if ( $@ ) { warn("Illegal cache -- invalidated\n") if $verbose; return; } @{$self}{keys(%$info)} = values(%$info); } sub store { my ($self, $file) = @_; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Sortkeys = 1; # avoid warnings $Data::Dumper::Purity = 1; my $cache = do { local *C; *C }; open($cache, ">", $file) and print $cache (Data::Dumper->Dump([$self],[qw(info)]), "\n1;\n") and close($cache); } sub entry { my ($self, $file, $entry) = @_; $file =~ s;^\./;;; if ( defined $entry ) { $self->{$file} = $entry; } else { $entry = $self->{$file}; } $entry; } sub entries { my ($self) = @_; [ sort(keys(%{$self})) ]; } sub version { my ($self) = @_; $self->{_version}; } package main; =head1 NAME Album - create and maintain HTML based photo albums =head1 SYNOPSIS A photo album consists of a number of (large) pictures, small thumbnail images, and index pages. Optionally, medium sized images can be generated as well. The album will be organised as follows: index/ index pages with thumbnails icons/ directory with navigation icons large/ original (large) images, with HTML pages medium/ optional medium sized images, with HTML pages Each image can be labeled with a description, a tag (applies to a group of images, e.g. a date), the image name, and some characteristics (size and dimensions). Images can be handled 'in situ', or imported from e.g. a CD-ROM or digital camera. Optionally, EXIF information from digital camera files can be taken into account. =head1 DESCRIPTION For a description how to use the program, see L. =head1 AUTHOR AND CREDITS Johan Vromans (jvromans@squirrel.nl) wrote this module. Web site: http://johan.vromans.org/Album/index.html =head1 COPYRIGHT AND DISCLAIMER This program is Copyright 2004,2007 by Squirrel Consultancy. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of either: a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" which comes with Perl. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the Artistic License for more details. =cut __END__ Xbegin 644 index.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!?TE$050XC;V3 XMOTM"413'/^JSAR))B3B;0F[A%KGDT-(D+D(0!*W^`[T6EPP:0W`2A":7>+N# XM+C4(\D9U4&?Q1QB1^"L:>M?W>$HNT7>YY][[.>=^W^$=6Y4MDO1U.;3>^!PF XM8E)1YUTK$70FXB[`5H7/6PTY8B6:4Z)W;K!5F2A:)./W6HEQ(]>,9EW8H:)% XMBN$U`.]Q,:)50`*5C/0FSMWRQUP/G9YT6CU'8CF7_;,S02A)Y54/3QX/?[YE XMV#WRSM@)`0SZ``Y#0#NEW]C7'%KU%X3P\5X`J`/PO`^, XMK,0X;V25-M20%+&/DCK5PX"9L">-G-A&'U_JJD;PI2=JQ$S$(BL()5A:==U, XM@/<"H%X#2.T#(^%7$+O7`-0`DB&@+8C_[7KO$F``P(T,3`W"%VR.75;3(D0`)XF7M*N-;M]C*-:-Q8^8V XC2LP<3"KJ)L"8V]^UO6/?/_-HCRMMEKD`````245.1*Y"8((` X` Xend Xbegin 644 medium.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!(DE$050XC;W3 XMOTZ#4!3'\2\4TM3$="!]@"8FLK*Y]@U(]`&.YJEA== XMU>RU`,F'K^N`IIX7X1)C=`"2S^(JT*U..R^29SLT;EK(X`7ZY*@`:)],],`# XM&5PLI5`'4(:X(+-9-3LE/WH,(#.?Z<46F-]/`RKA/.P XM);Z(RL5TO`7B050FG-MO`-[[D5A,[_9/2?]%))QQ^C89I-O]O=5/]3+;_54O XMB,,SQ/FOF].Z85(M&NKRK:08[;J86&LA6-N8H$#O*3BW-,$7[-#HI3,GS'[F X@8.&Y(I#.;77JS^,'X]I4;J49'ZH`````245.1*Y"8((` X` Xend Xbegin 644 first.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!;4E$050XC873 XMNT["4!C`\7]+"<&%@?``)"9V,`8F77D#XF4$W44G0Q"7+BI1)X75(*QBV!P< XM8'6QQAC%B0KR`B"R XM457U%LC01%,6[@&4'$V0F8Q",9]!5P!D>EU5*/%FA.F[=2([OZK=&K'TZ"/J XMY2D`YP]"-\+]K'YME"@)P"%,,+D4@5AE#J[N'97M''<5H]1%P]F\E:-6,9HL XMN8"5X]6E[6M0C0[F33-EOSG/-WQP,6DTO8+_;OY?\'^`7/!!E X1K2\A))\`````245.1*Y"8((` X` Xend Xbegin 644 first-gr.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!2DE$050XC873 XM36K"0!3`\7]B1"P%H5&$?@A"H=EFUZTW\`@]0$_0;K)I3Y`#>(3LNX@WR++I XM2M"V(-520131U"XF'Y,QJ6_S(//+O)?,/&W(D3#B',W5%;,BB;7O;4>JZ%;[ XMO3J@#6'U&%"S5!%NL)].0!NR?@@LI]50Q>+5#>WG.CKX@36X/@`T;@=6X(,. XM'HYQL`Y@W..!3K2MM4H^]`9`9SZRI!*12/LH?:+GWYK^B#Q9E8CIQQZ`\4SJ XM)@]$B8D$R&BXS$ XM0Z)5)%&I;I232.,M[K2/LRL$.Y<^&-![">XL''`````$E%3D2N0F"" X` Xend Xbegin 644 last.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!<4E$050XC863 XM/4M"41C'?_>HB"T.X@<0@BX1HE.MCFVB-1K1FC9)6"TNO2`MU6T-J[$4MX8& XM_087(L@F/T`XM"2B7AN.+_?E6/_E.3SGQ__YWWMXM!;_R#^IHZ[[)N*S$;UF XM8]!Q$[%`.A4"M!;\G)@$=3?1[I,\70*M1>_8U,O1L)OX_C#:R;,0`IJF7EWV XM`(0WJKK9!`$-RG[//8`_3P,$HT$PNN!#5P`$W8X>!GBW9/O-A8G9Z>5"UKN' XM103U44R"[+OOYA#H'V9)L[T^'N*=L%^6?*.1F+:?'5E&F XM.)@#3H_,H?``#B)[)&T+=L`^)3,!\@[`YK&Y*NM>W!E>$(FUOP'6)G8N`($O XMT/]"K<])CC3EH1(8&J3!#ZE7<[<<43@8[61JOG-*37<.>LV&"ICO[=_RO+Y' X5OU]T8)`W'C>9`````$E%3D2N0F"" X` Xend Xbegin 644 last-gr.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!3DE$050XC873 XM34K#0!3`\7\F*:5U4:$4W`A&"V;;G=O>H$?P`)Y`-]GH"7J`'B%[%^D-LC3B XM1P0W@A3LHBVE^7"1IDDF,_HV$_)^O'G#FS'F_!/6?DT6!XPYZLE@^33KYTHGTF@)0/RVU@BR XM][Q*_++2"+(H)\GK2B/(/@JRU@A2!:F+LMV22((LBG,2I1IAV/D$+%NHA;#S XM$5K#KGH7<78,@%F"NC`4H"8,NP!'E;^5FV&<-WJHUS@T>5$#E1HG^]M_:M8` XM@KX=+@&*A`00F*WM-^IXWO6GKN> XMR*)3ZOV7QF'1=EK[J?JKS><,4$5]U,ANR%4: XMKQ*016M4ER^5JCP=*WT)S$ZX^$M5@*!86GVJ^P!\[/KH8V]RP<:A#P;TWH)S XMV\RYP0F[O73FGV4]9TE/N(>U73L12;UWF5S$8H*,IX$1F`\=TB+@M0D).YQCF` XM^T`.BG;G3_YYZ"V`8EW'QRM:DZEN6NL^H5=-CZ!YET0*FI7N$>@/W2,DL0C1 XMKDW0?/8)[^HDL?V('WGG:_B1SUDA@'F+=^UU-V0-[T8`6<._E$"*<&#TI0C" XM:O.7F@#%8*B_S'T`W@Z=3LGV5K"?,P47TI?R/@LL%>95DAYGSAJ_,P?;(K>! X=X]R>#^OO=^('W$Y.6'!/."L`````245.1*Y"8((` X` Xend Xbegin 644 prev.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!-TE$050XC863 XM34["0!S%?RU%@HEA43D`B8G==L>V*JPY@@?P``90&R/&`_0`+#Q`UZ"V-^C2 XMNN(`RH*8V/!E7(BA\R%]FTEF?O/FS4O^1DR!K,VZGLHG=BE'9%&XG,A$H]SQ XMJH`1PU+(1#K''>R#$9-U$\>OUV1B]AJD[GT5$Z+$&1XI`+7FT$DB,"'$ XMMY1S`.N"$$S6RTK]GX\>`YA,)X[XQ*>(F+M?L),;];]E4 XM)$8W*R670(S\A0*0[^KY6G40/#YZ.B!/'`ZTY>=SG-[N%1"T?0TB&K>-*R6+ XM]'3+Z,J5R:V?/90*"$[NI"SJ!UM-V<-NI#-AZT`F2N7YNV+TJ[=-C@Z^MFY6 XM`1VPP!LGY[ZM<0A2U]O.G%9_,P=9%.J`[=SNECH-LGX`6BE1+/?*'38````` X(245.1*Y"8((` X` Xend Xbegin 644 prev-gr.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!(4E$050XC873 XM,6[",!0&X-].(D2'TBAB)Q(27K.QY@8Y0@_0$[1+EO8$.0!'R,Z0W,`CZ136 XM"B'*T@B%B`ZE`K]GR%LLQ5^>GU_R1(F><,]KMZ4[@7,EFB)O:RI"+XF'`$0) XM_+QI#!05U0'1^P,@2C2O6J7C$17[559%'T-(H-!J,64`H_E"Z0*00([49?L` XMX+X@!R2Z=C"^<=$9`$AL:V4>T9E,LA=W7SWB>WTBU=`,K',DQV[-3I4$G.X+ XM&S`$*Y**MK:!:^&%HD?@:6(C1J6^C9BWM1'2,7_"!.VZ+VC;V9=CY?*_RW^D XM.8*PVAN/'"H<[[!AB?[B\UQ'@O1H!<<,">`"\5(_IX$E0U9%\67FK/$_!QB_1RTX:[.:Q%@````!)14Y$KD)@@@`` X` Xend Xbegin 644 up.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!)TE$050XC;73 XML4K#0!S'\6^2%IK!$FA?H%,A3]#)1Y`.3I+!P2(:'\!"44=?((LX]@$RMRJ9 XMQ+%3H#AT2=,B+92ZE+01E[8F,5AT.RH XMJIQH(JOJX<<@:""#BYEQ#1,79,)U[F61F8_JY7P!)44D1%WB>3J:Y(M^QS\? XMYXG>[0;?]+)%[RX`O*MQEGB^"0#P+B;IXJ6]V59^:YHF^NVO?3UM^?MZ]X/X XM?#^)=G\]_B4.+DG/W]_T_\0JS!>5VG"9+Y2BL(N.E;%IH4,!#,=._^V6K1F1 XEF4O+;N9@UG73P,_<"D\JR#@(0SZ:@E)I+8[GG/@6/+\D\H5?U\,E;\ XMCAJBD$F7[HQ$KS,:<0.V++QO:+A4(Y$K$MSR@"U+85U38^T^(Y&_2N:"<3=V XMR&CJ;I\)P#>RJVH94"!-3"D>`LYI+FY$>:0;4):7TU,H?)9<[;S'@>9I3E*" XM2'0#](M9GNZ&?+B7@`88;:X.8MI'XZ+0<-CDITJ43F6B8Y"S-V#8JR=>5R4Q XM$2=Q"Z0&](1K5A(J3#X";88NGK6:S@M6/LJ7PG"_R:@D7L0L@;VZ1,,$`)U0 XMWI2E:+B6:(K+?.5(OD(ZHG(OM-4+'=L`!_OZ+OD9H2MSX!H".#?X4$)"N^HZ XM]>Z82WKBZPT`NP>HO`)\&(CG<:"ZC_LQJS\<`0!ZP!:0I19)^'MS>5_+SS*= XMQK7:<3B+#W5,7@N"*+&R)5!.$@4%(L?:?,QO\4,R%XS\WIQER)N#0B9M!?S> X;[=]A_P_@&\BG:"&P_Q,B`````$E%3D2N0F"" X` Xend Xbegin 644 sound.png XMB5!.1PT*&@H````-24A$4@```"$````A"`````!RCYVS```!FTE$050XC9V3 XMOTL"<1C&G_/.S"(;5"@20HF\P7Y($$5#V"@$_0$5$FT);0VYN"31$`5"2X1+ XM0TO<%$3#'10M#5<.>0:A!(:D$DJDG@]9!+9&/ XMA05WR`@=P/)T9*P)$*.%,YIG`0I@$*14HE2(/0K1+]S[_8P7%"318&VHJ?0S XMGWZI'9S567*)*;G%Y452TK93U_]\U>J`KCG5L48M2*-K9'X_VX88'%BP3S@! XM'+>IL;%JZN)C2`O\P>D_9\E7LA93!R)Y^"`"XQ]MB:>M$@#4OXU,I&^368LM XM!>"\U-+']5&FEJB\:VI69WD+9.3$4OW-12F(.\6=];6J,AO2*;J,RGH_J.UE XMK@S,3/?4?9CM0GYN,EH]THL`'`ZU#U)?SI`G?AN`X%!$4'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX> XM'AX>'AX>'AX>'AX>'AX>'AX>'AX>'A[_P``1"`!:`'@#`2(``A$!`Q$!_\0` XM'P```04!`0$!`0$```````````$"`P0%!@<("0H+_\0`M1```@$#`P($`P4% XM!`0```%]`0(#``01!1(A,4$&$U%A!R)Q%#*!D:$((T*QP152T?`D,V)R@@D* XM%A<8&1HE)B7J#A(6&AXB)BI*3E)66EYB9FJ*CI*6FIZBIJK*SM+6VM[BYNL+#Q,7& XMQ\C)RM+3U-76U]C9VN'BX^3EYN?HZ>KQ\O/T]?;W^/GZ_\0`'P$``P$!`0$! XM`0$!`0````````$"`P0%!@<("0H+_\0`M1$``@$"!`0#!`<%!`0``0)W``$" XM`Q$$!2$Q!A)!40=A<1,B,H$(%$*1H;'!"2,S4O`58G+1"A8D-.$E\1<8&1HF XM)R@I*C4V-S@Y.D-$149'2$E*4U155E=865IC9&5F9VAI:G-T=79W>'EZ@H.$ XMA8:'B(F*DI.4E9:7F)F:HJ.DI::GJ*FJLK.TM;:WN+FZPL/$Q<;'R,G*TM/4 XMU=;7V-G:XN/DY>;GZ.GJ\O/T]?;W^/GZ_]H`#`,!``(1`Q$`/P#ZI\=^,O#? XM@?0I-9\3:I#I]HG0N?FD/]U5ZL?85\I^._VQ]5O]1.F?#?PJ7W,5CN+Y2\LG XMNL2<#\2?H*X[_@H)JHU7XCZ)-;27!LUTPQJK,?++K*Y+JO8D,H)ZD!?2O1/V XM8_!EA;_!+2O&-MIEK%-=^8LUP0#-(RSO'UZA?DZ<*I\1/CG\1_'/_")O XMXOO--U&0RAH%G^R1QE068$KR.F/RJ;XD_#SXM^$_"=UXEUSXA_;K:RV!DAUJ XMXDE^=U08#`=V]>F:T?A[%8P?ML20:A=I:V4M]>F>620(%!MI'ZGIS@5Z/^U] XMJOAU/AQJFEZ%K=E>B26V^6.Y21F&]6/0^U`'D'P>M?V@-=\/W/B/P%K^M7=I XM9SM;R0_VGN;<%#D"*1CD8<<@=37H_@W]K'QSX/UI_#_Q2\-O>/;D+.8XQ;W< XM60""5/RMP0?X>O6NG_8_\/F#X%1ZW:ZE!'/)>7$\D*R8D`5O+SP,>Z'G`]1D5W61ZU\/_&/X&Z9X3TB[^)?PE\37.C-I*&YGLY; XMEDEB`/6)QSG)QM/7UKH/@#^UM:W2VOA_XFC[-.1L36(T`C8^LRC[O^\!C/4` XM9-`'V!15>PO;2_LXKRQN(KFVF4/%+$P974]"".HJQ0`4444`%%%)O%`"Y'K1 XM7"GXD:%/\68/AUI\\=WJ0LIKR\,;@K;*A4*C?[9+9QV`]Q10!\:?MQ6GE:KX XM>G"][N$D=.&C('ZFN%\*?$7XJ7GP_L/AMX,%Y'IUNTCN+"$M-*SR%_FDZJH) XM'`('KFO6/V\+/;;:?<;.(M5N(P1T&\9Q_P".?H:L_L?RH_PQNE^4O'JDBG`P XM=NR,CGZDUPYEC7@L.ZJ5SNR_"+%5O9MV/+]._9]^)6M2O>:K)I]G-)\SF\O0 XM\K$]SLW?J:WT_92\7.H(\0:1_P!\O_A7U9IYD`!ALXQ_M.:UX6N0*^#_V6O`&E?$CXIC0]=2:33(K"XN;@0N4<`*$4@CIAW4^ XM^,5]+E^,G6HN=5;'A8FE&G4Y8'M'QX^)G@;Q;\&-9N?!6H2(]PL,4]G,2DT+ XM-,N5V]P0#R"1[]JXK]D[X>>"/'7A'Q3;>,;24&6XMXK&_B)62U=5YCOU4AX)"B)Y*8=6L+-T8V\H\RVD1T#K\IY1MK#.TBOJ#X6?M6_#OQ3!%;> XM(ISX8U$A0RW9)@9C_=D`P!_O8^M?&/BB[M_&/Q_>6T=+JVU#6X886&2LD>]( XMP>>V!7H'[8'A/P5H?]CZOXTOQ'JUQ,8F;9-J,@0(.N XM"?"MLUSX@\4:381JN<27*[V^BC+,?8`U\I?&_]KB?48)M XM!^%EM<0-.#$VJ31?O<'C]RG.">S'D9Z`\CYVG\!7]G\5K#P1KEVJW-SI`->OVW@/POX(_:+^'NDVTSVVGW",]UO6BBX&]^WE9[O"LDH3F'68G)!Z!HI. XM?U'YURG[&-SO\)Z[:;@?*ODDVXY&Y,9_\<_2O2?VY;/S/`>NR>6#Y4MK,#GI XM\R)G_P`>->0_L53<>*+8E?\`EU<#O_RU!_I7C9_&^`G\OS/6R65L7'YGTU:? XM9RPW0RSMZ#I7060"HNV+R!_+\JP;-[D#"3QPKZG`K:T]P<#[1]H;T/\`A7P5 XM$^FQ:W9R/[5&HRZ1^SOXFG\^"0W,45JA48)\R558?]\EJ\=_X)SZ,6U?Q=XB XM="1!;P6<9`Z[V9V_]%I^==5^WEJ4=I\)-)TQ+=[>:^U9&?GATCC,_AZUYE\1OA=X-\=P/_;FE1B\8`"\@&R=<=/F'7Z'->D:J;A&V7<,; XM_P!R51@FLF5U1"S%L'T&:^9K5I4JO-#W7]Q[.&IJ4+2U/C3XA_`SQ)\,[C_A XM,O"^OQ75MI;BZ65T$4\!4Y!PPW.IS/LN(0Z'"1KT8>J_SKWL-G$X8-UZRO9V.:>61J8CV4';2YA_ XM#WX[>"?#O@'2M#ET_61=6-JL4@$2%'?'S,IW9P6SU&>:\W_9O\7^&O!/C'4M XM9\327@A;2Y;>V6VBWEI6>,KGG@84U]*W?P@^'-S,\TGA6S#, XM+/A%X"MO">LSZ=X;MXKQ=/G,#AW)1O+)4C+$9!%.GQ+A9R4;.[+GP_7BF^9' XMA.L^.FUGX\V_COPYH5YJ!MI(9H+.1#N=HT`!.S/&X`_3TKT:V^%/Q"^,.L6V XMN>.KBU\/V4*;(X8HMTVPG)`3/'U8\9Z&M_\`9%^RR?#*0B%1*FI2I(RJ-S?* XMA&3_`,"_2OH73$*Q<0")".`3DGWK#'YY5IU94:<;6ZCH953=*-63O<^8/A5X XM9TKP#^W!HOA[2O.^Q1PNL6]MSY>P8DD^[$G\>/2BMZYC2#]O_P`-3%L>=$K- XMN/H%:UJ,*T'3FKIA2JSHS4X/5%#Q/\:/B=>[+@W[Z3:SY,*V]N$#`>C$$GKUS XM7H.E:1^TM;P+=VOB3'R[DB>\B;?"?Q3^)_C7XAVNFZ5XJEBN9-&>?RVABVL=^T-NVG:<;."`.IKZM_96^,/PR XMM_AIH/@2XUC^S-4LX"CQWD?E)+([EV*/RIRS'`)!]J\%_9;TE]5^-.M,+0W2 XM6ME'[B[;1_&MC<>& XM-6@?RYO-1GA5P<<_Q+^(_'O7RF891B(OGAJO+?[CV\'C*5K-V]1G[9=U:P_" XM-D16CGN-0@B*GY=PPS_C]VG_`+,MK/;?!/178.J7#SR\]/\`7.O\E!KE_P!M XM'Q!9:KX`\.KIU[9WMK&?L;331>']?TQ0!-;:D&8$<@L@7G_O@U])V!8# XM$UR)9"/N@]*^;/V^5KZ!.JZ3HNF2ZAJ$\ XM&GV,0)ENKF0(OYG^E>[F<7+'2MJW9_@CP*#2PJOTNOQ/$?',<4/[YV2 XM.\%I9VY*1;CS@#)[=R>E45T?XCK&D:Z7XK"(`$46]QA<<#`QQ7Z+Z=\/=1MK XM58;:VL;.)1Q$F%`_!1@58?P-K2@D/;-CH`Y_PH`_/+P%XJ^(7PGUYO$.D6M[ XMI=Q,A@E:[LVV3+D,4;>.>0#Z\5/\9OBIJ?Q.FT^YU73K:RGM/.9_(9MDC2%2 XM2%/W<;?4]:^_6\/^(K#=LM79#PRH0RL/0CO7G&K_``J^'6JWDH\1>$XH_,5@ XMSVJ_9Y58_P`0Q@9SF@"U\'H_"]S\'-`D\/\`B:RU">RTVV2]M4<&2&8HH=2! XMR`&++OVCFM[J%;BVN];N)98V&0\:N[D$#V6O9O&/[->G:: XML>L_#7QO>VUZC_);78*LG'7S$P1R/0YKS[P#X;^)_P`&?'=IX[E\"3ZS#:^< XMFY%,L3AT9&8,F2#ACR1WZ4`6_P!IGX=^%?!_A^QU/08KFUENK[RVM_-+0[=C XM$D`]#D*!STSQ6]\(/%_Q6;X?V-W8>'=+US1K0?9(8U;R;G9&`O!!P?3)!Z5Q XM/[1OQ9TWXF0:7]@T:ZTB2WGEDN;64@JA*H%"D?>_BR2`?:OZ)JELX'/EA9`,#GH0<5=E_:,\"JC%(-4=@.%\@J?[7UBL?Q)TM[:*-/M&EH-L:X)82R XM\GU/('X5Y3X']+C`U<;42ZR?*R XM5.XA3@G(/?O4/CR3XD^+/!B>./%6H.^C&18[6,N%1B25RD:\`?*>:]G_`&J] XM(L=!^$\>AN;2VO;6XMF\A2@=P%(S@=?O9->6^)O'NGZY\(=`^'&@Z=>7VH)% XM"9I%7A95))11C+=>O`^M>I3PM&#YE'7:_IL>;4Q-6HK2>G^9W&L^%].\$?$C XMX`RZ;8QQ_P!HFQNYRH(\R22:+.6ZD@,#^/I17=_LT?!'7-!M.GRUH[VS>GWE_ XM6LV/1?%6B+MTZ=+JWSS$#Q_WR>GX&N_I#WH`^?\`7?`?A+6->:\\8^#+2]W, XM[3%K8+(=V?XA@]3GK7&^-_@1\(]0>)O#FFZCI64(E6*Y?`/88RMF.[O$I_I0!\W^#?V=[/PK;7%]X5^+^OZ"][`IN(;61% XMWX!(5O[V,G!/(R?6N1N_V>H_$^J?;?$_Q+U:4P1A8Y+B+S7QG.%YX_QKZL;3 XM=.W?\>%KU_YXK_A3FTS3=I/]GVG7_GBO^%`'RO#^SGX2FU@S76K:_KP^4_Z0 XMX5Y#CG.,G&?TKVGX;?!?0M`(DL]#M-)C8#<57=/(/0LO6%O;PQ*(H(H XBP%Z*@%63]X#MB@"&QLK>QMEM[2%8HUZ!1^OUHJS10!__V0`` X` Xend Xbegin 644 extern.jpg XM_]C_X``02D9)1@`!`0$`2`!(``#_X0`617AI9@``34T`*@````@```````#_ XMVP!#``4#!`0$`P4$!`0%!04&!PP(!P<'!P\+"PD,$0\2$A$/$1$3%AP7$Q0: XM%1$1&"$8&AT='Q\?$Q)!P>'Q[_VP!#`04%!0<&!PX("`X>%!$4'AX> XM'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX> XM'A[_P``1"`"``(`#`2(``A$!`Q$!_\0`'0```@(#`0$!``````````````<% XM!@($"`,!"?_$`#X0``$#`P(#!00(!00"`P````$"`P0`!1$&(1(Q00<346%Q XM%"(R@2-"4F)RD:&Q"!4SP?`D--'A%B5#@K+_Q``<`0`"`P$!`0$````````` XM```%!@`$!P,!`@C_Q``Q$0`!`P,#`P$&!04````````!``(#!`41(3%!!A)1 XM81,4(C)Q@2.AL='P%4.1P>'_V@`,`P$``A$#$0`_`.FJ***BB****BB^+4E" XM2M:TH2D94I1``'B2>0I$:SO3>J9ZYZK;;;%"K1U6^G(BJM6^>0K-9;&R_$S0I]45 XM2=+:Y;D.LV[4*&84QT\+$IL_Z62>@2H[H7]Q6_@3FKM6B05$=0P21'(*798G XMQ.[7!%%%%=ES111144111144111144147JF]1=/6*3=90XTM`!MH*PIY9.$H XM3YDD>.-SR!J4I&=J>IQ>M0^SQ7.*!;EJ:8X3E+K^X6[ML0GX4G?ZQ!WH?@]-ATK"X/OA+,*"@NSIB@U'0G89)^+/3%9.\2U<_ESBFP!L;?0+:8 XMO%N5.5"$E(=!*<8V)\,\LU-->)`&_4U:+1HVQG3#-AEQ&Y+205+>`X7%.GXG XM$JY@YY>0`.:JU^T[>]'@OJX[G9`!B0E.76!X+'AY\O3E1V\=&U=%")6_$,:^ XMB%T5\I:N0QM.#QGE;:"%,K:<;2\RXGA<:<2"A8\"#5@TQJ>YV-*8Z`_=[8D? XM[5:^.5&`Z-*)^E1]Q7O#8`G&*K$&0Q*92\PL.()YYY5[=-CT^J*6[==JJV29 XMC.G(*)U%-'.W#@G79+M;KW;T3[7+;E1E$CC3D%*AS2H'=*AG<$`BMZDC"ER( XMER_F4*6J!<-@J0$\:'Q]E]OZXZ<6RQT.P%,32^LHMR?:MMS:3;;HM)*&RYQ, XMR<@9.`JCVNZH58K&+?">[NY XM7!*D(4D[LM(.^$^9R/A-(D*!PE">%"1PH2.0`K;U5?I.HK]*NDK*5O*` XM0WG^BT/@1\AN?$DFM!K<=#^W.LUOEP-9-I\HV350TP@CP=RML.(;0IQ:@$(! XM*B=@D`5:NP_3;VHK\+S)!1WZ"F-M_1CC92QYJ/NCR)Z$54(ELD:CN[%FCMNO XM-E7%(#0]Y2<[('F?TY\@:ZBT19&]+Z>2IZ.AJ4XV.\X<8;`&R!C8!(P-O"CW XM2%I&M9(-=FCU\_9!K]7X'NS.=7'P/'W57[4KPSIQIBR6AIN.%M=X^Y@%:AD@ XM#)]#2R;U32#Z;?AY XMU,1Y3G?KASF'(DQLE*V5C&]52V19$E<2&-MP!USS\L?]4F7^W4KH3.#AP6@62YUK9&4T[>X.V/@?5;7(=!L/ XM,UFHH<85&DM(?C+5E33F2"1R4,;I4.B@01XU@.6.6W(5DVA;CJ6VTE2UJPE* XM=R32)&]T;@YAP4Y.`(U5RT7J:=&N$"UW&2Y/@SUJ9BR7O]PPZE'%W3JN3@*0 XM>%>`K((4#G-,6EGI"U"XZECH:`,"P.J7(>&XD3U(X>!)^RTE6_WB-N=,RMLL XMJP_,1I6$\.[8`?FX4/?7S0C;H.9\\ XM?9I@=J&KHVC-*2+HZM/M!!1&0<'*\ XM^O(=!).`3];Q!^+?J<$1=Y"83$TX)3/T]9G5A,SM`-OJF`VHE6GD@ XM?V/"?G\.%IMT6RS;J'&7I:W`A:@1Q)]W)R.8Y\._V21L:F.TW5`;;]BCKPXL XM8V^J/&D;I._3++/1)BOK;5@@E/PJ&>2AU%76(576[&Y*=#I("@WG.#T]4CGG XMTK3.C[W13N;3S88]HT'!^GJL[ZDHJJ%I?'\37'4\A1VO=+7F]6&$[:0VIZWM XMN%UIQ7"3QX42#XI`WS2=EZ1UFPEUZ18;AW205K=0T5H`'7B3D?K3BGZC?C=H XMUH;C-K6W`E)0$@;J43A:N?AD>@(ZU8NV#6^I-.ZF;DLPX[]F4A+;+6Y333-/:&YR3MH@5TL;K;%%)([61H.!QG.$C='Q;O-D) XMM"UJ::4`MQ.V66NN3T4?#I^=,8(:;2EIA(#38X4`;``5Y6^$JU0G#)2T;K<' XM/:)Q;`"4J4<\*0.0'(?GUKT!RG.<['TK$[[<_?)RQA^!OY^J?NG+=)34P?,< XMN/Y#POHY8\AL.E;T)N"!R\2:U&NX0AV3,< XM[J''27'U_92!R'B3R`\:IUO[0+W;=6N:FMZFV7%([E$=:>)"8X.S7Z`DC&]& XM.B^F9;S5=V/@;J?V5B]76.@AR[S[M*TQK=2(BW!:+XH8[AQ0P\?N'DOTV5^6:N+[+T=80^CA)^%0^% XM7H?[']:UB:D?3N['#&$K15+9AW`YRL****KKNN:/XQ)TE5[M-L[P^SB+WP1G XM8**U`_GPI_(5S[@?+]ZZ@_BPTG,G1(.J(;2WFXC?L\L)&>[1Q$I7Z9403TV\ XM:YB"3@>.`*H5=*7_`!!/73MU9%&(2MQN0W+"43'"V^G'!)Z^07CWM<-/YLG22**LCPY.)A04D$'B!!Y)#(GN/7NURT1[DIM*6W5HXDH((XB1T)2,2W<'S&!X"M6VRWH;X=95PDDY!WXA6]+G+ XMEA`[M#3:1LE.PR3DGUJS-U/6OMQH#L3DNY.?/D^J5I;%'+6-J'.)`&W&FR]5 XM.K?>4ZX2I2E9)/2O5L%82`.(G`'@:U&SU\,[GI6IJN^HT_95R4J_UCV41Q]G XM;=?RSMYTO4=%)6U#8(ADDHW+*V&,O=L%':TO3#MP1IYESC8CJ[R60=G71R1^ XM%/[^E1:^.2CZ#NW-O@R`?UJBQKD&I2GG7``H'B4H\JWK-.ZQ]?4&0_*-E[WJ+=V5+>;LKP: XM0.(/)"DXQUR$XV\Y60HLNMF'[O:CA(D'"I#(\R3](/7WO,\JB7M XM97B\2Q8-/0ESUNY"(C#1+>/O`;NXZ\6$=2FKGH/^'VW,`7SM&G-M-#Z0V]EP XM)'/_`.1P?_E&!YT4KY*4Q8JAKQC=<:`3=WX7W/">UEF6C4=E;OFEKDU=+<]JEG6 XMVW6EM.H2XVM)2I*AD*!Y@CJ*YF[<^Q5=I6]J72$=;MNSWDJ`D%2H_4J1U4C< XMG',>8Y=.45Q:["LM<6G+3JOSI"3@#R2/W_XK$CKY5TYVY]BB)Q>U+HV,E$O/ XM>2K<@82]C1Z;['FAQI;2RRXA2%H(0I*A@@@D$$?*JE31M>.YB<; XM-?RTB.4ZK[$E+CDIX4NLKQQ-+^%7GY'GN-ZF;-ADCOF%G=/XO XM[*&Q\N50&-O45G&>=CO!UE?`L;#;(QUR.1'D:7JBFR""$^Q2QU+,'7*=6G+U XM!O,7OXC@"A_4;/QH-3C1Y=.6YZTC;=(=;DIFV9:HTM'-@$D+'4I\1XI._AGH XMR]%ZMB7M(CN%+$XY-YNJI/=K6WGA:;2L`(2.62?\`.=6'M*U#NBR0U?>? XM*3S(Z?+EZY\*@]+Z(OVJ7DAE!C1"=WE@X(\A];]O,5H_0/3[:2/^H5&A.V5F XM74ER<]WNL6OE51EU;\U#0W.M%E2>/ XMV?.'5_\`UY)]5;^56G2^G](:%6EN)'5=[YR/"`M:5>9Y-C]?6IV?)FSEI1?Y XM2FT*W;M4+=2ATXO^\"M`J+L[';#IZI;CH6Y#I/\`"GM.RM.Z3C&S:%L[O$QC3UN/P1VSQ27_`"`&Y^7*@0^KGY^HIA]@UMOL&UW.3=6WF6) XML@/L-O'*R2,*6?-6,GSH4VXLJ7F.(9`YX5]E(Z-OM'Z>B9=%%%=5ZBE%VW=C XM\/5R'+Y8$-1+^D<2T_"W,QOA7@O[W7KXANT5]-<6G(47YYW*WS+;/=MT^,[& XME,+#;K3J>%22"1@C\JT\93GRS7:_;!V86K7L#VA'=PKVPC$>8$_$!N$.8YIS XMUYCIX'D'4^GKKIF].V:\PUQ9;)2.%7)2'C(W!..1'4]?7UHDI"EW![XPV1D)1GEGGL,G;P%#Z:V^TDR[8)BO/43 XM8:7\/5SM`IFTZ2LMA>_F&IY7MD]T93&2.)2O((_NK`JT/S)[\9/M+J;!;5;( XM9;.9+X\-M_D,<]ZC+4PEM#DFVMAI).7KM<>9/BD*_<_E4G;&\ER9;T=Z0.)Z XM[W(X2D#F4A73UP!X4S.=H`=@LK.KB>2MN"V(<,*92BQ0%;=\Y[TE[\(Z>@R? XM.I".M$&WN34%JQ6X;NW":H=^[GPSR)^9-4V?K&VPY"S8FC?[D/=7RZ6U7KNYIF2E/W!23M)DC@CL@]&THNK0[V<`[W>FP^ XMI5N*C+@GB6L^+:#N3X<7Y5AI70.IM73_P"9 XM23(PX?I+C/W61]Q)Y#P'3P%-K1G9=8[(42I__LYZ1\;H]Q/X4_YZ5?DI2E(2 XMD!*0,``8`JG[I)4'NJG9]!M_U6VOCATB&ODJH:-[/-/Z<`>2S[;.(]^2^.(D XM^0Z5<***OM:U@PT8"XN<7')11117TO$4445%$54^TS0=EUW9?8;FCNI+63%F XM-I'>,*\O%.>:3L?(X-6RBO02#D+PC*X0[4-%7O1+DNVW9C!#:UQY"!EM]L9] XMY)_<-+\:BPTN08ZT))R4.8!XAO XMZ59]L!&2!\7ZKQ_=)AKCH%\NNH+5"D\"W%:ENS?PLLD)B1SYD>[MY9.V^*U( XM=JU;KZ:AI]*YC2593&8!;AL>OVL>))]:9V@NQ>/#8:=U&XTI2<'V2-LV#YGK XM_G*FY;X42WQ4Q8,9J.PGDAM.!_W09\=15'\=V&^!_LJPP10_*,GR4M]$]D5K XMMH:DWU:9[Z,%+"1PLH^76F8PRU'92RPTAIM`PE"$@`?*O2BK<4+(F]K!@+X> X1]SSEQ111171?"****BB__]D` X` Xend Xbegin 644 bg.jpg XM_]C_X``02D9)1@`!`0$`2`!(``#_X0`617AI9@``34T`*@````@```````#_ XMVP!#``4#!`0$`P4$!`0%!04&!PP(!P<'!P\+"PD,$0\2$A$/$1$3%AP7$Q0: XM%1$1&"$8&AT='Q\?$Q)!P>'Q[_VP!#`04%!0<&!PX("`X>%!$4'AX> XM'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX>'AX> XM'A[_P``1"`)8`E@#`2(``A$!`Q$!_\0`&P`!`0$!`0$!`0````````````$" XM`P0%!@C_Q``H$`$!``(#``,``P`#`0$!`0$``0(1`R$Q!!)!(E%A$S)Q,T)2 XM%('_Q``8`0$!`0$!`````````````````0(#!/_$`!L1`0$!`0$!`0$````` XM```````!$0(2(3$#_]H`#`,!``(1`Q$`/P#^F@`````````````````#8`)I XM4M043L4-;8Y.;'#JKS9?3';Y_)EER9LVK&^;DF=Z[CY,O725FQHE XM"-,B:V+^=`EZ#WU=:``````````````````````````````````````````` XM````````````````)!`5/S:R.7RN2<<9M6//\OEN4^L9X,=8[KGQ[SY-N^5F XM,TY==-2)GE^,D(YVM*N.7XSH-7&L^*6;<<<\^++J=.LNF[]X^=GQ98W#Y&=Y[:Y]5J1TXY,>\ XM77GIFQ]&=^*\?!SWJ9/7,L``":`(H`H````````` XM`````````````````````````````````````BRZ$`9^1E,./;5NH\'/R7+/ XMZ[8M:CG;>3/;O9](G%C)CO\`4MMJJSCE,O%_6I4Q0%9P`%````````````````````````````` XM```````````````!-J`$*`GZK')9,:#E\WEF./\`%Y>/'[9?:IGE>3/7XZR? XM7!QZK+5WB XMW&YE^.DZ1QXN7+"ZKVWGY.*9N/\N*[G;I.F;'TAYN#G^W5Z>F:OE= XM)6LZ)=-RC XMGR<64[Q7A^1>/^.3KCEN]^,OAYY XME)N]NDK-CO+TA+OP:9771I.S=_07<3]6%``````````````````````````` XM````````````]32BH33.=^LW5O\`;S?,YOMC]8Q:U'#FSN7)_C6,U&./']K6 XM5TY=5N00O<(YK#8:78I"TGI065"""[Z2Y&5U''++=60=?LU*\_;6.6E\FNU$ XMQRVU6;`BI%0!-KZ``F!LI%V*GXUAE8A>UE1K+&+K]F\;+ZZ3I, XMD] XM`F_QI%`$`!0``````````````````````````````````U1+;,=IIC'-RSCF XMJ\'>>=OXW\K/_DR,)]9TY]5N+YCIGU;>QRK:165GB!^E%$(?I/2HJ_B;D],? XM&.2K(:SR9;\7@XKE=LXS[74>WX^'UQ[=N>6;TF?%+CU'EY.++&[>_P`\2XS/ XMJM^6/3Y\RL=,,_[;Y^#5_C'"2SURZY;E>CV$K XMHT"=K#8@:#:$&IE=_P"+GCAG/.V%G3?'?M.HZ\/R.]9>NDOWFLO'+ XMEX).\72=)CV\>4SG2WKU\W#FY>++5\>[CY<W6=,6-_JFOZ\&D//1)WZU XMJ:$0`4````````````````````````````````CS_*YM8W%UYK],?MMX.3+_ XM`),]L6M1./'=MK>]=$_C&=[KC:W%I#\(RJ+#HH@5)ZHI.EG8END@N74<,LMW XM37)ELXN/[9.O,9KK\;CU9D]67?B828X:(]',<;2=+YV*WC.K['+EX9EVZ>-1 XMSO+4Z?.Y,+C4QRL?0Y./'*>/)R\%QNW/KAUG1CEMN//NRMX9.-C>NO2^,2M2 XML8"Q-+`2^BFP14-@IZBSL$O3>&3&KM?%T:SPG)/Z>;+'+BRW.W>95TWAE-6- XMSIFQGX_R=ZQO3TRR_KP\O#EO[8IQ\MX[K)UG3-CZ`YVI$3VO9\?"XS=B%QF4U7";@GY'DRPRPO;Z$SW&,L,>3USO+I.WCQR=<W.\NLNO1LVYX9-S7K%BJ+Z:9#06DH!>O$I`79.TJP$JSH/_%T; XMF7Y6>3CQSFYZBXVXM2ICAKDXKN>/3P_(EDEO9=.4]=)=9L:#R;/\`6F5```30`4`````````````````` XM`0%J:_2K+KU*,YW6-KPNF\W/M=-2IB\G%,YO'IY]Y XM<.7>Z]$RLOO3=QX\YVWSTEB\'R9R3ZNWCYW+QY<5^V/CT_&YIECJWMVE9L>G XM6Q)N*TR````````````````````";4``$QWMCY.4QX]R]NENH^?\C.W.QGJK XM&-W.[J[T8SI-]N-K<4!EH`0H;#0@"P%ZD<>3+;7)EKIQ[N73?/);CKP8??+5 XM>K_K/K&>+'Z82MR?:O3S,<>NG3AQWV],\<^''ZQTBUF#.?\`UK3')_U9H\>= XMW4,YK)FL5FI1*1$79N@HEJ54#5BVT38:N]-\>7VNJY7M<+JF-2NG)Q?TY7&R XMO=Q:RP<>7CC%Y=9TX2MRLY8:)ZY6-SIM8S%E9;6Z-Q/30A2>A$P70GZLH%ND XMW:H:)^M3U$GJRE=>7^?'IY./^'/(]F/CR= XMVOD<[6Y$I/2*YU03:Q%T``A0O0BQ,^IM9XYY9;NJLBLY7[.OQL)^QSX\;CB.7=-[OUCMP\;'!A_+;U34FG5R6=%$M8:+6,_%M<\ZN,VO/S=5RV XMZO"?3&-%_T=6``````` XM``````````````MZ"3:$36I]GC^7R[NH[_)Y9CC<7A[RNZQU6HN,_3?:7J$[ XM<];BTA_AXRI5B+"HF^U!!9_:7NGXE6!E=1SUNK;MTXL=7=;YC/5;XIK%J;M; XMQX[E=SQZ>/CD]CT3XY7Z<6.L8WI?PT:2);IFW;59RZ6):SE7+DO36=6L>HOJZT?ZY=5VYF`J1S=%OB`(;"19`1=&P"INE:PQVHUQ XM8;NVOD(H+*5(H$3(J3NK()AA;7KXL)9ISXY8]G#A-;= XMN8Y=5OCPDC4(K;(EJ[8RJPI:YY5;7'DR;D8M9RRW6\,=N7^NW'>EK+A\J21P XMCM\NN..G'HIMFTRNF;62+L9VNS%7:40-=,+IU][<)U%QSN]#4JNT7*7%]'";]<^7BE\CE88\%NV7IRX:Y9<.48QF.<7:W"Q-=JU(G XMK4Z-,VW\2H99?C?'C^F''^UGCQTY]5TY^KKIFM5FN- XMKO(1=I2(ILA811;`$"H"P-?JX]W23W3MACJ;6%:RUACMX>7*\F?U=?E*W+[5VXY8UZ/B<7UP[=ZS+KJ-5UD8M#\!I`*(0`%`````````````G90`` XM''GY/K+'3ER^F&W@YL_O_)&^8Y\F7:3..7)EMG'_`-KK^DVWAC^M$:QDD_U>]I?6I/UF XMUJ36M_6+AGCE=;<.?/73EP[QS^VW/77Q\>GEPOVZB?\`';['HXK]L=M#G>7F XM_P"#&^I__FP]>BZ9MG]KB//G\?&3IY\N'5\>]G/&67;-B5X+E]9IS_[5OY$U XMETO!AOMRZN+(Z<.&G6W46>,9>N/5UWYYPM2+!AT*1+5A!*L3]/%"K#TO4`L0 XMG:XS=)!KCQ[VUS9S''2VS#!X^?.Y9.O/+-IQ8WDS?2XN.8X1Y/A68WM[I9?U XMWG..=K&6/ZDMVZI<=M(S^'^LZNUE_$%"@H!H`3:@``````````3H"B02=;M6 XM./R>34Z1N1Q^1R;RL>3//^6FN7+??Z\]N[MFNO,=,\-S< XMO4='*?VZ89W]<]6=40L=Y=M.&.3MCE+&;&;&B](,XS8LIH\-HRL3:H@H`*LG XM;*RK^#KA;BZXY[CA*LNO&N>L9ZY;Y-N<=)W$RG]/5QVX=ZZ XM6LR+.URR^N)-2.7-E^.7==OY\[7+/+[4PR[TS=)O7;EKT6?'T?CY?QTZUY/B XMY;CTY?\`5TCS]./R<_K.GCG+E,][Z7Y%SN5WXYR;CM'&O9Q\TR3Y/+,<7DEN XM$Z8SSRY+JN?=)]7"7DY-O7AA]8QP828R_KM;T\G5=N8ENF-[NBU)ZY.T*`H6 XM+"T@&RI1`\/5]2]*+.G3CQ_6./&VM<^O@^1+_`-JS>5E>NZK% XMPM\7#*9>-,8KEWCZUO;=DOKG99XBKK0DO]K_`.*'2?JB``*`````````0$Y+ XM,<7S^7/=NW?Y>7UNMO#S9,UTYC')EVR)6:[0L;PS^K,$:=OK,Y]OUSRQ9QMQ XMKKC9G!7'6FLY7?GMRO*9_\`5YL^ XMZ].7+'ZWSI[;=QY?EWBQU'F XM[Z;YY:QFNOQG.]]-9WIRN[7GUWD4"(T`:`6(OA0HB^F"+)NFG3&:C4*LLPQK XMQJFTJ_4L;837^IYY52])BQWX? XMD98]/=Q/=?)]7#/+'+WIB\M2OLR[\5XOC_*GE>S'*6;VQ9C6IEBSWBZQ XM+CM-&)=B7'2;1IH63K+KP$'7#*?_I,\-]SQSG;IAG?+X*YY=&-KKGA+.G*RP2NN XM&>G2?R\>9KCSLZ2QFQZ)UZNF<;,EWIBQBQ?T/P93`!46(L\1%)=-XY,G[T#M XM*NZY8VNF-AN,V.F-7F XMS^V3KQRQ:S;][VU)I,9IK;U<JZ63..5T8Y644^M XMF6BSIUW,YJ>N6>-QO8EAAE<7:9?::<)VLOUJ6:SCTX^*Y8GXSCI,M+=9QGJCMQVQURS_P`4EVY\_)]9 XMJ.N=LQ>'EMRR=;WKEY,,;ED]O%CK%RX./J5Z=S&//UTZ<\LY74<[VN5[3US= XM9%@GBB@``!194JZ1($=,9J;9Q3ES^L=)$M8^1R=:CSX^F5WD;>G^?+EU727H XMGK.*UZ)'&UHTS*LJLFDNV]PT*YLYS;K<6;QVB.'UO]EQ_P!=[Q9,_P#%DNM. XM.O\`3>G3/'ZLZ56=GVOI8B7DUZ.#Y-PKZ'Q_D3DZO3Y$TN.>6-W*Q>5E??W/ XM[*^9\;Y?UU,J^AP\N/)-QRLQHN#-_C75,IL'/>U3+JDIBJ(J*``C'R/_`)5O XM3//WQ4(^3G_VK%E=>3'NN=OX-RI?$D6D9;E0A0QJ4"B-:`(H0#6EQMEZ=)9E XMZY3U?]0:RPO_`.7._P!5VPY/RG)A-;@CE.G7CS_MRUH$L>N67P>?#.QVQREC XMG8S8VG9%]98L/PD/PE$4$%4VA$&\F+#&?6, XM97MK.].>]U&I"^DT5%59ZK+2";4`%B""PUL]:Q]:D2TNL<>WEY,[E>W3Y&?? XM3SVO1_/EBT)ZBQZ.9CEU6Q-GK>N:QCDRU>FI[IG/&6J+CE:ZXW;CC-.O'K8. XMDCIA#&?JW+&3U!OZ_P!IE>.37ZXY<]\D9F'W_E;HQ=9YL+EWB\^4L]>O+.<< XMU.WFSR^U:@YVFJU<9.V=WR1=$JS'++J.W%P99WN/=\?XDQ[KGUU&Y'D^/\;* XMV6Q]+@XYQS4=,<9C%KE:H;3;-R,$Y/[<>3/7AS\\NP>G#O';43&: XMQ76XB@D!53*?::4!\[YF'UKRZ[?5^1Q_>6OFIE!'+QK#+59SQL)` XMQZ<,]M[_`*>29:KMAEOUBQFQU]32RQ6?QBQ(J;-HBHJ2(*0VL!9=+]F:1,1; XM=H4%&D+X"-)%!-JD4`#](-8Q.7+46V8XO-RY[KMSRQ:QGE]JS2U'IXYQRZH2 XM@ZL5N"8KM<3"U&UQDQ3//\`H5O< XMQG;GER_D<[E;4Z_1&K;;VEJXXW*]/1P_&RO_`&B6K(X8<66=>OX_Q-=UZN+A XMQQDZ=9TYWIN1G##'&>-]2)TSEEJ,_JM,Y9R.67+)UMRRSM!USY%PRVX8RVN_ XM'CKL%SQE8DF-=,JY6#%Z$QVPS_MUQNWDE=,,M,V XM,V/0:3'*6*QC%BG0(A2`@M(FU``0%V@`U&5T"I5@"1K23U.3+4;YB6L\V76G XMGM7.]L6O3QRY=4HD*[R.=IL0\6(NT^U3>ZW^*,R;;QQN_#&.LSF,$)A/TRRF XM,Z8Y.7;E;:HZW/;&TFW7BX;G?$M,<]6^._!PW._RCT\'QICW7IQDG4C-Z:QR XMXOC8X>/1.IK20MTYVM1:GVU^N>?)IQRY=HKMR.# XMICBU=0&9C(W'*Y]MX7:8,\V64\<<9++#*] XM=`YQ*?JT:E/Q/U94_4QN4HM32-2@+ZC4J`#0`BZGI+92>J@WCE_;6>/V\<;6 XM\,[/02]=([98XV;GKE<;*"XY65WPSECSW_#'*RI8S8]@Y89].DKG8S8NU]2P XMC+)HB@```""Z5-H&M"2-19#2]1Y^7/MOES_'GRNZ[\8Y XM6KHU4N2?;)MA;9&;=U9-WM=2*)CC6XDJ=[$JVFSHF.5O26F,UTX^*Y>1VX?C XMV^Q[>/BQQGC.M2//P_&U_P!H]>&&,G44C-K6+%W)&5!TRS8N5ICC:Z8X0&<<;7 XM7":7J("YY3UY^7DF5Z:Y=L<6&[V@]/'_`-)LO?BR:QTGB+#L-@J@`"*";5*L XM\`CES<4Y)ITB_@/D_(X+AEU'!]KDX\LQ=HL7'.XUUZSCD8VRBKEA]:R[2S*,9 XM8_H,3+5=N/-QL)N%C-CV8Y;:V\V&>G?#+<<[&+%V%(PR+#8`"^((L]+4BP:C XM.66FKU'GY,MNG/+-J9W=8I4>GGERM,^XQ]6QUC&LS!>DM9NU1JUGVGXN,O\` XM1JK)I>[^-\?%ED]7#P3'NQ-,<.+@^WKU\7!,73'&3R-2,VKACJ?C26R.6?+( XMRKK2U)+:WA@#,QM=,,&I)"Y3'M!9C(992.6?)OQGNJ-7.[=>+MC#!VQDB:)R8Q XMSPLE;Y XM!^#+6@`LH0$Q386)/15EL=<,I^N27:#KGCN[C%FF^/+K5:RQF4V*XMX9V5FR XMQ!+'JPSW'25XL1RM6W^F;:MJ>M,B::F-WU'?BX=^HTXX<>5KU<7#_< XM=L.*8QTD9M6)AA)XW(>,Y92?K.JW>F,N21QSY6-W(&\^6WQCO*]KCQNV.$!S XMQP=<,--36,8SY=>`W5VQ>FY';/FN76W+*?6[:QQ_:SR7?1*N/;\7DW)'I>#XO5>Z>-)5H@N XM(H```F@`H```!4NK-6':]`\?R?C?;^4>#/&X76GVKWU^//\`*^/,IO&*FOF; XM/8WS<=PZ66/#+KQTPSLO;-C-CU'_ XM`/U,,I8NF/+FMZFW#DRW6^7/K4<,KJ:=>>6+61(NNGIYCC:AI8UCQY6M( XMQ)77CX;D[>65Z0= XM>3GGCE;,#6I)&6=OB3&WT%N5SNUF XM%K>''IO&2`SC@Z:DC&6;Y&_*\]RSROO3%Z7'?FY[EXX7[6^KCC:W))ZQ:UC.,;U XM,9M+E&>ZBM99[FHQCC;73#CM>GBX9_31J1SZ:SO>W+*[JY7:8XVNDCA:N,:^M_'3CXW?'#3H XMPX\?%^UWPPU&L<6Y(S:N,XXM]3UC/DF$<,^6Y(._)R2>5PRY,LF9+;ZZ8X`Y XMS'==<,&\,/[;NL8"3&1;EC'+/EVQ=Y`UGR7?7B27+UK#!VQP@.6'&[3&2%RF XM, XM3EO)XS>C'HYOD7_\UYLL\L_4QQN^V],6M2,3!TQQAN1FY6UEIJW7C-MK6.%R XM=N/A:Q''#CM>CCX?\=N/CD=9)&I$<\..1TDTHTB*`````````````````)5G XM04&.7CQSGCYWR/CW"V_CZ<_U.3"9SL'Q?W2:>OY/!<;N1Y,I95#*:7?1372- XM2H4GJU,;E06)4QJ4"%1N4`%`$$T5:1!O#+7K.?=W&:UC_K4B6M8>=E[:QQWX XMZX<<_6I''JN6/':[\?'KUO''^FYC6XY5)(UC%WC)VX\O+_\`S51URSF+CR/EY\KY6;T8]7)SXSJ/+R\URO5<_P"67K>.$8O34C$QRR]= XM,,-+UBF>77\65QJV1FY;\3''+*]NV'#N]1J17+'"VN^'#N.W%PZ]CMCC(U(F XMN7'Q2>NTDBI=M8FJA%_!``````````````````````$I+OI0$SQF4T\/ROC_ XM`%NY'O+)9JD1\//&R]D?0^9\;1+5QZ,^3'&=5Y.7Y-MTXVY97U<<7. XM]-8EMSNVL<6YCHN3+1)$N6O$[R=,..U9!SDN3KQ\3OAPNV.$C#&S6&7;/_CZO/P8Y8V_KYO+Q986]*,2)UM94U_(65.>\LZ#>7);XS-Y7MK#!UQPT#GCQNV&,TN\9.ZX?/GRRZ8F.[NN=Z7%SY,\KZ8X[;QPGM:ZDZ8U4F.EMDB7+*^-887)9% XM8W:WAACCXI&YRFN''P7^GHPXY/72="XFIJ?B@J``H`F@```````H XM``````````````````````&]`!K?;ES<,Y)TZ_\`@J/D<_#<+7*73[/+QXYX XM^=OF_)^/<)C4H!4:UFUKCGVK&3T_$DUVU&>JN.#KAQ XMM;D+RXR*Y-221C+EUTY99Y6])CC;>Q5RRN2X86^MX8.LD@C..#@QAA;ZZXX:;U(QGS8XS2:KI))'+EYI@\W+S7* XM_P`:Y?RRO=8O2R-\G-<[TQJWUK'!TDDC&U<8QP;U(EO]$EIFJ7+?1CA:[.16&,.-TF,GIEE,8Y9\EH.F6>,\KEGR9;U&9+E77#C#7.2Y7MUPX_P!= XM)A(9YXXSU%)CKM,N3#&>]N'+\F_]8\^65SK-Z7'?EY[^.%RRSO:XX.DQD8M7 XM&,<&Y)"W1-U/UHM)+6\..VO3Q\*R(\_'Q6O3Q\,GXZXX2-_C<2LXX2-`J`2` XM````````````````````````````````````````````%Z@'Z!ZEDOJW_`1X XM_E?'W+9'@RQN%?;LEFJ\WR/CRRV0'SM[2]-J[?%O2I7M_YL9^,YGD^1/'ESSRROI,=^MXX:8O3.%KMQ\-_I9$URPX[7?CX7?#CF,[C*9SI94?%Y9WLX,_K=/3\KAN/X\5WCDUH^KQ27';?\` XM&?KP\/R-336?+'%(ZR20HTB7M0``````````` XM`````````````````````````````````````````````!/U;JB`3?ZM_P`+ XMV3T'/DXYE.X^?\KXM]D?4M[Z2XS*=KH_/Y\>6-7#+7KZW-\;'+R/+G\75\0> XM>@!-?JWOH!#P`4C.4EO@`?2+)`$5/T$54GH"AK8-(+OH$`!(`"@`` X4```````````````````````#_]D` X` Xend