5ba15568d09e77017faf41f784ea350c69b46389
[mkgallery.git] / mkgallery.pl
1 #!/usr/bin/perl
2
3 my $version='$Id$';
4
5 # Recursively create image gallery index and slideshow wrappings.
6 # Makes use of modified "slideshow" javascript by Samuel Birch
7 # http://www.phatfusion.net/slideshow/
8
9 # Copyright (c) 2006-2008 Eugene G. Crosser
10
11 #  This software is provided 'as-is', without any express or implied
12 #  warranty.  In no event will the authors be held liable for any damages
13 #  arising from the use of this software.
14 #
15 #  Permission is granted to anyone to use this software for any purpose,
16 #  including commercial applications, and to alter it and redistribute it
17 #  freely, subject to the following restrictions:
18 #
19 #  1. The origin of this software must not be misrepresented; you must not
20 #     claim that you wrote the original software. If you use this software
21 #     in a product, an acknowledgment in the product documentation would be
22 #     appreciated but is not required.
23 #  2. Altered source versions must be plainly marked as such, and must not be
24 #     misrepresented as being the original software.
25 #  3. This notice may not be removed or altered from any source distribution.
26
27 package FsObj;
28
29 use strict;
30 use Carp;
31 use POSIX qw/getcwd strftime/;
32 use HTTP::Date;
33 use CGI qw/:html *table *Tr *td *center *div *Link/;
34 use Image::Info qw/image_info dim/;
35 use Term::ReadLine;
36 use Getopt::Long;
37 use Encode;
38 use UUID;
39 #use encoding 'utf-8';
40 binmode(STDOUT, ":utf8");
41
42 my $haveimagick = eval { require Image::Magick; };
43 { package Image::Magick; }      # to make perl compiler happy
44
45 my $havefeed = eval { require XML::FeedPP; };
46 { package XML::FeedPP; }        # to make perl compiler happy
47
48 my $havegeoloc = eval { require Image::ExifTool::Location; };
49 { package Image::ExifTool::Location; }  # to make perl compiler happy
50
51 my @sizes = (160, 640, 1600);
52 my $incdir = ".gallery2";
53
54 ######################################################################
55
56 my $incpath;
57 my $feedobj;
58 my $debug = 0;
59 my $asktitle = 0;
60 my $noasktitle = 0;
61 my $feed = "";
62
63 charset("utf-8");
64
65 unless (GetOptions(
66                 'help'=>\&help,
67                 'incpath'=>\$incpath,
68                 'asktitle'=>\$asktitle,
69                 'noasktitle'=>\$noasktitle,
70                 'feed=s'=>\$feed,
71                 'debug'=>\$debug)) {
72         &help;
73 }
74
75 if ($feed && !$havefeed) {
76         print STDERR "You need to install XML::FeedPP to use --feed\n";
77         exit 1;
78 }
79
80 my $term = new Term::ReadLine "Edit Title";
81
82 FsObj->new(getcwd)->iterate;
83 if ($feedobj) {
84         $feedobj->{-feed}->pubDate(time);
85         $feedobj->{-feed}->to_file($feedobj->{-savepath});
86 }
87
88 sub help {
89
90         print STDERR <<__END__;
91 usage: $0 [options]
92  --help:        print help message and exit
93  --incpath:     do not try to find .gallery2 directory upstream, use
94                 specified path (absolute or relavive).  Use with causion.
95  --debug:       print a lot of debugging info to stdout as you run
96  --asktitle:    ask to edit album titles even if there are ".title" files
97  --noasktitle:  don't ask to enter album titles even where ".title"
98                 files are absent.  Use partial directory names as titles.
99  --feed=...:    build Atom feed for newly added "albums",
100                 enter filename, base URL, and optionally PuSH hub url,
101                 separated by commas. (Note: PuSH obviously does not work
102                 "out of the box" for static tree! You need a separate
103                 "watcher" script to do the publishing for you.)
104 __END__
105
106         exit 1;
107 }
108
109 sub new {
110         my $this = shift;
111         my $class;
112         my $self;
113         if (ref($this)) {
114                 $class = ref($this);
115                 my $parent = $this;
116                 my $name = shift;
117                 $self = {
118                                 -parent=>$parent,
119                                 -root=>$parent->{-root},
120                                 -toppath=>$parent->{-toppath},
121                                 -depth=>$parent->{-depth}+1,
122                                 -base=>$name,
123                                 -fullpath=>$parent->{-fullpath}.'/'.$name,
124                                 -relpath=>$parent->{-relpath}.$name.'/',
125                                 -inc=>'../'.$parent->{-inc},
126                         };
127         } else {
128                 $class = $this;
129                 my $root=shift;
130                 $self = {
131                                 -root=>$root,
132                                 -fullpath=>$root,
133                         };
134                 # fill in -inc, -feed, -relpath
135                 initpaths($self); # we are not blessed yet, so cheat.
136         }
137         bless $self, $class;
138         if ($debug) {
139                 print "new $class:\n";
140                 foreach my $k(keys %$self) {
141                         print "\t$k\t=\t$self->{$k}\n";
142                 }
143         }
144         return $self;
145 }
146
147 sub initpaths {
148         my $self=shift;         # this is not a method but we cheat
149         my $depth=20;           # arbitrary max depth
150         my $fullpath=$self->{-fullpath};
151         my $inc;
152         my $relpath;
153
154         if ($incpath) {
155                 $inc = $incpath;
156                 $inc .= '/' unless ($inc =~ m%/$%);
157         } else {
158                 $inc="";
159                 while ( ! -d $fullpath."/".$inc."/".$incdir ) {
160                         $inc = "../".$inc;
161                         last unless ($depth-- > 0);
162                 }
163         }
164         if ($depth > 0) {
165                 $self->{-inc} = $inc;
166                 my $dp=0;
167                 my $pos;
168                 for ($pos=index($inc,'/');$pos>=0;
169                                         $pos=index($inc,'/',$pos+1)) {
170                         $dp++;
171                 }
172                 $self->{-depth} = $dp;
173                 for ($pos=length($fullpath);$dp>0 && $pos>0;
174                                         $pos=rindex($fullpath,'/',$pos-1)) {
175                         $dp--;
176                 }
177                 my $relpath = substr($fullpath,$pos);
178                 $relpath =~ s%^/%%;
179                 $relpath .= '/' if ($relpath);
180                 $self->{-relpath} = $relpath;
181                 $self->{-toppath} = substr($fullpath,0,$pos);
182                 #print "rel=$relpath, top=$self->{-toppath}, inc=$inc\n";
183                 initfeed($self);
184         } else {
185                 $self->{-inc} = 'NO-.INCLUDE-IN-PATH/'; # won't work anyway
186                 $self->{-feed} = '';
187                 $self->{-relpath} = '';
188                 $self->{-depth} = 0;
189         }
190 }
191
192 sub initfeed {
193         my $self=shift;         # this is not a method but we cheat
194         my $fullpath=$self->{-fullpath};
195         my $toppath=$self->{-toppath};
196         my $inc=$self->{-inc}.$incdir.'/';
197         my $conffile=$toppath.'/'.$incdir.'/feed.conf';
198         my $CONF;
199
200         if (! $incpath) {
201                 if ($feed) {
202                         if (open($CONF,">".$conffile)) {
203                                 print $CONF $feed,"\n";
204                                 close($CONF);
205                         } else {
206                                 print STDERR "could not open $conffile: $!\n";
207                         }
208                 } else {
209                         if (open($CONF,$conffile)) {
210                                 $feed=<$CONF>;
211                                 close($CONF);
212                                 chop $feed;
213                         }
214                 }
215         }
216
217         return unless ($feed);
218
219         my ($feedfile, $feedbase, $feedhub) = split(',', $feed);
220         $feedbase .= '/' unless ($feedbase =~ /\/$/);
221         print "($feedfile, $feedbase, $feedhub)\n";
222
223         $feedobj->{-savepath} = $self->{-toppath}.'/'.$feedfile;
224         $feedobj->{-file} = $feedfile;
225         $feedobj->{-base} = $feedbase;
226         $feedobj->{-hub} = $feedhub;
227         if ( -f $feedobj->{-file} ) {
228                 $feedobj->{-feed} = XML::FeedPP::Atom::Atom10->new(
229                                                         $feedobj->{-file});
230                 $feedobj->{-feed}->limit_item(15);
231         } else {
232                 $feedobj->{-feed} = XML::FeedPP::Atom::Atom10->new;
233                 $feedobj->{-feed}->title("Gallery");
234                 $feedobj->{-feed}->description("generated by ".
235                         "<a href=\"http://www.average.org/mkgallery/\">".
236                         "mkgallery</a>");
237                 $feedobj->{-feed}->link($feedbase, 'type' => 'text/html', 'rel' => 'alternate');
238                 $feedobj->{-feed}->xmlns("xmlns:ostatus" =>
239                         "http://ostatus.org/schema/1.0");
240                 #$feedobj->{-feed}->copyright("");
241                 #$feedobj->{-feed}->language("en");
242                 #$feedobj->{-feed}->image($url, $tit, $link, $desc, $w, $h);
243         }
244         $self->{-feed} = $feedobj->{-feed};
245 }
246
247 sub iterate {
248         my $self = shift;
249         my $fullpath .= $self->{-fullpath};
250         print "iterate in dir $fullpath\n" if ($debug);
251
252         my $youngest=0;
253         my @rdirlist;
254         my @rimglist;
255         my $D;
256         unless (opendir($D,$fullpath)) {
257                 warn "cannot opendir $fullpath: $!";
258                 return;
259         }
260         while (my $de = readdir($D)) {
261                 next if ($de =~ /^\./);
262                 my $child = $self->new($de);
263                 my @stat = stat($child->{-fullpath});
264                 $youngest = $stat[9] if ($youngest < $stat[9]);
265                 if ($child->isdir) {
266                         push(@rdirlist,$child);
267                 } elsif ($child->isimg) {
268                         push(@rimglist,$child);
269                 }
270         }
271         closedir($D);
272         my @dirlist = sort {$a->{-base} cmp $b->{-base}} @rdirlist;
273         undef @rdirlist; # inplace sorting would be handy here
274         my @imglist = sort {$a->{-base} cmp $b->{-base}} @rimglist;
275         undef @rimglist; # optimize away unsorted versions
276         $self->{-firstimg} = $imglist[0];
277
278         print "Dir: $self->{-fullpath}\n" if ($debug);
279
280 # 1. first of all, fill title for this directory and create hidden subdirs
281
282         $self->initdir;
283
284 # 2. recurse into subdirectories to get their titles filled
285 #    before we start writing out subalbum list
286
287         foreach my $dir(@dirlist) {
288                 $dir->iterate;
289         }
290
291 # 3. iterate through images to build cross-links,
292
293         my $previmg = undef;
294         foreach my $img(@imglist) {
295                 # list-linking must be done before generating
296                 # aux html because aux pages rely on prev/next refs
297                 if ($previmg) {
298                         $previmg->{-nextimg} = $img;
299                         $img->{-previmg} = $previmg;
300                 }
301                 $previmg=$img;
302         }
303
304 # 4. create scaled versions and aux html pages
305
306         foreach my $img(@imglist) {
307                 # scaled versions must be generated before aux html
308                 # and main image index because they both rely on
309                 # refs to scaled images and they may be just original
310                 # images, this is not known before we try scaling.
311                 $img->makescaled;
312                 # finally, make aux html pages
313                 $img->makeaux;
314         }
315
316 # no need to go beyond this point if the directory timestamp did not
317 # change since we built index.html file last time.
318
319         my @istat = stat($self->{-fullpath}.'/index.html');
320         return unless ($youngest > $istat[9]);
321
322 # 5. start building index.html for the directory
323
324         $self->startindex;
325
326 # 6. iterate through subdirectories to build subalbums list
327
328         if (@dirlist) {
329                 $self->startsublist;
330                 foreach my $dir(@dirlist) {
331                         $dir->sub_entry;
332                 }
333                 $self->endsublist;
334         }
335
336 # 7. iterate through images to build thumb list
337
338         if (@imglist) {
339                 $self->startimglist;
340                 foreach my $img(@imglist) {
341                         print "Img: $img->{-fullpath}\n" if ($debug);
342                         $img->img_entry;
343                 }
344                 $self->endimglist;
345         }
346
347 # 8. comlplete building index.html for the directory
348
349         $self->endindex;
350 }
351
352 sub isdir {
353         my $self = shift;
354         return ( -d $self->{-fullpath} );
355 }
356
357 sub isimg {
358         my $self = shift;
359         my $fullpath = $self->{-fullpath};
360         return 0 unless ( -f $fullpath );
361
362         if ($havegeoloc) {
363                 my $exif = new Image::ExifTool;
364                 $exif->ExtractInfo($fullpath);
365                 my ($la,$lo) = $exif->GetLocation();
366                 if ($la && $lo) {
367                         $self->{-geoloc} = [$la,$lo];
368                 }
369         }
370
371         my $info = image_info($fullpath);
372         if (my $error = $info->{error}) {
373                 if (($error !~ "Unrecognized file format") &&
374                     ($error !~ "Can't read head")) {
375                         warn "File \"$fullpath\": $error\n";
376                 }
377                 return 0;
378         }
379
380         tryapp12($info) unless ($info->{'ExifVersion'});
381
382         $self->{-isimg} = 1;
383         $self->{-info} = $info;
384         return 1;
385 }
386
387 sub tryapp12 {
388         my $info = shift;       # this is not a method
389         my $app12;
390         # dirty hack to take care of Image::Info parser strangeness
391         foreach my $k(keys %$info) {
392                 $app12=substr($k,6).$info->{$k} if ($k =~ /^App12-/);
393         }
394         return unless ($app12); # bad luck
395         my $seenfirstline=0;
396         foreach my $ln(split /[\r\n]+/,$app12) {
397                 $ln =~ s/[[:^print:]\000]/ /g;
398                 unless ($seenfirstline) {
399                         $seenfirstline=1;
400                         $info->{'Make'}=$ln;
401                         next;
402                 }
403                 my ($k,$v)=split /=/,$ln,2;
404                 if ($k eq 'TimeDate') {
405                         $info->{'DateTime'} =
406                                 strftime("%Y:%m:%d %H:%M:%S", localtime($v))
407                                                         unless ($v < 0);
408                 } elsif ($k eq 'Shutter') {
409                         $info->{'ExposureTime'} = '1/'.int(1000000/$v+.5);
410                 } elsif ($k eq 'Flash') {
411                         $info->{'Flash'} = $v?'Flash fired':'Flash did not fire';
412                 } elsif ($k eq 'Type') {
413                         $info->{'Model'} = $v;
414                 } elsif ($k eq 'Version') {
415                         $info->{'Software'} = $v;
416                 } elsif ($k eq 'Fnumber') {
417                         $info->{'FNumber'} = $v;
418                 }
419         }
420 }
421
422 sub initdir {
423         my $self = shift;
424         my $fullpath = $self->{-fullpath};
425         for my $subdir(@sizes, 'html') {
426                 my $tdir=sprintf "%s/.%s",$self->{-fullpath},$subdir;
427                 mkdir($tdir,0755) unless ( -d $tdir );
428         }
429         $self->edittitle;
430 }
431
432 sub edittitle {
433         my $self = shift;
434         my $fullpath = $self->{-fullpath};
435         my $title;
436         my $T;
437         if (open($T,'<'.$fullpath.'/.title')) {
438                 binmode($T, ":utf8");
439                 $title = <$T>;
440                 $title =~ s/[\r\n]*$//;
441                 close($T);
442         }
443         if ($asktitle || (!$title && !$noasktitle)) {
444                 my $prompt = $self->{-relpath};
445                 $prompt = '/' unless ($prompt);
446                 my $OUT = $term->OUT || \*STDOUT;
447                 print $OUT "Enter title for $fullpath\n";
448                 $title = $term->readline($prompt.' >',$title);
449                 $term->addhistory($title) if ($title);
450                 if (open($T,'>'.$fullpath.'/.title')) {
451                         print $T $title,"\n";
452                         close($T);
453                 }
454         }
455         unless ($title) {
456                 $title=$self->{-relpath};
457         }
458         $self->{-title}=$title;
459         print "title in $fullpath is $title\n" if ($debug);
460 }
461
462 sub makescaled {
463         my $self = shift;
464         my $fn = $self->{-fullpath};
465         my $name = $self->{-base};
466         my $dn = $self->{-parent}->{-fullpath};
467         my ($w, $h) = dim($self->{-info});
468         my $max = ($w > $h)?$w:$h;
469
470         foreach my $size(@sizes) {
471                 my $nref = '.'.$size.'/'.$name;
472                 my $nfn = $dn.'/'.$nref;
473                 my $factor=$size/$max;
474                 if ($factor >= 1) {
475                         $self->{$size}->{'url'} = $name; # unscaled version
476                         $self->{$size}->{'dim'} = [$w, $h];
477                 } else {
478                         $self->{$size}->{'url'} = $nref;
479                         $self->{$size}->{'dim'} = [int($w*$factor+.5),
480                                                         int($h*$factor+.5)];
481                         if (isnewer($fn,$nfn)) {
482                                 doscaling($fn,$nfn,$factor,$w,$h);
483                         }
484                 }
485         }
486 }
487
488 sub isnewer {
489         my ($fn1,$fn2) = @_;                    # this is not a method
490         my @stat1=stat($fn1);
491         my @stat2=stat($fn2);
492         return (!@stat2 || ($stat1[9] > $stat2[9]));
493         # true if $fn2 is absent or is older than $fn1
494 }
495
496 sub doscaling {
497         my ($src,$dest,$factor,$w,$h) = @_;     # this is not a method
498
499         my $err=1;
500         if ($haveimagick) {
501                 my $im = new Image::Magick;
502                 print "doscaling $src -> $dest by $factor\n" if ($debug);
503                 if ($err = $im->Read($src)) {
504                         warn "ImageMagick: read \"$src\": $err";
505                 } else {
506                         $im->Scale(width=>$w*$factor,height=>$h*$factor);
507                         $err=$im->Write($dest);
508                         warn "ImageMagick: write \"$dest\": $err" if ($err);
509                 }
510                 undef $im;
511         }
512         if ($err) {     # fallback to command-line tools
513                 system("djpeg \"$src\" | pnmscale \"$factor\" | cjpeg >\"$dest\"");
514         }
515 }
516
517 sub makeaux {
518         my $self = shift;
519         my $name = $self->{-base};
520         my $dn = $self->{-parent}->{-fullpath};
521         my $pref = $self->{-previmg}->{-base};
522         my $nref = $self->{-nextimg}->{-base};
523         my $inc = $self->{-inc}.$incdir.'/';
524         my $title = $self->{-info}->{'Comment'};
525         $title = $name unless ($title);
526
527         print "slide: \"$title\": \"$pref\"->\"$name\"->\"$nref\"\n" if ($debug);
528
529         # slideshow
530         for my $refresh('static', 'slide') {
531                 my $fn = sprintf("%s/.html/%s-%s.html",$dn,$name,$refresh);
532                 if (isnewer($self->{-fullpath},$fn)) {
533                         my $imgsrc = '../'.$self->{$sizes[1]}->{'url'};
534                         my $fwdref;
535                         my $bakref;
536                         if ($nref) {
537                                 $fwdref = sprintf("%s-%s.html",$nref,$refresh);
538                         } else {
539                                 $fwdref = '../index.html';
540                         }
541                         if ($pref) {
542                                 $bakref = sprintf("%s-%s.html",$pref,$refresh);
543                         } else {
544                                 $bakref = '../index.html';
545                         }
546                         my $toggleref;
547                         my $toggletext;
548                         if ($refresh eq 'slide') {
549                                 $toggleref=sprintf("%s-static.html",$name);
550                                 $toggletext = 'Stop!';
551                         } else {
552                                 $toggleref=sprintf("%s-slide.html",$name);
553                                 $toggletext = 'Play-&gt;';
554                         }
555                         my $F;
556                         unless (open($F,'>'.$fn)) {
557                                 warn "cannot open \"$fn\": $!";
558                                 next;
559                         }
560                         binmode($F, ":utf8");
561                         if ($refresh eq 'slide') {
562                                 print $F start_html(
563                                         -encoding=>"utf-8",
564                                         -title=>$title,
565                                         -bgcolor=>"#808080",
566                                         -head=>meta({-http_equiv=>'Refresh',
567                                                 -content=>"3; url=$fwdref"}),
568                                         -style=>{-src=>$inc."gallery.css"},
569                                         ),"\n",
570                                         comment("Created by ".$version),"\n";
571                                                 
572                         } else {
573                                 print $F start_html(-title=>$title,
574                                         -encoding=>"utf-8",
575                                         -bgcolor=>"#808080",
576                                         -style=>{-src=>$inc."gallery.css"},
577                                         ),"\n",
578                                         comment("Created by ".$version),"\n";
579                         }
580                         print $F start_table({-class=>'navi'}),start_Tr,"\n",
581                                 td(a({-href=>"../index.html"},"Index")),"\n",
582                                 td(a({-href=>$bakref},"&lt;&lt;Prev")),"\n",
583                                 td(a({-href=>$toggleref},$toggletext)),"\n",
584                                 td(a({-href=>$fwdref},"Next&gt;&gt;")),"\n",
585                                 td({-class=>'title'},$title),"\n",
586                                 end_Tr,
587                                 end_table,"\n",
588                                 center(table({-class=>'picframe'},
589                                         Tr(td(img({-src=>$imgsrc,
590                                                    -class=>'standalone',
591                                                    -alt=>$title}))))),"\n",
592                                 end_html,"\n";
593                         close($F);
594                 }
595         }
596
597         # info html
598         my $fn = sprintf("%s/.html/%s-info.html",$dn,$name);
599         if (isnewer($self->{-fullpath},$fn)) {
600                 my $F;
601                 unless (open($F,'>'.$fn)) {
602                         warn "cannot open \"$fn\": $!";
603                         return;
604                 }
605                 binmode($F, ":utf8");
606                 my $imgsrc = sprintf("../.%s/%s",$sizes[0],$name);
607                 print $F start_html(-title=>$title,
608                                 -encoding=>"utf-8",
609                                 -style=>{-src=>$inc."gallery.css"},
610                                 -script=>[
611                                         {-src=>$inc."mootools.js"},
612                                         {-src=>$inc."urlparser.js"},
613                                         {-src=>$inc."infopage.js"},
614                                 ]),"\n",
615                         comment("Created by ".$version),"\n",
616                         start_center,"\n",
617                         h1($title),"\n",
618                         table({-class=>'ipage'},
619                                 Tr(td(img({-src=>$imgsrc,
620                                            -class=>'thumbnail',
621                                            -alt=>$title})),
622                                         td($self->infotable))),
623                         a({-href=>'../index.html',-class=>'conceal'},
624                                 'Index'),"\n",
625                         end_center,"\n",
626                         end_html,"\n";
627                 close($F);
628         }
629 }
630
631 sub startindex {
632         my $self = shift;
633         my $fn = $self->{-fullpath}.'/index.html';
634         my $block = $self->{-fullpath}.'/.noindex';
635         $fn = '/dev/null' if ( -f $block );
636         my $IND;
637         unless (open($IND,'>'.$fn)) {
638                 warn "cannot open $fn: $!";
639                 return;
640         }
641         binmode($IND, ":utf8");
642         $self->{-IND} = $IND;
643
644         my $inc = $self->{-inc}.$incdir.'/';
645         my $title = $self->{-title};
646         my $feedlink="";
647         if ($feedobj) {
648                 $feedlink=Link({-rel=>'alternate',
649                                 -type=>'application/atom+xml',
650                                 -title=>'Gallery Feed',
651                                 -href=>$feedobj->{-base}.$feedobj->{-file}});
652         }
653         print $IND start_html(-title => $title,
654                         -encoding=>"utf-8",
655                         -head=>$feedlink,
656                         -style=>[
657                                 {-src=>$inc."gallery.css"},
658                                 {-src=>$inc."custom.css"},
659                         ],
660                         -script=>[
661                                 {-src=>$inc."mootools.js"},
662                                 {-src=>$inc."overlay.js"},
663                                 {-src=>$inc."urlparser.js"},
664                                 {-src=>$inc."multibox.js"},
665                                 {-src=>$inc."showwin.js"},
666                                 {-src=>$inc."controls.js"},
667                                 {-src=>$inc."show.js"},
668                                 {-src=>$inc."gallery.js"},
669                         ]),"\n",
670                 comment("Created by ".$version),"\n",
671                 start_div({-class => 'indexContainer',
672                                 -id => 'indexContainer'}),
673                 "\n";
674         my $EVL;
675         if (open($EVL,$self->{-toppath}.'/'.$incdir.'/header.pl')) {
676                 my $prm;
677                 while (<$EVL>) {
678                         $prm .= $_;
679                 }
680                 close($EVL);
681                 %_ = (
682                         -version        => $version,
683                         -depth          => $self->{-depth},
684                         -title          => $title,
685                         -path           => $self->{-fullpath},
686                         -breadcrumbs    => "breadcrumbs unimplemented",
687                 );
688                 print $IND eval $prm,"\n";
689         } else {
690                 print STDERR "could not open ",
691                         $self->{-toppath}.'/'.$incdir.'/header.pl',
692                         " ($!), reverting to default header";
693                 print $IND a({-href=>"../index.html"},"UP"),"\n",
694                         h1({-class=>'title'},$title),"\n",
695         }
696 }
697
698 sub endindex {
699         my $self = shift;
700         my $IND = $self->{-IND};
701
702         print $IND end_div;
703         my $EVL;
704         if (open($EVL,$self->{-toppath}.'/'.$incdir.'/footer.pl')) {
705                 my $prm;
706                 while (<$EVL>) {
707                         $prm .= $_;
708                 }
709                 close($EVL);
710                 %_ = (
711                         -version        => $version,
712                         -depth          => $self->{-depth},
713                         -title          => $self->{-title},
714                         -breadcrumbs    => "breadcrumbs unimplemented",
715                 );
716                 print $IND eval $prm,"\n";
717         } else {
718                 print STDERR "could not open ",
719                         $self->{-toppath}.'/'.$incdir.'/footer.pl',
720                         " ($!), reverting to default empty footer";
721         }
722         print $IND end_html,"\n";
723
724         close($IND) if ($IND);
725         undef $self->{-IND};
726         if ($feedobj) {
727                 my $feedtitle=sprintf "%s [%d images, %d subalbums]",
728                                 $self->{-title},
729                                 $self->{-numofimgs},
730                                 $self->{-numofsubs};
731                 my $feedlink=$feedobj->{-feed}->link.
732                         $self->{-relpath}."index.html";
733                 my $uu;
734                 my $us;
735                 UUID::generate($uu);
736                 UUID::unparse($uu, $us);
737                 $feedobj->{-feed}->add_item(
738                         title           => $self->{-title},
739                         link            => $feedlink,
740                         description     => $feedtitle,
741                         pubDate         => time,
742                         guid            => $us,
743                 );
744         }
745 }
746
747 sub startsublist {
748         my $self = shift;
749         my $IND = $self->{-IND};
750
751         print $IND h2({-class=>"atitle"},"Albums"),"\n",start_table,"\n";
752 }
753
754 sub sub_entry {
755         my $self = shift;
756         my $IND = $self->{-parent}->{-IND};
757         my $name = $self->{-base};
758         my $title = $self->{-title};
759
760         $self->{-parent}->{-numofsubs}++;
761         print $IND Tr(td(a({-href=>$name.'/index.html'},$name)),
762                         td(a({-href=>$name.'/index.html'},$title))),"\n";
763 }
764
765 sub endsublist {
766         my $self = shift;
767         my $IND = $self->{-IND};
768
769         print $IND end_table,"\n",br({-clear=>'all'}),hr,"\n\n";
770 }
771
772 sub startimglist {
773         my $self = shift;
774         my $IND = $self->{-IND};
775         my $first = $self->{-firstimg}->{-base};
776         my $slideref = sprintf(".html/%s-slide.html",$first);
777
778         print $IND h2({-class=>"ititle"},"Images ",
779                 a({-href=>$slideref,-class=>'showStart',-rel=>'i'.$first},
780                         '&gt; slideshow')),"\n";
781 }
782
783 sub img_entry {
784         my $self = shift;
785         my $IND = $self->{-parent}->{-IND};
786         my $name = $self->{-base};
787         my $title = $self->{-info}->{'Comment'};
788         $title = $name unless ($title);
789         my $thumb = $self->{$sizes[0]}->{'url'};
790         my $info = $self->{-info};
791         my ($w, $h) = dim($info);
792
793         my $i=0+$self->{-parent}->{-numofimgs};
794         $self->{-parent}->{-numofimgs}++;
795
796         print $IND a({-name=>$name}),"\n",
797                 start_table({-class=>'slide'}),start_Tr,start_td,"\n";
798         print $IND div({-class=>'slidetitle'},
799                         "\n ",a({-href=>".html/$name-info.html",
800                                 -title=>'Image Info: '.$name,
801                                 -class=>'infoBox'},
802                                 $title),"\n"),"\n",
803                 start_div({-class=>'slideimage'});
804         if ($self->{-geoloc}) {
805                 my ($la,$lo) = @{$self->{-geoloc}};
806                 print $IND a({-href=>"http://maps.google.com/".
807                                                 "?q=$la,$lo&ll=$la,$lo",
808                                 -title=>"$la,$lo",
809                                 -class=>'geoloc'},
810                                 div({-class=>'geoloc'},"")),"\n";
811         }
812         print $IND a({-href=>".html/$name-static.html",
813                                 -title=>$title,
814                                 -class=>'showImage',
815                                 -rel=>'i'.$name},
816                                 img({-src=>$thumb,
817                                      -class=>'thumbnail',
818                                      -alt=>$title})),"\n",end_div,
819                 start_div({-class=>'varimages',-id=>'i'.$name,-title=>$title}),"\n";
820         foreach my $sz(@sizes) {
821                 my $src=$self->{$sz}->{'url'};
822                 my $w=$self->{$sz}->{'dim'}->[0];
823                 my $h=$self->{$sz}->{'dim'}->[1];
824                 print $IND "  ",a({-href=>$src,
825                         -class=>"conceal",
826                         -rel=>$w."x".$h,
827                         -title=>"Reduced to ".$w."x".$h},
828                         $w."x".$h)," \n";
829         }
830         print $IND "  ",a({-href=>$name,
831                                 -rel=>$w."x".$h,
832                                 -title=>'Original'},$w."x".$h),
833                 "\n",end_div,"\n",
834                 end_td,end_Tr,end_table,"\n";
835 }
836
837 sub endimglist {
838         my $self = shift;
839         my $IND = $self->{-IND};
840
841         print $IND br({-clear=>'all'}),hr,"\n\n";
842 }
843
844 sub infotable {
845         my $self = shift;
846         my $info = $self->{-info};
847         my $msg='';
848
849         my @infokeys=(
850                 'DateTime',
851                 'ExposureTime',
852                 'FNumber',
853                 'Flash',
854                 'ISOSpeedRatings',
855                 'MeteringMode',
856                 'ExposureProgram',
857                 'FocalLength',
858                 'FileSource',
859                 'Make',
860                 'Model',
861                 'Software',
862         );
863         $msg.=start_table({-class=>'infotable'})."\n";
864         foreach my $k(@infokeys) {
865                 $msg.=Tr(td($k.":"),td($info->{$k}))."\n" if ($info->{$k});
866         }
867         $msg.=end_table."\n";
868 }
869