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