]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blob - doc/users/smcv/gallery.mdwn
po: make the "backlinks involve dependencies" feature optional
[git.ikiwiki.info.git] / doc / users / smcv / gallery.mdwn
1 [[!template id=plugin name=smcvgallery author="[[Simon_McVittie|smcv]]"]]
2 [[!tag type/chrome]]
4 This plugin has not yet been written; this page is an experiment in
5 design-by-documentation :-)
7 ## Requirements
9 This plugin formats a collection of images into a photo gallery,
10 in the same way as many websites: good examples include the
11 PHP application [Gallery](http://gallery.menalto.com/), Flickr,
12 and Facebook's Photos "application".
14 The web UI I'm trying to achieve consists of one
15 [HTML page of thumbnails](http://www.pseudorandom.co.uk/2008/2008-03-08-panic-cell-gig/)
16 as an entry point to the gallery, where each thumbnail links to
17 [a "viewer" HTML page](http://www.pseudorandom.co.uk/2008/2008-03-08-panic-cell-gig/img_0068/)
18 with a full size image, next/previous thumbnail links, and
19 [[plugins/comments]].
21 (The Summer of Code [[plugins/contrib/gallery]] plugin does the
22 next/previous UI in Javascript using Lightbox, which means that
23 individual photos can't be bookmarked in a meaningful way, and
24 the best it can do as a fallback for non-Javascript browsers
25 is to provide a direct link to the image.)
27 Other features that would be good to have:
29 * minimizing the number of separate operations needed to make a gallery -
30   editing one source file per gallery is acceptable, editing one
31   source file per photo is not
33 * keeping photos outside source code control, for instance in an
34   underlay
36 * assigning [[tags|ikiwiki/directive/tag]] to photos, providing a
37   superset of Facebook's "show tagged photos of this person" functionality
39 * constructing galleries entirely via the web by uploading attachments
41 * inserting grouping (section headings) within a gallery; as in the example
42   linked above, I'd like this to split up the thumbnails but not the
43   next/previous trail
45 * rendering an `<object>/<embed>` arrangement to display videos, and possibly
46   thumbnailing them in the same way as totem-video-thumbnailer
47   (my camera can record short videos, so some of my web photo galleries
48   contain them)
50 My plan is to have these directives:
52 * \[[!gallery]] registers the page it's on as a gallery, and displays all
53   photos that are part of this gallery but not part of a \[[!gallerysection]]
54   (below).
56   All images (i.e. `*.png *.jpg *.gif`) that are attachments to the gallery page
57   or its subpages are considered to be part of the gallery.
59   Optional arguments:
61   * filter="[[ikiwiki/PageSpec]]": only consider images to be part of the
62     gallery if they also match this filter
64   * sort="date|filename": order in which to sort the images
66 * \[[!gallerysection filter="[[ikiwiki/PageSpec]]"]] displays all photos in the
67   gallery that match the filter
69 So,
70 [the gallery I'm using as an example](http://www.pseudorandom.co.uk/2008/2008-03-08-panic-cell-gig/)
71 could look something like this:
73     \[[!gallery]]
74     <!-- replaced with one uncategorized photo -->
76     # Gamarra
78     \[[!gallerysection filter="link(sometag)"]]
79     <!-- all the Gamarra photos -->
81     # Smokescreen
83     \[[!gallerysection filter="link(someothertag)"]]
84     <!-- all the Smokescreen photos -->
86     <!-- ... -->
88 ## Implementation ideas
90 The next/previous part this plugin overlaps with [[todo/wikitrails]].
92 A \[[!galleryimg]] directive to assign metadata to images is probably necessary, so
93 the gallery page can contain something like:
95     \[[!galleryimg p1010001.jpg title="..." caption="..." tags="foo"]]
96     \[[!galleryimg p1010002.jpg title="..." caption="..." tags="foo bar"]]
98 Making the viewer pages could be rather tricky. Here are some options:
99 "synthesize source pages for viewers" is the one I'm leaning towards at the
100 moment.
102 ### Viewers' source page is the gallery
104 One possibility is to write out the viewer pages as a side-effect of
105 preprocessing the \[[!gallery]] directive. The proof-of-concept implementation
106 below does this.  However, this does mean the viewer pages can't have tags or
107 metadata of their own and can't be matched by [[pagespecs|ikiwiki/pagespec]] or
108 [[wikilinks|ikiwiki/wikilink]]. It might be possible to implement tagging by
109 using \[[!galleryimg]] to assign the metadata to the *images* instead of their
110 viewers.
112 ### Synthesize source pages for viewers
114 Another is to synthesize source pages for the viewers. This means they can have
115 tags and metadata, but trying to arrange for them to be scanned etc. correctly
116 without needing another refresh run is somewhat terrifying.
117 [[plugins/autoindex]] can safely create source pages because it runs in
118 the refresh hook, but I don't really like the idea of a refresh hook that scans
119 all source pages to see if they contain \[[!gallery]]...
121 The photo galleries I have at the moment, like the Panic Cell example above,
122 are made by using an external script to parse XML gallery descriptions (lists
123 of image filenames, with metadata such as titles), and using this to write
124 IkiWiki markup into a directory which is then used as an underlay. This is a
125 hack, but it works. The use of XML is left over from a previous attempt at
126 solving the same problem using Django.
128 Perhaps a better approach would be to have a setupfile option that names a
129 particular underlay directory (meeting the objective of not having large
130 photos under source code control) and generates a source page for each file
131 in that directory during the refresh hook. The source pages could be in the
132 underlay until they are edited (e.g. tagged), at which point they would be
133 copied into the source-code-controlled version in the usual way.
135 The synthetic source pages can be very simple, using the same trick as my
136 [[plugins/comments]] plugin (a dedicated [[directive|ikiwiki/directives]]
137 encapsulating everything the plugin needs). If the plugin automatically
138 gathers information like file size, pixel size, date etc. from the images, then
139 only the human-edited information and a filename reference need to be present
140 in the source page; with some clever lookup rules based on the filename of
141 the source page, not even the photo's filename is necessarily needed.
143     \[[!meta title="..."]]
144     \[[!meta date="..."]]
145     \[[!meta copyright="..."]]
146     \[[!tag ...]]
148     \[[!galleryimageviewer p1010001.jpg]]
150 However, this would mean that editing tags and other metadata would require
151 editing pages individually. Rather than trying to "fix" that, perhaps it would
152 be better to have a special CGI interface for bulk tagging/metadata editing.
153 This could even be combined with a bulk upload form (a reasonable number of
154 file upload controls - maybe 20 - with metadata alongside each).
156 Uploading multiple images is necessarily awkward due to restrictions placed on
157 file upload controls by browsers for security reasons - sites like Facebook
158 allow whole directories to be uploaded at the same time, but they achieve this
159 by using a signed Java applet with privileged access to the user's filesystem.
161 I've found that it's often useful to be able to force the creation time of
162 photos (my camera's battery isn't very reliable, and it frequently decides that
163 the date is 0000-00-00 00:00:00), so treating the \[[!meta date]] of the source
164 page and the creation date of the photo as synonymous would be useful.
166 ### Images are the viewer's source - special filename extension
168 Making the image be the source page (and generate HTML itself) would be
169 possible, but I wouldn't want to generate a HTML viewer for every `.jpg` on a
170 site, so either the images would have to have a special extension (awkward for
171 uploads from Windows users) or the plugin would have to be able to change
172 whether HTML was generated in some way (not currently possible).
174 ### Images are the viewer's source - alter `ispage()`
176 It might be possible to hack up `ispage()` so some, but not all, images are
177 considered to "be a page":
179 * srcdir/not-a-photo.jpg → destdir/not-a-photo.jpg
180 * srcdir/gallery/photo.jpg → destdir/gallery/photo/index.html
182 Perhaps one way to do this would be for the photos to appear in a particular
183 underlay directory, which would also fulfil the objective of having photos not
184 be version-controlled:
186 * srcdir/not-a-photo.jpg → destdir/not-a-photo.jpg
187 * underlay/gallery/photo.jpg → destdir/gallery/photo/index.html
189 ## Proof-of-concept implementation of "viewers' source page is the gallery"
191     #!/usr/bin/perl
192     package IkiWiki::Plugin::gallery;
193     
194     use warnings;
195     use strict;
196     use IkiWiki 2.00;
197     
198     sub import {
199         hook(type => "getsetup", id => "gallery",  call => \&getsetup);
200         hook(type => "checkconfig", id => "gallery", call => \&checkconfig);
201         hook(type => "preprocess", id => "gallery",
202                 call => \&preprocess_gallery, scan => 1);
203         hook(type => "preprocess", id => "gallerysection",
204                 call => \&preprocess_gallerysection, scan => 1);
205         hook(type => "preprocess", id => "galleryimg",
206                 call => \&preprocess_galleryimg, scan => 1);
207     }
208     
209     sub getsetup () {
210         return
211                 plugin => {
212                         safe => 1,
213                         rebuild => undef,
214                 },
215     }
216     
217     sub checkconfig () {
218     }
219     
220     # page that is a gallery => array of images
221     my %galleries;
222     # page that is a gallery => array of filters
223     my %sections;
224     # page that is an image => page name of generated "viewer"
225     my %viewers;
226     
227     sub preprocess_gallery {
228         # \[[!gallery filter="!*/cover.jpg"]]
229         my %params=@_;
230     
231         my $subpage = qr/^\Q$params{page}\E\//;
232     
233         my @images;
234     
235         foreach my $page (keys %pagesources) {
236                 # Reject anything not a subpage or attachment of this page
237                 next unless $page =~ $subpage;
238     
239                 # Reject non-images
240                 # FIXME: hard-coded list of extensions
241                 next unless $page =~ /\.(jpg|gif|png|mov)$/;
242     
243                 # Reject according to the filter, if any
244                 next if (exists $params{filter} &&
245                         !pagespec_match($page, $params{filter},
246                                 location => $params{page}));
247     
248                 # OK, we'll have that one
249                 push @images, $page;
250     
251                 my $viewername = $page;
252                 $viewername =~ s/\.[^.]+$//;
253                 $viewers{$page} = $viewername;
254     
255                 my $filename = htmlpage($viewername);
256                 will_render($params{page}, $filename);
257         }
258     
259         $galleries{$params{page}} = \@images;
260     
261         # If we're just scanning, don't bother producing output
262         return unless defined wantarray;
263     
264         # actually render the viewers
265         foreach my $img (@images) {
266                 my $filename = htmlpage($viewers{$img});
267                 debug("rendering image viewer $filename for $img");
268                 writefile($filename, $config{destdir}, "# placeholder");
269         }
270     
271         # display a list of "loose" images (those that are in no section);
272         # this works because we collected the sections' filters during the
273         # scan stage
274     
275         my @loose = @images;
276     
277         foreach my $filter (@{$sections{$params{page}}}) {
278                 my $_;
279                 @loose = grep { !pagespec_match($_, $filter,
280                                 location => $params{page}) } @loose;
281         }
282     
283         my $_;
284         my $ret = "<ul>\n";
285         foreach my $img (@loose) {
286                 $ret .= "<li>";
287                 $ret .= "<a href=\"" . urlto($viewers{$img}, $params{page});
288                 $ret .= "\">$img</a></li>\n"
289         }
290         return "$ret</ul>\n";
291     }
292     
293     sub preprocess_gallerysection {
294         # \[[!gallerysection filter="friday/*"]]
295         my %params=@_;
296     
297         # remember the filter for this section so the "loose images" section
298         # won't include these images
299         push @{$sections{$params{page}}}, $params{filter};
300     
301         # If we're just scanning, don't bother producing output
302         return unless defined wantarray;
303     
304         # this relies on the fact that we ran preprocess_gallery once
305         # already, during the scan stage
306         my @images = @{$galleries{$params{page}}};
307         @images = grep { pagespec_match($_, $params{filter},
308                         location => $params{page}) } @images;
309     
310         my $_;
311         my $ret = "<ul>\n";
312         foreach my $img (@images) {
313                 $ret .= "<li>";
314                 $ret .= htmllink($params{page}, $params{destpage},
315                         $viewers{$img});
316                 $ret .= "</li>";
317         }
318         return "$ret</ul>\n";
319     }
320     
321     sub preprocess_galleryimg {
322         # \[[!galleryimg p1010001.jpg title="" caption="" tags=""]]
323         my $file = $_[0];
324         my %params=@_;
325     
326         return "";
327     }
328     
329     1