]> git.defcon.no Git - plsgen/blob - plsgen
Four new features, two bugfixes. Version 0.3 code
[plsgen] / plsgen
1 #!/usr/bin/perl
2 use Image::ExifTool;
3 use POSIX;
4 use AppConfig;
5 use Getopt::Long;
6 use threads;
7 use threads::shared;
8 use strict;
9
10 # DONE: Config option for size of view/thumbnail
11 # DONE: Config option to disable arrow-up navigation
12 # DONE: Config option to disable image rescaling ?
13 # DONE: Consider multithreading
14 # TODO: Stripping of image suffix for HTML file w/config option?
15 # TODO: Templating of EXIF
16 # TODO: Priority/sorting of EXIF tags
17 # TODO: Possibility for hide/show EXIF
18 # TODO: Clear old generated files and meta on regen
19 # TODO: Use perlmagick et. al instead of convert/jhead..
20 # BUG: The naive handling of filenames breaks on special characters
21
22 my $version = "0.3";
23
24 # Runtime data
25 my $title = undef;
26 my $htmlonly = 0;
27 my $configfile = undef;
28 my $halp = undef;
29 my $album_comment = undef;
30 my $doexif = 1;
31 my $threadcounter = 0;
32 share ( $threadcounter );
33
34 GetOptions (
35 "title=s" => \$title,
36 "htmlonly!" => \$htmlonly,
37 "exif!" => \$doexif,
38 "config=s" => \$configfile,
39 "help" => \$halp
40 );
41 if ( $halp )
42 {
43 print "\nplsgen version " . $version . "\n";
44 print "Copyright Jon Langseth, BSD lisence\n\n";
45 print " --title='Your album title'\n";
46 print " Sets the album title. Title will be stored in .title\n";
47 print " If no title is given, it will be read from .title, if present\n";
48 print " --htmlonly\n";
49 print " Add this option to only generate HTML files\n";
50 print " No image operations will be performed with this option\n";
51 print " --noexif\n";
52 print " Forces EXIF data block not to be written to HTML output\n";
53 print " --config=/path/to/config\n";
54 print " Overrides default config file location.\n";
55 print " Default is to look for ./plsgen.cfg, then ../plsgen.cfg\n";
56 print " and finally /etc/plsgen.cfg.\n";
57 print "\n";
58 exit;
59 }
60
61 # Configuration data
62 my $config = get_config( $configfile );
63 my $full_tpl_file = $config->full_tpl_file;
64 my $index_tpl_file = $config->index_tpl_file;
65 my $css_file = $config->css_file;
66 my $navigation_script = $config->navigation_script;
67 my $up_arrow_navigate = $config->up_arrow_navigate;
68 my $disable_rescale = $config->disable_rescale;
69 my $columns = $config->columns;
70 my $rows = $config->rows;
71 my $thumb_pre = $config->thumb_pre;
72 my $thumb_post = $config->thumb_post;
73 my $thumb_maxwidth = $config->thumb_maxwidth;
74 my $thumb_maxheight = $config->thumb_maxheight;
75 my $view_maxwidth = $config->view_maxwidth;
76 my $view_maxheight = $config->view_maxheight;
77 my $comment_pre = $config->comment_pre;
78 my $comment_post = $config->comment_post;
79 my $idx_prev_text = $config->idx_prev_text;
80 my $idx_next_text = $config->idx_next_text;
81 my $idx_ret_text = $config->idx_ret_text;
82 my $footer_tag = $config->footer_tag;
83 my $concurrent = $config->concurrent_threads;
84
85 # Get or save the title, depending..
86 if ( (not $title) && ( -f ".title" ) )
87 {
88 open TF, "<.title";
89 $title = <TF>;
90 chomp($title);
91 close TF;
92 }
93 elsif ( $title )
94 {
95 open TF, ">.title";
96 print TF $title . "\n";
97 close TF;
98 }
99
100 # Get the album comment, if available
101 if ( -f "comment.txt" )
102 {
103 open CF, "<comment.txt";
104 while (<CF>) { $album_comment .= $_; }
105 close CF;
106 }
107
108 mkdir "thumb";
109 mkdir "view";
110
111 # Glob file names to an array
112 my @images = glob("*png *.jpg *.JPG *.gif *.GIF");
113 # Keep count of total number of images
114 my $imagecount = $#images+1;
115
116 my ($current, $previous, $next);
117 my $indexcount = 1;
118 my $indexfile;
119
120 my $main_meta = "<link rel='stylesheet' href='" . $css_file . "' type='text/css' />";
121 my $navigation_header = "<script type='text/javascript' src='" . $navigation_script . "'></script>";
122
123 my $thumbs = "<div class='thumbnails'>";
124
125 my $indexes = ceil( $imagecount/($rows*$columns) );
126 my $gentime = localtime;
127
128 # In groups of N ( X rows x Y colums of index):
129 # - Step through image array:
130 for ( my $i = 1; $i <= $imagecount; $i++)
131 {
132 $current = $images[$i-1];
133 # - - Previous image is previous location in array, unless cur=first
134 $previous = $images[$i-2] if $i > 1;
135 $previous = undef if $i == 1;
136 # - - Next image is next location in array, unless cur=last.
137 $next = $images[$i] if $i < ( $imagecount);
138 $next = undef if $i >= ( $imagecount);
139
140 #Set up text-strings for template replacement
141 if ( $indexcount == 1 ) { $indexfile = "index.html"; }
142 else { $indexfile = "index" . $indexcount . ".html"; }
143 my $navscript = gen_navscript( $previous, $next, $indexfile );
144 my $position = $i . " of " . $imagecount;
145 my $prev_text = "<a href='" . $previous . ".html'><img src='thumb/" . $previous . "' /></a>";
146 my $next_text = "<a href='" . $next . ".html'><img src='thumb/" . $next . "' /></a>";
147 my $cur_index_text = "<a href='". $indexfile ."'>" . $idx_ret_text . "</a>";
148 my $current_display = $current;
149 $current_display = "view/" . $current unless $disable_rescale;
150
151 # Check for, and load comment from FILENAME.txt here..
152 my $comment = undef;
153 if ( -f $current . ".txt" )
154 {
155 open CF, "<" . $current . ".txt";
156 while (<CF>) { $comment .= $_; }
157 close CF;
158 }
159 if ( $comment )
160 {
161 $comment = $comment_pre . $comment . $comment_post;
162 }
163
164 printf ("Processing image %s: %s\n", $position, $current);
165
166 # - - If rotated according to EXIF, do rotation
167 my $exif = get_exifdata( $current );
168 if ( $exif->{'Orientation'} =~ m/rotate/i )
169 {
170 system("jhead -autorot " . $current . ">/dev/null") unless $htmlonly;
171 }
172 my $exif_text = get_exifblock($exif) if $doexif;
173
174 ## - - Create thumbnail and normal view images, support threading
175 if ( not $htmlonly )
176 {
177 $threadcounter++;
178 my $thread = threads->create (
179 \&scale_image_t,
180 $current, $thumb_maxwidth, $thumb_maxheight,
181 $view_maxwidth, $view_maxheight, $disable_rescale);
182
183 if ( $threadcounter >= $concurrent )
184 {
185 foreach my $thr ( threads->list() ) { $thr->join(); }
186 }
187 }
188 # - - Save a reference to the "primary image"
189 if ( not -f ".indeximage" )
190 {
191 open IM, ">.indeximage";
192 print IM $current;
193 close IM;
194 }
195 # - - Create full view HTML file
196 my $cur_html;
197 open TEMPLATE, "<" . $full_tpl_file or die "UNABLE TO LOAD TEMPLATE $full_tpl_file\n";
198 while (<TEMPLATE>)
199 {
200 if ( $previous ) { $_ =~ s/%\{previous\}/$prev_text/; }
201 else { $_ =~ s/%\{previous\}//; }
202 if ( $next ) { $_ =~ s/%\{next\}/$next_text/; }
203 else { $_ =~ s/%\{next\}//; }
204 $_ =~ s/%\{index\}/$cur_index_text/;
205 $_ =~ s/%\{title\}/$title/;
206 $_ =~ s/%\{main_meta\}/$main_meta/;
207 $_ =~ s/%\{navigation_script\}/$navigation_header/;
208 $_ =~ s/%\{position\}/$position/;
209 $_ =~ s/%\{current\}/$current/;
210 $_ =~ s/%\{current_display\}/$current_display/;
211 $_ =~ s/%\{comment\}/$comment/;
212 $_ =~ s/%\{exif\}/$exif_text/;
213 $_ =~ s/%\{gallery_timestamp\}/$gentime/;
214 $_ =~ s/%\{navscript\}/$navscript/;
215 $_ =~ s/%\{footer_tag\}/$footer_tag/;
216 $cur_html .= $_;
217 }
218 close TEMPLATE;
219 open HTML, ">" . $current . ".html" or die "UNABLE TO WRITE\n";
220 print HTML $cur_html;
221 close HTML;
222
223 # - - Append image thumbnail code to current index content
224 $thumbs .= $thumb_pre . "<a href='" . $current . ".html'><img src='thumb/" . $current . "' /></a>" . $thumb_post;
225 if ( $i % ($rows*$columns) == 0 )
226 {
227 $thumbs .= "</div>";
228 # - - On each Y, terminate index file/group:
229 make_index( $index_tpl_file, $indexcount, $indexes, $thumbs, $album_comment );
230 $thumbs = "<div class='thumbnails'>";
231 $indexcount++;
232 }
233 elsif ( $i % ($columns) == 0 )
234 {
235 # - - On each X, terminate index content row
236 $thumbs .= "\n</div>\n<div class='thumbnails'>";
237 }
238 }
239 $thumbs .= "</div>";
240 make_index( $index_tpl_file, $indexcount, $indexes, $thumbs, $album_comment );
241
242 printf ("Waiting for %d threads to finish\n", $threadcounter) if $threadcounter;
243 foreach my $thr ( threads->list() ) { $thr->join(); }
244 # Done.
245
246
247 # -------------- Functions supporting above code -----------------------
248 sub make_index
249 {
250 my $tpl = shift;
251 my $idxcount = shift;
252 my $lastidx = shift;
253 my $thumbs = shift;
254 my $comment = shift;
255
256 my $gentime = localtime;
257 my $html;
258
259 my $prev_file;
260 my $prev_text;
261 my $next_file;
262 my $next_text;
263 my $indexfile;
264
265 # If index counter is 1, filename is index.html
266 $indexfile = ( $indexcount ==1 ) ? "index.html" : "index" . $idxcount . ".html";
267
268 # If index counter is > 1, add previous index link.
269 if ( $idxcount > 1 )
270 {
271 $prev_file = "index" . ($idxcount-1) . ".html";
272 $prev_file = "index.html" if ( $idxcount == 2 );
273 $prev_text = "<a href='$prev_file'>" . $idx_prev_text . "</a>";
274 }
275
276 # If current image is last in array, do not add nex-index link
277 if ( $idxcount < $lastidx )
278 {
279 $next_file = "index" . ($idxcount+1) . ".html";
280 $next_text = "<a href='" . $next_file . "'>" . $idx_next_text . "</a>";
281 }
282
283 if ( $comment )
284 {
285 $comment = $comment_pre . $comment . $comment_post;
286 }
287
288 my $position = $indexcount . " of " . $lastidx;
289 my $navscript = gen_navscript( $prev_file, $next_file );
290
291 open TEMPLATE, "<" . $tpl or die "UNABLE TO LOAD TEMPLATE\n";
292 while (<TEMPLATE>)
293 {
294 $_ =~ s/%\{previous\}/$prev_text/;
295 $_ =~ s/%\{next\}/$next_text/;
296 $_ =~ s/%\{title\}/$title/;
297 $_ =~ s/%\{comment\}/$comment/;
298 $_ =~ s/%\{position\}/$position/;
299 $_ =~ s/%\{main_meta\}/$main_meta/;
300 $_ =~ s/%\{navigation_script\}/$navigation_header/;
301 $_ =~ s/%\{thumbnails\}/$thumbs/;
302 $_ =~ s/%\{gallery_timestamp\}/$gentime/;
303 $_ =~ s/%\{navscript\}/$navscript/;
304 $_ =~ s/%\{footer_tag\}/$footer_tag/;
305 $html .= $_;
306 }
307 close TEMPLATE;
308 open HTML, ">" . $indexfile or die "UNABLE TO WRITE\n";
309 print HTML $html;
310 close HTML;
311
312 }
313
314 sub get_exifdata ($)
315 {
316 my $image = shift;
317 my $exifTool = new Image::ExifTool;
318 $exifTool->Options(Unknown => 1);
319
320 my $exif;
321 my $info = $exifTool->ImageInfo($image);
322 $exif->{'Make'} = $info->{'Make'};
323 $exif->{'Model'} = $info->{'Model'};
324 $exif->{'Orientation'} = $info->{'Orientation'};
325 $exif->{'ExposureTime'} = $info->{'ExposureTime'};
326 $exif->{'FNumber'} = $info->{'FNumber'};
327 $exif->{'ISO'} = $info->{'ISO'};
328 $exif->{'CreateDate'} = $info->{'CreateDate'};
329 $exif->{'ExposureCompensation'} = $info->{'ExposureCompensation'};
330 $exif->{'Flash'} = $info->{'Flash'};
331 $exif->{'FocalLength'} = $info->{'FocalLength'};
332 $exif->{'ExposureMode'} = $info->{'ExposureMode'};
333 $exif->{'Macro'} = $info->{'Macro'};
334 $exif->{'LensType'} = $info->{'LensType'};
335 return $exif;
336 }
337
338 sub get_exifblock
339 {
340 my $exif = shift;
341 my $exifTool = new Image::ExifTool;
342 my $block = "<table id='exifdata' cellspacing='0'>\n";
343 $block .= "<tr class='exifhead'><td>EXIF Parameter</td><td>Value</td></tr>\n";
344 my $tagcount = 0;
345 my $flipflop = 0;
346 foreach my $tag ( keys %$exif )
347 {
348 next if $tag =~ m/Orientation/;
349 my $val = $exif->{$tag};
350 next unless $val;
351 $block .= "<tr class='exiflight'>" if $flipflop;
352 $block .= "<tr class='exifdark'>" if not $flipflop;
353 $block .= "\t<td>";
354 $block .= $exifTool->GetDescription($tag);
355 $block .= "</td>\n";
356 $block .= "\t<td>";
357 $block .= $val;
358 $block .= "</td>\n";
359 $block .= "</tr>\n";
360 $flipflop = not $flipflop;
361 $tagcount++;
362 }
363 $block .= "</table>";
364 $block = undef if not $tagcount;
365 return $block;
366 }
367
368 sub gen_navscript
369 {
370 my $prev = shift;
371 my $next = shift;
372 my $index = shift;
373
374 $prev =~ s/\.html//;
375 $next =~ s/\.html//;
376
377 my $scriptbuffer = "<script type='text/javascript'>\n";
378 $scriptbuffer .= "\tnav_reg_prev('" . $prev . "');\n" if $prev;
379 $scriptbuffer .= "\tnav_reg_next('" . $next . "');\n" if $next;
380 if ( $up_arrow_navigate == 1 )
381 {
382 $scriptbuffer .= "\tnav_reg_index('" . $index . "');\n" if $index;
383 }
384 $scriptbuffer .= "\tnav_reg_onkeypress();\n";
385 $scriptbuffer .= "</script>\n";
386 return $scriptbuffer;
387 }
388
389 sub scale_image_t
390 {
391 my $current = shift;
392 my $thumb_maxwidth = shift;
393 my $thumb_maxheight = shift;
394 my $view_maxwidth = shift;
395 my $view_maxheight = shift;
396 my $disable_rescale = shift;
397
398 my $tgeom = $thumb_maxwidth . "x" . $thumb_maxheight;
399 my $vgeom = $view_maxwidth . "x" . $view_maxheight;
400 # - - Create thumbnail image (resize to new image)
401 system("convert " . $current . " -geometry '" . $tgeom . ">' thumb/" . $current);
402 # - - Create normal display image (resize to new image)
403 system("convert " . $current . " -geometry '" . $vgeom . ">' view/" . $current) unless $disable_rescale;
404 $threadcounter--;
405
406 }
407
408 sub get_config
409 {
410 # My standard way of implementing AppConfig use ...
411 my $filename = shift;
412 if ( not $filename )
413 {
414 my @files = ( "./plsgen.cfg", "../plsgen.cfg", "/etc/plsgen.cfg" );
415 foreach my $file ( @files ) {
416 $filename = $file;
417 last if ( -f $filename );
418 }
419 }
420
421 my $cfg = AppConfig->new(
422 {
423 CASE => 1,
424 ERROR => \&cfg_error,
425 GLOBAL => {
426 DEFAULT => "<unset>",
427 ARGCOUNT => AppConfig::ARGCOUNT_ONE
428 },
429 }
430
431 );
432 $cfg->define('full_tpl_file');
433 $cfg->full_tpl_file("/usr/local/share/plsgen/full.tpl");
434 $cfg->define('index_tpl_file');
435 $cfg->index_tpl_file("/usr/local/share/plsgen/index.tpl");
436
437 $cfg->define('css_file');
438 $cfg->css_file("../style.css");
439 $cfg->define('navigation_script');
440 $cfg->navigation_script("../nav.js");
441
442 $cfg->define('up_arrow_navigate');
443 $cfg->up_arrow_navigate(1);
444
445 $cfg->define('columns');
446 $cfg->columns(4);
447 $cfg->define('rows');
448 $cfg->rows(3);
449
450 $cfg->define('concurrent_threads');
451 $cfg->concurrent_threads(1);
452
453 $cfg->define('thumb_maxwidth');
454 $cfg->thumb_maxwidth(160);
455 $cfg->define('thumb_maxheight');
456 $cfg->thumb_maxheight(120);
457 $cfg->define('view_maxwidth');
458 $cfg->view_maxwidth(800);
459 $cfg->define('view_maxheight');
460 $cfg->view_maxwidth(600);
461 $cfg->define('disable_rescale');
462 $cfg->disable_rescale(0);
463
464 $cfg->define('thumb_pre');
465 $cfg->thumb_pre("<div class='thumb'>");
466 $cfg->define('thumb_post');
467 $cfg->thumb_post("</div>");
468
469 $cfg->define('comment_pre');
470 $cfg->comment_pre("<div id='comment'>");
471 $cfg->define('comment_post');
472 $cfg->thumb_post("</div>");
473
474 $cfg->define('idx_prev_text');
475 $cfg->idx_prev_text("&larr; Back");
476 $cfg->define('idx_next_text');
477 $cfg->idx_next_text("Next &rarr;");
478 $cfg->define('idx_ret_text');
479 $cfg->idx_ret_text("To index");
480
481 $cfg->define('footer_tag');
482 $cfg->footer_tag('plsgen : Perl Simple Gallery Generator ' . $version);
483
484 $cfg->file($filename) if -f $filename;
485 return $cfg;
486 }
487
488 sub cfg_error {
489 die "Error occured in config-handling: $_\n";
490 }
491