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