diff options
Diffstat (limited to 'IkiWiki/Plugin/album.pm')
-rw-r--r-- | IkiWiki/Plugin/album.pm | 559 |
1 files changed, 559 insertions, 0 deletions
diff --git a/IkiWiki/Plugin/album.pm b/IkiWiki/Plugin/album.pm new file mode 100644 index 000000000..16de8eb97 --- /dev/null +++ b/IkiWiki/Plugin/album.pm @@ -0,0 +1,559 @@ +#!/usr/bin/perl +# Copyright © 2009-2011 Simon McVittie <http://smcv.pseudorandom.co.uk/> +# Licensed under the GNU GPL, version 2, or any later version published by the +# Free Software Foundation +package IkiWiki::Plugin::album; + +use warnings; +use strict; +use IkiWiki 3.00; + +sub import { + hook(type => "getsetup", id => "album", call => \&getsetup); + hook(type => "needsbuild", id => "album", call => \&needsbuild); + hook(type => "preprocess", id => "album", + call => \&preprocess_album, scan => 1); + hook(type => "preprocess", id => "albumsection", + call => \&preprocess_albumsection, scan => 1); + hook(type => "preprocess", id => "albumimage", + call => \&preprocess_albumimage, scan => 1); + hook(type => "pagetemplate", id => "album", call => \&pagetemplate); + + # We need these plugins. Additionally, meta is recommended. + IkiWiki::loadplugin("filecheck"); + IkiWiki::loadplugin("img"); + IkiWiki::loadplugin("inline"); + IkiWiki::loadplugin("trail"); + IkiWiki::loadplugin("transient"); +} + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => undef, + section => "widget", + }, + # FIXME: unimplemented + album_copydir => { + type => "string", + example => "$ENV{HOME}/photos-to-upload", + description => "if set, copy photos to this directory at reduced size", + advanced => 1, + safe => 0, + rebuild => 1, + }, +} + +# Terminology: +# +# album - main page for an album/gallery, contains a list of inlined viewers +# viewer - page generated to contain/display/represent one image +# section - a subset of the viewers in an album +# +# Page state for albums: +# +# size - default size for viewers in this album +# thumbnailsize - size to resize thumbnails to +# viewertemplate - template to use for viewers +# nexttemplate - template for "next image", as embedded in viewers +# prevtemplate - template for "previous image", as embedded in viewers +# sort - as for inline +# sections - ref to array of pagespecs representing sections +# viewers - list of viewers' names +# +# Page state for image viewers: +# +# album - full name of associated album +# image - full name of image file +# caption - caption if any +# size, thumbnailsize, viewertemplate, nexttemplate, prevtemplate - +# override the corresponding option for the album + +sub isalbumableimage ($) { + my $file=shift; + + return $file =~ /\.(png|gif|jpg|jpeg|mov)$/i; +} + +sub needsbuild { + my $needsbuild = shift; + my $deleted = shift; + + foreach my $page (@$needsbuild, @$deleted) { + # it's neither an album nor a viewer, unless it later says + # it is + delete $pagestate{$page}{album}; + } + + return $needsbuild; +} + +sub thumbnail { + my $viewer = shift; + my $destpage = shift; + my $thumbnailsize = shift; + my $image = $pagestate{$viewer}{album}{image}; + + # img requires that each file is generated by one page, so nominate + # the viewer as the page that makes the thumbnail + my $img = ""; + my $title = IkiWiki::Plugin::trail::title_of($viewer); + + if (IkiWiki::isinlinableimage($image)) { + $img = IkiWiki::Plugin::img::preprocess( + "$image" => undef, + link => "/$viewer", + size => ($thumbnailsize or '96x96'), + alt => $title, + title => $title, + page => $viewer, + destpage => $destpage); + } + + return $img; +} + +sub show_in_album { + my $viewers = shift; + my %params = @_; + + return IkiWiki::preprocess_inline( + pagenames => join(" ", @$viewers), + actions => "no", + feeds => "no", + template => "albumitem", + %params); +} + +# album => [ list of viewers ] +my %scanned; + +sub create_viewer { + my ($viewer, $image, $album) = @_; + + my $vfile = newpagefile($viewer, $config{default_pageext}); + + # FIXME: try to read creation date, copyright etc. from EXIF tags + + add_autofile($vfile, "album", sub { + my $message = sprintf(gettext("creating album page %s"), $viewer); + debug($message); + + my $content = <<"END"; +[[!albumimage + title="" + caption="" + date="" + updated="" + author="" + authorurl="" + copyright="" + license="" + description="" + ]] +END + # size, thumbnailsize, viewertemplate, prevtemplate, + # nexttemplate aren't in the generated page because + # they're not expected to be commonly used - setting + # them for an entire album is likely to be more useful + + writefile($vfile, $IkiWiki::Plugin::transient::transientdir, + $content); + }); +} + +sub scan_images { + my $album = $_[0]; + + # There are two purposes to this, optimization (don't bother + # re-scanning images) and correctness (when we're preprocessing + # after the scan stage, we don't want to re-run scan_binary because + # it would override the metadata from [[!albumimage]]). + return @{$scanned{$album}} if exists $scanned{$album}; + + # All the images that are attached to an album or its subpages count as + # (potential) members of it. For each one, we synthesize a page if no + # page exists. + my $regexp = qr{^\Q$album\E/}i; + my @viewers; + + # collect the images + foreach my $candidate (keys %pagesources) { + if ($candidate =~ $regexp && + isalbumableimage($candidate)) { + my $viewer = $candidate; + $viewer =~ s/\.[^.]+$//; + + if (exists $IkiWiki::pagecase{lc $viewer}) { + $viewer = $IkiWiki::pagecase{lc $viewer}; + } + else { + create_viewer($viewer, $candidate, $album); + } + + push @viewers, $viewer; + + # FIXME: what if albums are nested? Current resolution + # is that the image goes in a randomly selected album. + # Because pages are scanned in arbitrary order, I + # don't think we can do better. + $pagestate{$viewer}{album}{album} = $album; + + # FIXME: what if there's more than one image file + # with the same basename? Current resolution is that + # a random one "wins". + $pagestate{$viewer}{album}{image} = $candidate; + } + } + + $scanned{$album} = \@viewers; + return @viewers; +} + +# These hashes are populated by collect_images whenever an album, or any +# image in that album, has changed. + +# album => { filter => array of viewers in that section } +# filter "" is the catch-all for images in no other section +my %albumsections; +# linked list of viewers in each album +my (%before, %after); +# viewer => index number in its album +my %albumorder; + +sub get_adjacent { + my ($trail, $member) = @_; + + IkiWiki::Plugin::trail::prerender(); + my $adjacent = $pagestate{$member}{trail}{item}{$trail}; + return (undef, undef) unless $adjacent; + return @$adjacent; +} + +sub collect_images { + my $album = shift; + + return if $albumsections{$album}; + + my $sort = $pagestate{$album}{album}{sort}; + + if (!defined $sort) { + $sort = '-age'; + } + + my %sections; + my $_; # localize iterator variable + + my @remaining = @{$pagestate{$album}{album}{viewers}}; + + foreach my $filter (@{$pagestate{$album}{album}{sections}}) { + next if $filter eq ''; + my @section = pagespec_match_list($album, + $filter, sort => $sort, + list => [@remaining]); + my %set = map { $_ => 1 } @section; + + @remaining = grep { ! exists $set{$_} } @remaining; + + $sections{$filter} = \@section; + } + + # the pagespec here matches everything; the part we actually want + # is the sorting + $sections{""} = [pagespec_match_list($album, + "internal(*)", sort => $sort, + list => [@remaining])]; + + my @ordered; + + foreach my $filter (@{$pagestate{$album}{album}{sections}}) { + my $pages; + $pages = $sections{$filter}; + push @ordered, @$pages; + $albumsections{$album}{$filter} = $pages; + } + + for (my $i = 0; $i <= $#ordered; $i++) { + my $viewer = $ordered[$i]; + $albumorder{$viewer} = $i; + # We don't need to track what's before or after: + # trail has API for that. + } +} + +sub preprocess_album { + # [[!album]] + my %params=@_; + my $album = $params{page}; + + my @viewers = scan_images($album); + + # placeholder for the "remaining images" section + push @{$pagestate{$album}{album}{sections}}, "" + unless grep { $_ eq "" } + @{$pagestate{$album}{album}{sections}}; + + $pagestate{$album}{album} = { + sort => $params{sort}, + size => $params{size}, + thumbnailsize => $params{thumbnailsize}, + viewertemplate => $params{viewertemplate}, + nexttemplate => $params{nexttemplate}, + prevtemplate => $params{prevtemplate}, + viewers => [@viewers], + # in the render phase, we want to keep the sections that we + # accumulated during the scan phase, if any + sections => $pagestate{$album}{album}{sections}, + }; + + # The sort order depends on matching pagespecs for each + # section, so we can't define it yet - delegate it to a + # sortspec defined by this plugin, which can collect the + # images lazily. + IkiWiki::Plugin::trail::preprocess_trailoptions( + sort => 'albumorder', + page => $album, + destpage => $params{destpage}, + ); + + my %trailparams = ( + pagenames => join(' ', @viewers), + page => $album, + destpage => $params{destpage}, + ); + + if (defined wantarray) { + collect_images($album) unless $albumsections{$album}; + + scalar IkiWiki::Plugin::trail::preprocess_trailitems(%trailparams); + + return show_in_album($albumsections{$album}{""}, + page => $album, + destpage => $params{destpage}); + } + else { + IkiWiki::Plugin::trail::preprocess_trailitems(%trailparams); + } +} + +sub preprocess_albumsection { + # [[!albumsection filter="friday/*"]] + my %params=@_; + my $album = $params{page}; + my $filter = $params{filter}; + my $_; + + # remember the filter for this section so the "remaining images" section + # won't include these images (this needs to be run in the scan stage + # so the info will be there for the album directive) + push @{$pagestate{$album}{album}{sections}}, $filter + unless grep { $_ eq $filter } + @{$pagestate{$album}{album}{sections}}; + + # If we're just scanning, don't bother producing output + return unless defined wantarray; + + collect_images($album) unless $albumsections{$album}; + + return show_in_album($albumsections{$album}{$filter}, + page => $album, + destpage => $params{destpage}); +} + +sub preprocess_albumimage { + my %params=@_; + my $viewer = $params{page}; + + my $album = $pagestate{$viewer}{album}{album}; + my $image = $pagestate{$viewer}{album}{image}; + + if (! defined $album || ! defined $image) { + error(sprintf(gettext("%s is not in any album"), $viewer)); + } + + $pagestate{$viewer}{album} = { + album => $album, + image => $image, + }; + + # [[!albumimage title=foo copyright=bar]] is a shortcut for a couple + # of [[!meta]] invocations. Zero-length metadata gets ignored, so we + # can put all the available metadata in the template album page, + # with zero-length values. + if (IkiWiki::Plugin::meta->can('preprocess')) { + foreach my $meta (qw(title date updated author authorurl + copyright license description)) { + if (defined $params{$meta} && length $params{$meta}) { + IkiWiki::Plugin::meta::preprocess( + $meta => $params{$meta}, + page => $viewer); + } + } + } + + # The thumbnail has to have exactly one source, so thumbnail() always + # claims that it is this page. To avoid ikiwiki deleting the + # thumbnail, we need to make sure will_render() gets called, and this + # seems the easiest way to do that. + if (defined wantarray) { + my $foo = thumbnail($viewer, $viewer, + $pagestate{$album}{album}{thumbnailsize}); + } + else { + thumbnail($viewer, $viewer, + $pagestate{$album}{album}{thumbnailsize}); + } + + add_depends($viewer, $album); + + # If we're just scanning, don't bother producing output + return unless defined wantarray; + + # Copy various settings from the album, unless overridden + foreach my $k (qw(viewertemplate nexttemplate prevtemplate size)) { + $params{$k} = $pagestate{$album}{album}{$k} + unless defined $params{$k} && length $params{$k}; + } + + # Because we're no longer in the scan phase, we know that + # preprocess_album has already run, so we know: + # - which album this photo appears in + # - what order the album is in + # (either because the scan phase has run for modified pages, or + # because the pagestate was loaded from last time for unmodified + # pages.) + + collect_images($album) unless $albumsections{$album}; + + my ($prevpage, $nextpage) = get_adjacent($album, $viewer); + + my $prev = ''; + my $next = ''; + + if (defined $nextpage) { + add_depends($album, $nextpage); + $next = show_prevnext($nextpage, $viewer, "next"); + } + + if (defined $prevpage) { + add_depends($album, $prevpage); + $prev = show_prevnext($prevpage, $viewer, "prev"); + } + + my $img; + if (IkiWiki::isinlinableimage($image)) { + my $title = IkiWiki::Plugin::trail::title_of($viewer); + + $img = IkiWiki::Plugin::img::preprocess("$image" => undef, + title => $title, + alt => $title, + size => ($params{size} or 'full'), + page => $viewer, + destpage => $params{destpage}); + } + else { + $img = htmllink($viewer, $params{destpage}, "/$image"); + } + + my $viewertemplate = template( + $pagestate{$album}{album}{viewertemplate} or + 'albumviewer.tmpl'); + $viewertemplate->param(page => $viewer, + img => $img, + next => $next, + prev => $prev); + + IkiWiki::run_hooks(pagetemplate => sub { + shift->(page => $viewer, destpage => $params{destpage}, + template => $viewertemplate); + }); + + return $viewertemplate->output; +} + +sub pagetemplate (@) { + my %params=@_; + my $template = $params{template}; + + eval q{use Image::Magick}; + return if $@; + + if (exists $pagestate{$params{page}}{album}{image}) { + # the page is a viewer, maybe treat it specially + my $viewer = $params{page}; + my $album = $pagestate{$viewer}{album}{album}; + my $image = $pagestate{$viewer}{album}{image}; + + return unless defined $album; + return unless defined $image; + + my $title = IkiWiki::Plugin::trail::title_of($viewer); + $template->param(album => $album); + $template->param(albumurl => urlto($album, $params{destpage})); + $template->param(albumtitle => $title); + + if ($template->query(name => 'thumbnail')) { + $template->param(thumbnail => + thumbnail($viewer, $params{destpage})); + } + if (IkiWiki::isinlinableimage($image) + && ($template->query(name => 'imagewidth') || + $template->query(name => 'imageheight') || + $template->query(name => 'imagefilesize') || + $template->query(name => 'imageformat'))) { + my $im = Image::Magick->new; + my ($w, $h, $s, $f) = $im->Ping(srcfile($image, 1)); + $s = IkiWiki::Plugin::filecheck::humansize($s); + $template->param(imagewidth => $w, imageheight => $h, + imagefilesize => $s, imageformat => $f); + } + if ($template->query(name => 'caption')) { + $template->param(caption => + $pagestate{$viewer}{album}{caption}); + } + } +} + +sub show_prevnext { + my ($page, $destpage, $which) = @_; + + my $template = template("album$which.tmpl", blind_cache => 1); + my $title = IkiWiki::Plugin::trail::title_of($page); + $template->param( + pageurl => urlto($page, $destpage), + title => $title, + ctime => displaytime($IkiWiki::pagectime{$page}), + mtime => displaytime($IkiWiki::pagemtime{$page}), + ); + + IkiWiki::run_hooks(pagetemplate => sub { + shift->(page => $page, destpage => $destpage, + template => $template); + }); + + return $template->output; +} + +package IkiWiki::SortSpec; + +sub cmp_albumorder { + # Firstly, are they even in the same album? If not, order is + # indeterminate - so let's just compare the paths. + my $album = $IkiWiki::pagestate{$a}{album}{album}; + my $album2 = $IkiWiki::pagestate{$b}{album}{album}; + + if (! defined $album || ! defined $album2 || $album ne $album2) { + return $a cmp $b; + } + + # OK, now we need to work out where they are in the album. + # We do this lazily because until the scan stage has finished, + # it could change. + IkiWiki::Plugin::album::collect_images($album) unless $albumsections{$album}; + + return $albumorder{$a} <=> $albumorder{$b}; + +} + +1; |