2 # Copyright © 2009-2011 Simon McVittie <http://smcv.pseudorandom.co.uk/>
3 # Licensed under the GNU GPL, version 2, or any later version published by the
4 # Free Software Foundation
5 package IkiWiki::Plugin::album;
12 hook(type => "getsetup", id => "album", call => \&getsetup);
13 hook(type => "needsbuild", id => "album", call => \&needsbuild);
14 hook(type => "preprocess", id => "album",
15 call => \&preprocess_album, scan => 1);
16 hook(type => "preprocess", id => "albumsection",
17 call => \&preprocess_albumsection, scan => 1);
18 hook(type => "preprocess", id => "albumimage",
19 call => \&preprocess_albumimage, scan => 1);
20 hook(type => "pagetemplate", id => "album", call => \&pagetemplate);
22 # We need these plugins. Additionally, meta is recommended.
23 IkiWiki::loadplugin("filecheck");
24 IkiWiki::loadplugin("img");
25 IkiWiki::loadplugin("inline");
26 IkiWiki::loadplugin("trail");
27 IkiWiki::loadplugin("transient");
37 # FIXME: unimplemented
40 example => "$ENV{HOME}/photos-to-upload",
41 description => "if set, copy photos to this directory at reduced size",
50 # album - main page for an album/gallery, contains a list of inlined viewers
51 # viewer - page generated to contain/display/represent one image
52 # section - a subset of the viewers in an album
54 # Page state for albums:
56 # size - default size for viewers in this album
57 # thumbnailsize - size to resize thumbnails to
58 # viewertemplate - template to use for viewers
59 # nexttemplate - template for "next image", as embedded in viewers
60 # prevtemplate - template for "previous image", as embedded in viewers
61 # sort - as for inline
62 # sections - ref to array of pagespecs representing sections
63 # viewers - list of viewers' names
65 # Page state for image viewers:
67 # album - full name of associated album
68 # image - full name of image file
69 # caption - caption if any
70 # size, thumbnailsize, viewertemplate, nexttemplate, prevtemplate -
71 # override the corresponding option for the album
73 sub isalbumableimage ($) {
76 return $file =~ /\.(png|gif|jpg|jpeg|mov)$/i;
80 my $needsbuild = shift;
83 foreach my $page (@$needsbuild, @$deleted) {
84 # it's neither an album nor a viewer, unless it later says
86 delete $pagestate{$page}{album};
95 my $thumbnailsize = shift;
96 my $image = $pagestate{$viewer}{album}{image};
98 # img requires that each file is generated by one page, so nominate
99 # the viewer as the page that makes the thumbnail
101 my $title = IkiWiki::Plugin::trail::title_of($viewer);
103 if (IkiWiki::isinlinableimage($image)) {
104 $img = IkiWiki::Plugin::img::preprocess(
107 size => ($thumbnailsize or '96x96'),
111 destpage => $destpage);
121 return IkiWiki::preprocess_inline(
122 pagenames => join(" ", @$viewers),
125 template => "albumitem",
129 # album => [ list of viewers ]
133 my ($viewer, $image, $album) = @_;
135 my $vfile = newpagefile($viewer, $config{default_pageext});
137 # FIXME: try to read creation date, copyright etc. from EXIF tags
139 add_autofile($vfile, "album", sub {
140 my $message = sprintf(gettext("creating album page %s"), $viewer);
143 my $content = <<"END";
156 # size, thumbnailsize, viewertemplate, prevtemplate,
157 # nexttemplate aren't in the generated page because
158 # they're not expected to be commonly used - setting
159 # them for an entire album is likely to be more useful
161 writefile($vfile, $IkiWiki::Plugin::transient::transientdir,
169 # There are two purposes to this, optimization (don't bother
170 # re-scanning images) and correctness (when we're preprocessing
171 # after the scan stage, we don't want to re-run scan_binary because
172 # it would override the metadata from [[!albumimage]]).
173 return @{$scanned{$album}} if exists $scanned{$album};
175 # All the images that are attached to an album or its subpages count as
176 # (potential) members of it. For each one, we synthesize a page if no
178 my $regexp = qr{^\Q$album\E/}i;
182 foreach my $candidate (keys %pagesources) {
183 if ($candidate =~ $regexp &&
184 isalbumableimage($candidate)) {
185 my $viewer = $candidate;
186 $viewer =~ s/\.[^.]+$//;
188 if (exists $IkiWiki::pagecase{lc $viewer}) {
189 $viewer = $IkiWiki::pagecase{lc $viewer};
192 create_viewer($viewer, $candidate, $album);
195 push @viewers, $viewer;
197 # FIXME: what if albums are nested? Current resolution
198 # is that the image goes in a randomly selected album.
199 # Because pages are scanned in arbitrary order, I
200 # don't think we can do better.
201 $pagestate{$viewer}{album}{album} = $album;
203 # FIXME: what if there's more than one image file
204 # with the same basename? Current resolution is that
205 # a random one "wins".
206 $pagestate{$viewer}{album}{image} = $candidate;
210 $scanned{$album} = \@viewers;
214 # These hashes are populated by collect_images whenever an album, or any
215 # image in that album, has changed.
217 # album => { filter => array of viewers in that section }
218 # filter "" is the catch-all for images in no other section
220 # linked list of viewers in each album
221 my (%before, %after);
222 # viewer => index number in its album
228 return if $albumsections{$album};
230 my $sort = $pagestate{$album}{album}{sort};
232 if (!defined $sort) {
237 my $_; # localize iterator variable
239 my @remaining = @{$pagestate{$album}{album}{viewers}};
241 foreach my $filter (@{$pagestate{$album}{album}{sections}}) {
242 next if $filter eq '';
243 my @section = pagespec_match_list($album,
244 $filter, sort => $sort,
245 list => [@remaining]);
246 my %set = map { $_ => 1 } @section;
248 @remaining = grep { ! exists $set{$_} } @remaining;
250 $sections{$filter} = \@section;
253 # the pagespec here matches everything; the part we actually want
255 $sections{""} = [pagespec_match_list($album,
256 "internal(*)", sort => $sort,
257 list => [@remaining])];
261 foreach my $filter (@{$pagestate{$album}{album}{sections}}) {
263 $pages = $sections{$filter};
264 push @ordered, @$pages;
265 $albumsections{$album}{$filter} = $pages;
268 for (my $i = 0; $i <= $#ordered; $i++) {
269 my $viewer = $ordered[$i];
270 $albumorder{$viewer} = $i;
271 # We don't need to track what's before or after:
272 # trail has API for that.
276 sub preprocess_album {
279 my $album = $params{page};
281 my @viewers = scan_images($album);
283 # placeholder for the "remaining images" section
284 push @{$pagestate{$album}{album}{sections}}, ""
285 unless grep { $_ eq "" }
286 @{$pagestate{$album}{album}{sections}};
288 $pagestate{$album}{album} = {
289 sort => $params{sort},
290 size => $params{size},
291 thumbnailsize => $params{thumbnailsize},
292 viewertemplate => $params{viewertemplate},
293 nexttemplate => $params{nexttemplate},
294 prevtemplate => $params{prevtemplate},
295 viewers => [@viewers],
296 # in the render phase, we want to keep the sections that we
297 # accumulated during the scan phase, if any
298 sections => $pagestate{$album}{album}{sections},
301 # The sort order depends on matching pagespecs for each
302 # section, so we can't define it yet - delegate it to a
303 # sortspec defined by this plugin, which can collect the
305 IkiWiki::Plugin::trail::preprocess_trailoptions(
306 sort => 'albumorder',
308 destpage => $params{destpage},
312 pagenames => join(' ', @viewers),
314 destpage => $params{destpage},
317 if (defined wantarray) {
318 collect_images($album) unless $albumsections{$album};
320 scalar IkiWiki::Plugin::trail::preprocess_trailitems(%trailparams);
322 return show_in_album($albumsections{$album}{""},
324 destpage => $params{destpage});
327 IkiWiki::Plugin::trail::preprocess_trailitems(%trailparams);
331 sub preprocess_albumsection {
332 # [[!albumsection filter="friday/*"]]
334 my $album = $params{page};
335 my $filter = $params{filter};
338 # remember the filter for this section so the "remaining images" section
339 # won't include these images (this needs to be run in the scan stage
340 # so the info will be there for the album directive)
341 push @{$pagestate{$album}{album}{sections}}, $filter
342 unless grep { $_ eq $filter }
343 @{$pagestate{$album}{album}{sections}};
345 # If we're just scanning, don't bother producing output
346 return unless defined wantarray;
348 collect_images($album) unless $albumsections{$album};
350 return show_in_album($albumsections{$album}{$filter},
352 destpage => $params{destpage});
355 sub preprocess_albumimage {
357 my $viewer = $params{page};
359 my $album = $pagestate{$viewer}{album}{album};
360 my $image = $pagestate{$viewer}{album}{image};
362 if (! defined $album || ! defined $image) {
363 error(sprintf(gettext("%s is not in any album"), $viewer));
366 $pagestate{$viewer}{album} = {
371 # [[!albumimage title=foo copyright=bar]] is a shortcut for a couple
372 # of [[!meta]] invocations. Zero-length metadata gets ignored, so we
373 # can put all the available metadata in the template album page,
374 # with zero-length values.
375 if (IkiWiki::Plugin::meta->can('preprocess')) {
376 foreach my $meta (qw(title date updated author authorurl
377 copyright license description)) {
378 if (defined $params{$meta} && length $params{$meta}) {
379 IkiWiki::Plugin::meta::preprocess(
380 $meta => $params{$meta},
386 # The thumbnail has to have exactly one source, so thumbnail() always
387 # claims that it is this page. To avoid ikiwiki deleting the
388 # thumbnail, we need to make sure will_render() gets called, and this
389 # seems the easiest way to do that.
390 if (defined wantarray) {
391 my $foo = thumbnail($viewer, $viewer,
392 $pagestate{$album}{album}{thumbnailsize});
395 thumbnail($viewer, $viewer,
396 $pagestate{$album}{album}{thumbnailsize});
399 add_depends($viewer, $album);
401 # If we're just scanning, don't bother producing output
402 return unless defined wantarray;
404 # Copy various settings from the album, unless overridden
405 foreach my $k (qw(viewertemplate nexttemplate prevtemplate size)) {
406 $params{$k} = $pagestate{$album}{album}{$k}
407 unless defined $params{$k} && length $params{$k};
410 # Because we're no longer in the scan phase, we know that
411 # preprocess_album has already run, so we know:
412 # - which album this photo appears in
413 # - what order the album is in
414 # (either because the scan phase has run for modified pages, or
415 # because the pagestate was loaded from last time for unmodified
418 collect_images($album) unless $albumsections{$album};
420 my ($prevpage, $nextpage) = IkiWiki::Plugin::trail::nearby_pages($album, $viewer);
425 if (defined $nextpage) {
426 add_depends($album, $nextpage);
427 $next = show_prevnext($nextpage, $viewer, "next");
430 if (defined $prevpage) {
431 add_depends($album, $prevpage);
432 $prev = show_prevnext($prevpage, $viewer, "prev");
436 if (IkiWiki::isinlinableimage($image)) {
437 my $title = IkiWiki::Plugin::trail::title_of($viewer);
439 $img = IkiWiki::Plugin::img::preprocess("$image" => undef,
442 size => ($params{size} or 'full'),
444 destpage => $params{destpage});
447 $img = htmllink($viewer, $params{destpage}, "/$image");
450 my $viewertemplate = template(
451 $pagestate{$album}{album}{viewertemplate} or
453 $viewertemplate->param(page => $viewer,
458 IkiWiki::run_hooks(pagetemplate => sub {
459 shift->(page => $viewer, destpage => $params{destpage},
460 template => $viewertemplate);
463 return $viewertemplate->output;
466 sub pagetemplate (@) {
468 my $template = $params{template};
470 eval q{use Image::Magick};
473 if (exists $pagestate{$params{page}}{album}{image}) {
474 # the page is a viewer, maybe treat it specially
475 my $viewer = $params{page};
476 my $album = $pagestate{$viewer}{album}{album};
477 my $image = $pagestate{$viewer}{album}{image};
479 return unless defined $album;
480 return unless defined $image;
482 my $title = IkiWiki::Plugin::trail::title_of($viewer);
483 $template->param(album => $album);
484 $template->param(albumurl => urlto($album, $params{destpage}));
485 $template->param(albumtitle => $title);
487 if ($template->query(name => 'thumbnail')) {
488 $template->param(thumbnail =>
489 thumbnail($viewer, $params{destpage}));
491 if (IkiWiki::isinlinableimage($image)
492 && ($template->query(name => 'imagewidth') ||
493 $template->query(name => 'imageheight') ||
494 $template->query(name => 'imagefilesize') ||
495 $template->query(name => 'imageformat'))) {
496 my $im = Image::Magick->new;
497 my ($w, $h, $s, $f) = $im->Ping(srcfile($image, 1));
498 $s = IkiWiki::Plugin::filecheck::humansize($s);
499 $template->param(imagewidth => $w, imageheight => $h,
500 imagefilesize => $s, imageformat => $f);
502 if ($template->query(name => 'caption')) {
503 $template->param(caption =>
504 $pagestate{$viewer}{album}{caption});
510 my ($page, $destpage, $which) = @_;
512 my $template = template("album$which.tmpl", blind_cache => 1);
513 my $title = IkiWiki::Plugin::trail::title_of($page);
515 pageurl => urlto($page, $destpage),
517 ctime => displaytime($IkiWiki::pagectime{$page}),
518 mtime => displaytime($IkiWiki::pagemtime{$page}),
521 IkiWiki::run_hooks(pagetemplate => sub {
522 shift->(page => $page, destpage => $destpage,
523 template => $template);
526 return $template->output;
529 package IkiWiki::SortSpec;
532 # Firstly, are they even in the same album? If not, order is
533 # indeterminate - so let's just compare the paths.
534 my $album = $IkiWiki::pagestate{$a}{album}{album};
535 my $album2 = $IkiWiki::pagestate{$b}{album}{album};
537 if (! defined $album || ! defined $album2 || $album ne $album2) {
541 # OK, now we need to work out where they are in the album.
542 # We do this lazily because until the scan stage has finished,
544 IkiWiki::Plugin::album::collect_images($album) unless $albumsections{$album};
546 return $albumorder{$a} <=> $albumorder{$b};