use XML::FeedPP. WIP, produces error
[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         $feedobj->{-feed} = XML::FeedPP::Atom::Atom10->new;
228         if ( -f $feedobj->{-file} ) {
229                 $feedobj->{-feed}->load($feedobj->{-file});
230                 $feedobj->{-feed}->limit_item(15);
231         } else {
232                 $feedobj->{-feed}->title("Gallery");
233                 $feedobj->{-feed}->description("generated by ".
234                         "<a href=\"http://www.average.org/mkgallery/\">".
235                         "mkgallery</a>");
236                 $feedobj->{-feed}->link($feedbase);
237                 #$feedobj->{-feed}->copyright("");
238                 #$feedobj->{-feed}->language("en");
239                 #$feedobj->{-feed}->image($url, $tit, $link, $desc, $w, $h);
240         }
241         $feedobj->{-feed}->pubDate(time);
242         $feedobj->{-feed}->to_file($feedobj->{-savepath});
243         $self->{-feed} = $feedobj->{-feed};
244 }
245
246 sub iterate {
247         my $self = shift;
248         my $fullpath .= $self->{-fullpath};
249         print "iterate in dir $fullpath\n" if ($debug);
250
251         my $youngest=0;
252         my @rdirlist;
253         my @rimglist;
254         my $D;
255         unless (opendir($D,$fullpath)) {
256                 warn "cannot opendir $fullpath: $!";
257                 return;
258         }
259         while (my $de = readdir($D)) {
260                 next if ($de =~ /^\./);
261                 my $child = $self->new($de);
262                 my @stat = stat($child->{-fullpath});
263                 $youngest = $stat[9] if ($youngest < $stat[9]);
264                 if ($child->isdir) {
265                         push(@rdirlist,$child);
266                 } elsif ($child->isimg) {
267                         push(@rimglist,$child);
268                 }
269         }
270         closedir($D);
271         my @dirlist = sort {$a->{-base} cmp $b->{-base}} @rdirlist;
272         undef @rdirlist; # inplace sorting would be handy here
273         my @imglist = sort {$a->{-base} cmp $b->{-base}} @rimglist;
274         undef @rimglist; # optimize away unsorted versions
275         $self->{-firstimg} = $imglist[0];
276
277         print "Dir: $self->{-fullpath}\n" if ($debug);
278
279 # 1. first of all, fill title for this directory and create hidden subdirs
280
281         $self->initdir;
282
283 # 2. recurse into subdirectories to get their titles filled
284 #    before we start writing out subalbum list
285
286         foreach my $dir(@dirlist) {
287                 $dir->iterate;
288         }
289
290 # 3. iterate through images to build cross-links,
291
292         my $previmg = undef;
293         foreach my $img(@imglist) {
294                 # list-linking must be done before generating
295                 # aux html because aux pages rely on prev/next refs
296                 if ($previmg) {
297                         $previmg->{-nextimg} = $img;
298                         $img->{-previmg} = $previmg;
299                 }
300                 $previmg=$img;
301         }
302
303 # 4. create scaled versions and aux html pages
304
305         foreach my $img(@imglist) {
306                 # scaled versions must be generated before aux html
307                 # and main image index because they both rely on
308                 # refs to scaled images and they may be just original
309                 # images, this is not known before we try scaling.
310                 $img->makescaled;
311                 # finally, make aux html pages
312                 $img->makeaux;
313         }
314
315 # no need to go beyond this point if the directory timestamp did not
316 # change since we built index.html file last time.
317
318         my @istat = stat($self->{-fullpath}.'/index.html');
319         return unless ($youngest > $istat[9]);
320
321 # 5. start building index.html for the directory
322
323         $self->startindex;
324
325 # 6. iterate through subdirectories to build subalbums list
326
327         if (@dirlist) {
328                 $self->startsublist;
329                 foreach my $dir(@dirlist) {
330                         $dir->sub_entry;
331                 }
332                 $self->endsublist;
333         }
334
335 # 7. iterate through images to build thumb list
336
337         if (@imglist) {
338                 $self->startimglist;
339                 foreach my $img(@imglist) {
340                         print "Img: $img->{-fullpath}\n" if ($debug);
341                         $img->img_entry;
342                 }
343                 $self->endimglist;
344         }
345
346 # 8. comlplete building index.html for the directory
347
348         $self->endindex;
349 }
350
351 sub isdir {
352         my $self = shift;
353         return ( -d $self->{-fullpath} );
354 }
355
356 sub isimg {
357         my $self = shift;
358         my $fullpath = $self->{-fullpath};
359         return 0 unless ( -f $fullpath );
360
361         if ($havegeoloc) {
362                 my $exif = new Image::ExifTool;
363                 $exif->ExtractInfo($fullpath);
364                 my ($la,$lo) = $exif->GetLocation();
365                 if ($la && $lo) {
366                         $self->{-geoloc} = [$la,$lo];
367                 }
368         }
369
370         my $info = image_info($fullpath);
371         if (my $error = $info->{error}) {
372                 if (($error !~ "Unrecognized file format") &&
373                     ($error !~ "Can't read head")) {
374                         warn "File \"$fullpath\": $error\n";
375                 }
376                 return 0;
377         }
378
379         tryapp12($info) unless ($info->{'ExifVersion'});
380
381         $self->{-isimg} = 1;
382         $self->{-info} = $info;
383         return 1;
384 }
385
386 sub tryapp12 {
387         my $info = shift;       # this is not a method
388         my $app12;
389         # dirty hack to take care of Image::Info parser strangeness
390         foreach my $k(keys %$info) {
391                 $app12=substr($k,6).$info->{$k} if ($k =~ /^App12-/);
392         }
393         return unless ($app12); # bad luck
394         my $seenfirstline=0;
395         foreach my $ln(split /[\r\n]+/,$app12) {
396                 $ln =~ s/[[:^print:]\000]/ /g;
397                 unless ($seenfirstline) {
398                         $seenfirstline=1;
399                         $info->{'Make'}=$ln;
400                         next;
401                 }
402                 my ($k,$v)=split /=/,$ln,2;
403                 if ($k eq 'TimeDate') {
404                         $info->{'DateTime'} =
405                                 strftime("%Y:%m:%d %H:%M:%S", localtime($v))
406                                                         unless ($v < 0);
407                 } elsif ($k eq 'Shutter') {
408                         $info->{'ExposureTime'} = '1/'.int(1000000/$v+.5);
409                 } elsif ($k eq 'Flash') {
410                         $info->{'Flash'} = $v?'Flash fired':'Flash did not fire';
411                 } elsif ($k eq 'Type') {
412                         $info->{'Model'} = $v;
413                 } elsif ($k eq 'Version') {
414                         $info->{'Software'} = $v;
415                 } elsif ($k eq 'Fnumber') {
416                         $info->{'FNumber'} = $v;
417                 }
418         }
419 }
420
421 sub initdir {
422         my $self = shift;
423         my $fullpath = $self->{-fullpath};
424         for my $subdir(@sizes, 'html') {
425                 my $tdir=sprintf "%s/.%s",$self->{-fullpath},$subdir;
426                 mkdir($tdir,0755) unless ( -d $tdir );
427         }
428         $self->edittitle;
429 }
430
431 sub edittitle {
432         my $self = shift;
433         my $fullpath = $self->{-fullpath};
434         my $title;
435         my $T;
436         if (open($T,'<'.$fullpath.'/.title')) {
437                 binmode($T, ":utf8");
438                 $title = <$T>;
439                 $title =~ s/[\r\n]*$//;
440                 close($T);
441         }
442         if ($asktitle || (!$title && !$noasktitle)) {
443                 my $prompt = $self->{-relpath};
444                 $prompt = '/' unless ($prompt);
445                 my $OUT = $term->OUT || \*STDOUT;
446                 print $OUT "Enter title for $fullpath\n";
447                 $title = $term->readline($prompt.' >',$title);
448                 $term->addhistory($title) if ($title);
449                 if (open($T,'>'.$fullpath.'/.title')) {
450                         print $T $title,"\n";
451                         close($T);
452                 }
453         }
454         unless ($title) {
455                 $title=$self->{-relpath};
456         }
457         $self->{-title}=$title;
458         print "title in $fullpath is $title\n" if ($debug);
459 }
460
461 sub makescaled {
462         my $self = shift;
463         my $fn = $self->{-fullpath};
464         my $name = $self->{-base};
465         my $dn = $self->{-parent}->{-fullpath};
466         my ($w, $h) = dim($self->{-info});
467         my $max = ($w > $h)?$w:$h;
468
469         foreach my $size(@sizes) {
470                 my $nref = '.'.$size.'/'.$name;
471                 my $nfn = $dn.'/'.$nref;
472                 my $factor=$size/$max;
473                 if ($factor >= 1) {
474                         $self->{$size}->{'url'} = $name; # unscaled version
475                         $self->{$size}->{'dim'} = [$w, $h];
476                 } else {
477                         $self->{$size}->{'url'} = $nref;
478                         $self->{$size}->{'dim'} = [int($w*$factor+.5),
479                                                         int($h*$factor+.5)];
480                         if (isnewer($fn,$nfn)) {
481                                 doscaling($fn,$nfn,$factor,$w,$h);
482                         }
483                 }
484         }
485 }
486
487 sub isnewer {
488         my ($fn1,$fn2) = @_;                    # this is not a method
489         my @stat1=stat($fn1);
490         my @stat2=stat($fn2);
491         return (!@stat2 || ($stat1[9] > $stat2[9]));
492         # true if $fn2 is absent or is older than $fn1
493 }
494
495 sub doscaling {
496         my ($src,$dest,$factor,$w,$h) = @_;     # this is not a method
497
498         my $err=1;
499         if ($haveimagick) {
500                 my $im = new Image::Magick;
501                 print "doscaling $src -> $dest by $factor\n" if ($debug);
502                 if ($err = $im->Read($src)) {
503                         warn "ImageMagick: read \"$src\": $err";
504                 } else {
505                         $im->Scale(width=>$w*$factor,height=>$h*$factor);
506                         $err=$im->Write($dest);
507                         warn "ImageMagick: write \"$dest\": $err" if ($err);
508                 }
509                 undef $im;
510         }
511         if ($err) {     # fallback to command-line tools
512                 system("djpeg \"$src\" | pnmscale \"$factor\" | cjpeg >\"$dest\"");
513         }
514 }
515
516 sub makeaux {
517         my $self = shift;
518         my $name = $self->{-base};
519         my $dn = $self->{-parent}->{-fullpath};
520         my $pref = $self->{-previmg}->{-base};
521         my $nref = $self->{-nextimg}->{-base};
522         my $inc = $self->{-inc}.$incdir.'/';
523         my $title = $self->{-info}->{'Comment'};
524         $title = $name unless ($title);
525
526         print "slide: \"$title\": \"$pref\"->\"$name\"->\"$nref\"\n" if ($debug);
527
528         # slideshow
529         for my $refresh('static', 'slide') {
530                 my $fn = sprintf("%s/.html/%s-%s.html",$dn,$name,$refresh);
531                 if (isnewer($self->{-fullpath},$fn)) {
532                         my $imgsrc = '../'.$self->{$sizes[1]}->{'url'};
533                         my $fwdref;
534                         my $bakref;
535                         if ($nref) {
536                                 $fwdref = sprintf("%s-%s.html",$nref,$refresh);
537                         } else {
538                                 $fwdref = '../index.html';
539                         }
540                         if ($pref) {
541                                 $bakref = sprintf("%s-%s.html",$pref,$refresh);
542                         } else {
543                                 $bakref = '../index.html';
544                         }
545                         my $toggleref;
546                         my $toggletext;
547                         if ($refresh eq 'slide') {
548                                 $toggleref=sprintf("%s-static.html",$name);
549                                 $toggletext = 'Stop!';
550                         } else {
551                                 $toggleref=sprintf("%s-slide.html",$name);
552                                 $toggletext = 'Play-&gt;';
553                         }
554                         my $F;
555                         unless (open($F,'>'.$fn)) {
556                                 warn "cannot open \"$fn\": $!";
557                                 next;
558                         }
559                         binmode($F, ":utf8");
560                         if ($refresh eq 'slide') {
561                                 print $F start_html(
562                                         -encoding=>"utf-8",
563                                         -title=>$title,
564                                         -bgcolor=>"#808080",
565                                         -head=>meta({-http_equiv=>'Refresh',
566                                                 -content=>"3; url=$fwdref"}),
567                                         -style=>{-src=>$inc."gallery.css"},
568                                         ),"\n",
569                                         comment("Created by ".$version),"\n";
570                                                 
571                         } else {
572                                 print $F start_html(-title=>$title,
573                                         -encoding=>"utf-8",
574                                         -bgcolor=>"#808080",
575                                         -style=>{-src=>$inc."gallery.css"},
576                                         ),"\n",
577                                         comment("Created by ".$version),"\n";
578                         }
579                         print $F start_table({-class=>'navi'}),start_Tr,"\n",
580                                 td(a({-href=>"../index.html"},"Index")),"\n",
581                                 td(a({-href=>$bakref},"&lt;&lt;Prev")),"\n",
582                                 td(a({-href=>$toggleref},$toggletext)),"\n",
583                                 td(a({-href=>$fwdref},"Next&gt;&gt;")),"\n",
584                                 td({-class=>'title'},$title),"\n",
585                                 end_Tr,
586                                 end_table,"\n",
587                                 center(table({-class=>'picframe'},
588                                         Tr(td(img({-src=>$imgsrc,
589                                                    -class=>'standalone',
590                                                    -alt=>$title}))))),"\n",
591                                 end_html,"\n";
592                         close($F);
593                 }
594         }
595
596         # info html
597         my $fn = sprintf("%s/.html/%s-info.html",$dn,$name);
598         if (isnewer($self->{-fullpath},$fn)) {
599                 my $F;
600                 unless (open($F,'>'.$fn)) {
601                         warn "cannot open \"$fn\": $!";
602                         return;
603                 }
604                 binmode($F, ":utf8");
605                 my $imgsrc = sprintf("../.%s/%s",$sizes[0],$name);
606                 print $F start_html(-title=>$title,
607                                 -encoding=>"utf-8",
608                                 -style=>{-src=>$inc."gallery.css"},
609                                 -script=>[
610                                         {-src=>$inc."mootools.js"},
611                                         {-src=>$inc."urlparser.js"},
612                                         {-src=>$inc."infopage.js"},
613                                 ]),"\n",
614                         comment("Created by ".$version),"\n",
615                         start_center,"\n",
616                         h1($title),"\n",
617                         table({-class=>'ipage'},
618                                 Tr(td(img({-src=>$imgsrc,
619                                            -class=>'thumbnail',
620                                            -alt=>$title})),
621                                         td($self->infotable))),
622                         a({-href=>'../index.html',-class=>'conceal'},
623                                 'Index'),"\n",
624                         end_center,"\n",
625                         end_html,"\n";
626                 close($F);
627         }
628 }
629
630 sub startindex {
631         my $self = shift;
632         my $fn = $self->{-fullpath}.'/index.html';
633         my $block = $self->{-fullpath}.'/.noindex';
634         $fn = '/dev/null' if ( -f $block );
635         my $IND;
636         unless (open($IND,'>'.$fn)) {
637                 warn "cannot open $fn: $!";
638                 return;
639         }
640         binmode($IND, ":utf8");
641         $self->{-IND} = $IND;
642
643         my $inc = $self->{-inc}.$incdir.'/';
644         my $title = $self->{-title};
645         my $feedlink="";
646         if ($feedobj) {
647                 $feedlink=Link({-rel=>'alternate',
648                                 -type=>'application/atom+xml',
649                                 -title=>'Gallery Feed',
650                                 -href=>$feedobj->{-base}.$feedobj->{-file}});
651         }
652         print $IND start_html(-title => $title,
653                         -encoding=>"utf-8",
654                         -head=>$feedlink,
655                         -style=>[
656                                 {-src=>$inc."gallery.css"},
657                                 {-src=>$inc."custom.css"},
658                         ],
659                         -script=>[
660                                 {-src=>$inc."mootools.js"},
661                                 {-src=>$inc."overlay.js"},
662                                 {-src=>$inc."urlparser.js"},
663                                 {-src=>$inc."multibox.js"},
664                                 {-src=>$inc."showwin.js"},
665                                 {-src=>$inc."controls.js"},
666                                 {-src=>$inc."show.js"},
667                                 {-src=>$inc."gallery.js"},
668                         ]),"\n",
669                 comment("Created by ".$version),"\n",
670                 start_div({-class => 'indexContainer',
671                                 -id => 'indexContainer'}),
672                 "\n";
673         my $EVL;
674         if (open($EVL,$self->{-toppath}.'/'.$incdir.'/header.pl')) {
675                 my $prm;
676                 while (<$EVL>) {
677                         $prm .= $_;
678                 }
679                 close($EVL);
680                 %_ = (
681                         -version        => $version,
682                         -depth          => $self->{-depth},
683                         -title          => $title,
684                         -path           => $self->{-fullpath},
685                         -breadcrumbs    => "breadcrumbs unimplemented",
686                 );
687                 print $IND eval $prm,"\n";
688         } else {
689                 print STDERR "could not open ",
690                         $self->{-toppath}.'/'.$incdir.'/header.pl',
691                         " ($!), reverting to default header";
692                 print $IND a({-href=>"../index.html"},"UP"),"\n",
693                         h1({-class=>'title'},$title),"\n",
694         }
695 }
696
697 sub endindex {
698         my $self = shift;
699         my $IND = $self->{-IND};
700
701         print $IND end_div;
702         my $EVL;
703         if (open($EVL,$self->{-toppath}.'/'.$incdir.'/footer.pl')) {
704                 my $prm;
705                 while (<$EVL>) {
706                         $prm .= $_;
707                 }
708                 close($EVL);
709                 %_ = (
710                         -version        => $version,
711                         -depth          => $self->{-depth},
712                         -title          => $self->{-title},
713                         -breadcrumbs    => "breadcrumbs unimplemented",
714                 );
715                 print $IND eval $prm,"\n";
716         } else {
717                 print STDERR "could not open ",
718                         $self->{-toppath}.'/'.$incdir.'/footer.pl',
719                         " ($!), reverting to default empty footer";
720         }
721         print $IND end_html,"\n";
722
723         close($IND) if ($IND);
724         undef $self->{-IND};
725         if ($feedobj) {
726                 my $feedtitle=sprintf "%s [%d images, %d subalbums]",
727                                 $self->{-title},
728                                 $self->{-numofimgs},
729                                 $self->{-numofsubs};
730                 my $feedlink=$feedobj->{-feed}->link.
731                         $self->{-relpath}."index.html";
732                 my $uu;
733                 my $us;
734                 UUID::generate($uu);
735                 UUID::unparse($uu, $us);
736                 $feedobj->{-feed}->add_item(
737                         title           => $self->{-title},
738                         link            => $feedlink,
739                         description     => $feedtitle,
740                         pubDate         => time,
741                         guid            => $us,
742                 );
743         }
744 }
745
746 sub startsublist {
747         my $self = shift;
748         my $IND = $self->{-IND};
749
750         print $IND h2({-class=>"atitle"},"Albums"),"\n",start_table,"\n";
751 }
752
753 sub sub_entry {
754         my $self = shift;
755         my $IND = $self->{-parent}->{-IND};
756         my $name = $self->{-base};
757         my $title = $self->{-title};
758
759         $self->{-parent}->{-numofsubs}++;
760         print $IND Tr(td(a({-href=>$name.'/index.html'},$name)),
761                         td(a({-href=>$name.'/index.html'},$title))),"\n";
762 }
763
764 sub endsublist {
765         my $self = shift;
766         my $IND = $self->{-IND};
767
768         print $IND end_table,"\n",br({-clear=>'all'}),hr,"\n\n";
769 }
770
771 sub startimglist {
772         my $self = shift;
773         my $IND = $self->{-IND};
774         my $first = $self->{-firstimg}->{-base};
775         my $slideref = sprintf(".html/%s-slide.html",$first);
776
777         print $IND h2({-class=>"ititle"},"Images ",
778                 a({-href=>$slideref,-class=>'showStart',-rel=>'i'.$first},
779                         '&gt; slideshow')),"\n";
780 }
781
782 sub img_entry {
783         my $self = shift;
784         my $IND = $self->{-parent}->{-IND};
785         my $name = $self->{-base};
786         my $title = $self->{-info}->{'Comment'};
787         $title = $name unless ($title);
788         my $thumb = $self->{$sizes[0]}->{'url'};
789         my $info = $self->{-info};
790         my ($w, $h) = dim($info);
791
792         my $i=0+$self->{-parent}->{-numofimgs};
793         $self->{-parent}->{-numofimgs}++;
794
795         print $IND a({-name=>$name}),"\n",
796                 start_table({-class=>'slide'}),start_Tr,start_td,"\n";
797         print $IND div({-class=>'slidetitle'},
798                         "\n ",a({-href=>".html/$name-info.html",
799                                 -title=>'Image Info: '.$name,
800                                 -class=>'infoBox'},
801                                 $title),"\n"),"\n",
802                 start_div({-class=>'slideimage'});
803         if ($self->{-geoloc}) {
804                 my ($la,$lo) = @{$self->{-geoloc}};
805                 print $IND a({-href=>"http://maps.google.com/".
806                                                 "?q=$la,$lo&ll=$la,$lo",
807                                 -title=>"$la,$lo",
808                                 -class=>'geoloc'},
809                                 div({-class=>'geoloc'},"")),"\n";
810         }
811         print $IND a({-href=>".html/$name-static.html",
812                                 -title=>$title,
813                                 -class=>'showImage',
814                                 -rel=>'i'.$name},
815                                 img({-src=>$thumb,
816                                      -class=>'thumbnail',
817                                      -alt=>$title})),"\n",end_div,
818                 start_div({-class=>'varimages',-id=>'i'.$name,-title=>$title}),"\n";
819         foreach my $sz(@sizes) {
820                 my $src=$self->{$sz}->{'url'};
821                 my $w=$self->{$sz}->{'dim'}->[0];
822                 my $h=$self->{$sz}->{'dim'}->[1];
823                 print $IND "  ",a({-href=>$src,
824                         -class=>"conceal",
825                         -rel=>$w."x".$h,
826                         -title=>"Reduced to ".$w."x".$h},
827                         $w."x".$h)," \n";
828         }
829         print $IND "  ",a({-href=>$name,
830                                 -rel=>$w."x".$h,
831                                 -title=>'Original'},$w."x".$h),
832                 "\n",end_div,"\n",
833                 end_td,end_Tr,end_table,"\n";
834 }
835
836 sub endimglist {
837         my $self = shift;
838         my $IND = $self->{-IND};
839
840         print $IND br({-clear=>'all'}),hr,"\n\n";
841 }
842
843 sub infotable {
844         my $self = shift;
845         my $info = $self->{-info};
846         my $msg='';
847
848         my @infokeys=(
849                 'DateTime',
850                 'ExposureTime',
851                 'FNumber',
852                 'Flash',
853                 'ISOSpeedRatings',
854                 'MeteringMode',
855                 'ExposureProgram',
856                 'FocalLength',
857                 'FileSource',
858                 'Make',
859                 'Model',
860                 'Software',
861         );
862         $msg.=start_table({-class=>'infotable'})."\n";
863         foreach my $k(@infokeys) {
864                 $msg.=Tr(td($k.":"),td($info->{$k}))."\n" if ($info->{$k});
865         }
866         $msg.=end_table."\n";
867 }
868