#!/usr/bin/env perl # README: cannot use plenv & v5.40 as Image::Magick will not install from CPAN # requires Image::Magick - easiest to install libimage-magick-perl =begin creates 'macbeath' style wrapbootstrap (v3.0) theme from dir or list of images in .cfg can use create_config.pl to create .cgf file from list of images extracts exif data from exif_data db (use extract_exif_data_to_db.pl to get it there) usage: $0 [-q|query; -v|verbose; --force-img; --no-title; --dry-run] -d|dir path/to/dir (where new index.html is to be created) idea based on exiftool util (http://www.sno.phy.queensu.ca/~phil/exiftool/) exiftool command-line usage: exiftool -@ tags.txt -h -s . > output.txt list of canon tags: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html =cut use v5.26; # say use strict; use warnings; use autodie; use lib ( # using system perl & local-lib - cannot install Image::Magick from cpan any more '/home/raj/perl5/lib/perl5', # '/usr/share/perl5', # these are already in system perl 5.34 @INC # '/usr/lib/x86_64-linux-gnu/perl5/5.34', ); # to check which Perl we are using: # say $^V; say for @INC; exit; use Data::Dumper::Concise; use Image::ExifTool; use Image::Magick; # libimage-magick-perl use Data::Printer; use Net::OpenSSH; use Getopt::Long; use File::Rsync; use YAML::Tiny; use Path::Tiny; use DateTime; use Template; use Net::SCP; use IO::All; use Local::DB; # path to .cfg file in web-page root under ~/photo/html: my $dir; # table truncation and full rebuild my $debug = 0; # switches on moddebug # force-overwrite images: my $force_image = 0; # dry run, doesn't transfer files to remote: my $dry_run = 0; # no titles/captions - for pages with single theme eg bluebells my $skip_title = 0; GetOptions( 'force-img' => \$force_image, 'verbose|v' => \$debug, 'no-title' => \$skip_title, 'dry-run' => \$dry_run, 'query|q' => \$ENV{SQL_TRACE}, # switches on query trace 'dir|d=s' => \$dir, ); # p $dir; unless ($dir) { say 'error: require -d flag + path to web-page root under ~/scripts/exifdata/folio/'; exit; } # warn $ENV{SQL_TRACE}; my $rsync = File::Rsync->new( archive => 1, compress => 1, update => 1, moddebug => $debug, # turns on verbose output # recursive => 1, # rsh => '/usr/bin/ssh', # 'rsync-path' => '/usr/bin/rsync', 'quote-dst' => 1, # doesn't seem to have any effect ? 'quote-src' => 1, # doesn't seem to have any effect ? ); # host domain: my $HOST_DOMAIN = 'volvox.uk'; # path to remote images directory: my $REMOTE = '/home/raj/images'; # full path to local images: my $LOCAL = '/home/raj/images'; # local path to full-size jpgs: my $SRC_DIR = '/mnt/nas_images'; # full path to local png thumbnail images: my $PNG_DIR = "$LOCAL/thumbnails"; # full path to local jpg carousel images: my $JPG_DIR = "$LOCAL/medium"; # path to local index.html dir: my $LOCAL_HTML_DIR = "/home/raj/scripts/exifdata/folio/${dir}"; # path to remote folio dir: my $REMOTE_HTML_DIR = "/home/raj/www/photo/html/folio"; # config file containing list of images, headings, etc: my $cfg_src = "$LOCAL_HTML_DIR/.cfg"; unless ( -e $cfg_src ) { print "error: ${cfg_src} doesn't exist\n"; exit; } my $dbix = Local::DB->dbix({ dbname => 'exif_data' }); # p $dbix; exit; my $yaml = YAML::Tiny->read($cfg_src) || die "Can't read $cfg_src"; # warn Dumper $yaml; my $cfg = $yaml->[0]; # warn Dumper $cfg; # diimensions to create thumbnail png images: my %dimensions_map = ( portrait => '133x200', # 2:3 ratio landscape => '200x133', # 3:2 ratio carousel => '900x600', # 3:2 ratio ); my $query = q!select t.name, it.value from images i join ( image_tag it join tags t on it.tag_id = t.id ) on it.image_id = i.id where binary i.img_name = ?!; # list of required images from config: my $images = $cfg->{img}; # p $images; my $folder = $cfg->{dir}; # p $folder; # alternative to $images die q!require either 'img' or 'dir' entry in config file! unless $images || $folder; # use directory if no images: if (! $images) { my %h = ( path => $folder ); # any images to skip if using directory: if ( my $skip_img = $cfg->{skip_img} ) { # p $skip_img; $h{img_name} = { -not_in => $skip_img }; } # p %h; $images = $dbix->select('images', 'img_name', \%h)->column; $query .= ' and path = ?'; # p $images; } my %h; # holds data for tt IMG: # loop through images list: for my $img (@$images) { say $img; # next IMG unless $img =~ /2107/; # get exif data from db: my @bind = ($img); push @bind, $folder if $folder; # warn Dumper [$query, @bind]; my $exif_data = $dbix->query($query, @bind)->map; # warn Dumper $exif_data; unless (%$exif_data) { say "no db exif data on $img, aborting"; next IMG; } # get img folder from db, unless already using directory of images (ie $folder): $exif_data->{Folder} = $folder; # will be undef if config imgs, so get from db: unless ( defined $exif_data->{Folder} ) { # keep $folder scope local (or @bind acquires it next time round): $dbix->select('images', 'path', { img_name => $img }) ->into($exif_data->{Folder}); } # p $exif_data->{Folder}; # convert ugly DateTimeOriginal (2013:07:29 20:49:15) to DateTime object: $exif_data->{DateTime} = _get_datetime($exif_data->{DateTimeOriginal}); my $imgHeight = $exif_data->{ImageHeight}; my $imgWidth = $exif_data->{ImageWidth}; # portrait or landscape (exif data unreliable - sometimes img has portrait # dimensions but exif Orientation tag is 'Horizontal (normal)'): $exif_data->{Orientation} ||= ''; # LR5 doesn't export orientation!! my $orientation = ( $exif_data->{Orientation} =~ /rotate/i # unreliable - see above || $imgHeight > $imgWidth ) ? 'portrait' : 'landscape'; # warn $orientation; # get lens info: $exif_data->{LensModel} ||= 0; # p $exif_data->{LensModel}; $h{lens_data}{$1}++ if $exif_data->{LensModel} =~ /(\d+(\-\d+)?)mm/; my ($prefix) = $img =~ /(.*)\.jpg/i; # warn $prefix; { # for perlfect search to index image number: my ($prefix_num) = $prefix =~ /(\d+)/; # warn $numerator; $exif_data->{PrefixNum} = $prefix_num; } # create png unless already exists: my $png = sprintf '%s/%s.png', $PNG_DIR, $prefix; # warn $png; # skip if png already exists, unless force-recreate images: if ( $force_image || not -e $png ) { my $src = join '/', $SRC_DIR, $exif_data->{Folder}, $img; # say $src; my $iMagic = Image::Magick->new; # new ImageMagick object for each image my $x = $iMagic->Read($src); # warn $x if $x; my $dimensions = $dimensions_map{$orientation}; # 2:3 or 3:2 ratio =begin # alternative method: my $factor = 200 / $imgHeight; my $dimensions = sprintf '%sx%s', int $factor * $imgWidth, int $factor * $imgHeight; =cut $x = $iMagic->Resize(geometry => $dimensions); # say $x if $x; # if portrait img, rotate (unless already has required dimensions) & save: if ( $orientation eq 'portrait' && $imgWidth > $imgHeight ) { my ($angle) = $exif_data->{Orientation} =~ /rotate (\d+)/i; # say $angle; $x = $iMagic->Rotate(degrees => $angle || 90); # in case used height vs width } $iMagic->Write($png); # warn $x if $x; say "creating new png image for $img"; undef $iMagic; # returns memory asap } # create carousel-sized jpg unless already exists, or force-recreate images: my $jpg = sprintf '%s/%s.jpg', $JPG_DIR, $prefix; # warn $jpg; # if ($orientation eq 'landscape' and not -e $jpg) { # portrait no good if ( $force_image || not -e $jpg ) { my $src = join '/', $SRC_DIR, $exif_data->{Folder}, $img; # say $src; my $iMagic = Image::Magick->new; # new ImageMagick object for each image my $x = $iMagic->Read($src); # warn $x if $x; my $dimensions = $dimensions_map{carousel}; # 3:2 ratio $x = $iMagic->Resize(geometry => $dimensions); # say $x if $x; $iMagic->Write($jpg); # say $x if $x; say "creating new jpg image for $img"; undef $iMagic; # returns memory asap } # add img exif data to %h, separate by image dimensions: push @{ $h{images}{$orientation} }, $exif_data; # get image title if exists: if ( my $title = _get_title($prefix) ) { # return image_cation.caption if exists $h{titles}{$img} = $title; } elsif ( ! $skip_title ) { say "no caption found for $img"; } # transfer full-size image to remote: upload({ dir => $exif_data->{Folder}, img => $img }); } # p %h; p $h{images}; exit; { # process template to html & save: my $t = Template->new({ INCLUDE_PATH => './tt' }); my $output; my %args = ( data => \%h, cfg => $cfg ); $t->process('index.tt', \%args, \$output) or die $t->error; # warn Dumper $output; io("$LOCAL_HTML_DIR/index.html")->print($output); } say "dry-run, skipping rsync" and exit if $dry_run; # rsync local 'thumbnails' & 'medium' dirs to remote =========================== say "rsync $PNG_DIR & $JPG_DIR -> $HOST_DOMAIN:${REMOTE}"; $rsync->exec( src => $_, dest => "$HOST_DOMAIN:${REMOTE}" ) or warn "rsync failed\n" for ($PNG_DIR, $JPG_DIR); # rsync folio directory to remote ============================================== =begin # for rsync'ing directory: { # need to append to $REMOTE_HTML_DIR if $dir is >1 folder deep: my @parts = split '/', $dir; # p @parts; # remove last item & prepend rest to $REMOTE_HTML_DIR if ( @parts > 1 ) { pop @parts; $REMOTE_HTML_DIR .= '/' . join '/', @parts; } } # p $LOCAL_HTML_DIR; p $REMOTE_HTML_DIR; exit; say "rsync ${LOCAL_HTML_DIR} -> ${HOST_DOMAIN}:${REMOTE_HTML_DIR}"; $rsync->exec( src => "${LOCAL_HTML_DIR}", dest => "$HOST_DOMAIN:${REMOTE_HTML_DIR}" ) or warn "rsync failed\n"; =cut #=begin # scp transfer just index.html to remote =============================== { my $remote = join '/', $REMOTE_HTML_DIR, $dir; # p $remote; my $local = join '/', $LOCAL_HTML_DIR, 'index.html'; # p $local; # say $local; say $remote"; say "scp $local -> ${HOST_DOMAIN}:$remote"; # create dir if it doesn't already exist (is ignored if it does): my $ssh = Net::OpenSSH->new( $HOST_DOMAIN ); # needs ssh-key transfer die "Couldn't establish SSH connection: " . $ssh->error if $ssh->error; $ssh->system( "mkdir -p $remote" ) or die "mkdir $remote failed: " . $ssh->error; $ssh->scp_put({ verbose => $debug }, $local, $remote) or die 'scp transfer failed: ' . $ssh->error; =begin # Net::SCP method: my $scp = Net::SCP->new( $HOST_DOMAIN ); # implies current user $scp->cwd( $remote ) or die $scp->{errstr}; $scp->put( $local ) or die $scp->{errstr}; =cut } #=cut #=============================================================================== sub upload { my $args = shift; # p $args; # href of dir & img return if $dry_run; # skip uploads if dry-run my $filename = path( $args->{dir}, $args->{img} ); # p $filename; my $local = path( $SRC_DIR, $filename ); # p $local; =begin # rsync cannot create directories 2 or more deep # my $remote = path( $REMOTE, $args->{dir} ); # p $remote; # $remote =~ s/\s/_/g; # p $remote; =cut my $remote = $REMOTE; # p $remote; # scp cannot create remote sub-directories, use $ssh->system( "mkdir -p ...) # $ssh->scp_put($local, $remote); $rsync->exec( src => $local, dest => "$HOST_DOMAIN:${remote}" ) or warn "rsync failed\n"; } sub _get_datetime { my $str = shift; my $re = qr{(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})}; $str =~ /$re/; return DateTime->new( year => $1, month => $2, day => $3, hour => $4, minute => $5, second => $6, time_zone => 'Europe/London', ); } sub _get_title { my $prefix = shift; # warn $prefix; # just capture numbers (discard eg LR5) # eg IMG_00069, SAM_2480, DSCF1324, 20190807_130723, 20190807_130433_cr my ($img) = $prefix =~ /^(((IMG|SAM)_|DSCF)\d+|\d{8}_\w+)/i; $dbix->select('image_caption', 'caption', { image_name => $img })->into(my $caption); # warn $caption; return $caption; }