a2605f73c4cfd559f76a87924aeabcd852cfcbd6
[booh] / bin / booh-backend
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2005 Guillaume Cottenceau <gc3 at bluewin.ch>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 require 'getoptlong'
23 require 'gettext'
24 include GetText
25 require 'rexml/document'
26 include REXML
27
28 require 'booh/booh-lib'
29 require 'booh/html-merges'
30
31 #- bind text domain as soon as possible because some _() functions are called early to build data structures
32 bindtextdomain("booh")
33
34 #- options
35 $options = [
36     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
37     [ '--version',       '-V', GetoptLong::NO_ARGUMENT,       _("Print version and exit") ],
38
39     [ '--no-check',      '-n', GetoptLong::NO_ARGUMENT,       _("Don't check for needed external programs at startup") ],
40
41     [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original images/videos as files or subdirs") ],
42     [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album") ],
43 #    [ '--clean',         '-c', GetoptLong::NO_ARGUMENT,       _("Clean destination directory") ],
44
45     [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
46     [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
47     [ '--config-skel',   '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
48     [ '--merge-config',  '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new images/videos from --source, and change theme info") ],
49     [ '--merge-config-onedir',  '-O', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the subdir specified with --dir") ],
50     [ '--dir',           '-D', GetoptLong::REQUIRED_ARGUMENT, _("Directory for merge with --merge-config-onedir") ],
51     [ '--use-config',    '-u', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to change theme info") ],
52     [ '--force',         '-f', GetoptLong::NO_ARGUMENT,       _("Force generation of album even if the GUI marked some directories as already generated") ],
53
54     [ '--sizes',         '-S', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of images sizes to use instead of all specified in the theme (this is a comma-separated list)") ],
55     [ '--thumbnails-per-row', '-T', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per row in the thumbnails page (if applicable in theme)") ],
56     [ '--optimize-for-32', '-o', GetoptLong::NO_ARGUMENT,       _("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)") ],
57     [ '--empty-comments','-e', GetoptLong::NO_ARGUMENT,       _("Prefer empty comments over filename when creating new albums") ],
58
59     [ '--mproc',         '-m', GetoptLong::REQUIRED_ARGUMENT, _("Specify the number of processors for multi-processors machines") ],
60
61     [ '--for-gui',       '-g', GetoptLong::NO_ARGUMENT,       _("Do the minimum work to be able to see the album under the GUI (don't generate all thumbnails)") ],
62
63     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
64     [ '--info-pipe',     '-i', GetoptLong::REQUIRED_ARGUMENT, _("Name a file where to write information about what's going on (used by the GUI)") ],
65 ]
66
67 #- default values for some globals 
68 $switches = []
69 $stdout.sync = true
70
71 def usage
72     puts _("Usage: %s [OPTION]...") % File.basename($0)
73     $options.each { |ary|
74         printf " %3s, %-18s %s\n", ary[1], ary[0], ary[3]
75     }
76 end
77
78 def handle_options
79     parser = GetoptLong.new
80     parser.set_options(*$options.collect { |ary| ary[0..2] })
81     begin
82         parser.each_option do |name, arg|
83             case name
84             when '--help'
85                 usage
86                 exit(0)
87
88             when '--version'
89                 puts _("Booh version %s
90
91 Copyright (c) 2005 Guillaume Cottenceau.
92 This is free software; see the source for copying conditions.  There is NO
93 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
94
95                 exit(0)
96
97             when '--no-check'
98                 $no_check = true
99
100             when '--source'
101                 $source = File.expand_path(arg.sub(%r|/$|, ''))
102                 if !File.directory?($source)
103                     die _("Argument to --source must be a directory")
104                 end
105             when '--destination'
106                 $dest = File.expand_path(arg.sub(%r|/$|, ''))
107                 if File.exists?($dest) && !File.directory?($dest)
108                     die _("If --destination exists, it must be a directory")
109                 end
110                 if $dest != make_dest_filename($dest)
111                     die _("Sorry, destination directory can't contain non simple alphanumeric characters.")
112                 end
113 #            when '--clean'
114 #                system("rm -rf #{$dest}")
115
116             when '--theme'
117                 $theme = arg
118             when '--config'
119                 arg = File.expand_path(arg)
120                 if File.readable?(arg)
121                     $xmldoc = REXML::Document.new File.new(arg)
122                     $mode = 'use_config'
123                 else
124                     die _('Config file does not exist or is unreadable.')
125                 end
126             when '--config-skel'
127                 arg = File.expand_path(arg)
128                 if File.exists?(arg)
129                     msg 1, _("Config skeleton file already exists, backuping to %s.backup") % arg
130                     File.rename(arg, "#{arg}.backup")
131                 end
132                 $config_writeto = arg
133                 $mode = 'gen_config'
134             when '--merge-config'
135                 arg = File.expand_path(arg)
136                 if File.readable?(arg)
137                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
138                     $xmldoc = REXML::Document.new File.new(arg)
139                     File.rename(arg, "#{arg}.backup")
140                     $config_writeto = arg
141                     $mode = 'merge_config'
142                 else
143                     die _('Config file does not exist or is unreadable.')
144                 end
145             when '--merge-config-onedir'
146                 arg = File.expand_path(arg)
147                 if File.readable?(arg)
148                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
149                     $xmldoc = REXML::Document.new File.new(arg)
150                     File.rename(arg, "#{arg}.backup")
151                     $config_writeto = arg
152                     $mode = 'merge_config_onedir'
153                 else
154                     die _('Config file does not exist or is unreadable.')
155                 end
156             when '--dir'
157                 arg = File.expand_path(arg)
158                 if !File.readable?(arg)
159                     die _('Specified directory to merge with --dir is not readable')
160                 else
161                     $onedir = arg
162                 end
163             when '--use-config'
164                 arg = File.expand_path(arg)
165                 if File.readable?(arg)
166                     msg 2, _("Use config notice: backuping current config file to %s.backup") % arg
167                     $xmldoc = REXML::Document.new File.new(arg)
168                     File.rename(arg, "#{arg}.backup")
169                     $config_writeto = arg
170                     $mode = 'use_config_changetheme'
171                 else
172                     die _('Config file does not exist or is unreadable.')
173                 end
174
175             when '--sizes'
176                 $limit_sizes = arg
177
178             when '--thumbnails-per-row'
179                 $N_per_row = arg
180
181             when '--optimize-for-32'
182                 $optimize_for_32 = true
183
184             when '--empty-comments'
185                 $empty_comments = true
186
187             when '--force'
188                 $force = true
189
190             when '--mproc'
191                 $mproc = arg.to_i
192                 $pids = []
193
194             when '--for-gui'
195                 $forgui = true
196
197             when '--verbose-level'
198                 $verbose_level = arg.to_i
199
200             when '--info-pipe'
201                 $info_pipe = File.open(arg, File::WRONLY)
202                 $info_pipe.sync = true
203             end
204         end
205     rescue
206         puts $!
207         usage
208         exit(1)
209     end
210
211     if !$source && $xmldoc
212         $source = from_utf8($xmldoc.root.attributes['source'])
213         $dest = from_utf8($xmldoc.root.attributes['destination'])
214         $theme ||= $xmldoc.root.attributes['theme']
215         $limit_sizes ||= $xmldoc.root.attributes['limit-sizes']
216         if $mode == 'use_config' || $mode =~ /^merge_config/
217             $optimize_for_32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
218             $N_per_row = $xmldoc.root.attributes['thumbnails-per-row']
219         end
220     end
221
222     if $mode == 'merge_config_onedir' && !$onedir
223         die _("Missing --dir for --merge_config_onedir")
224     end
225
226     if !$source
227         usage
228         exit(0)
229     end
230     if !$dest
231         die _("Missing --destination parameter.")
232     end
233     if !$theme
234         $theme = 'simple'
235     end
236
237     select_theme($theme, $limit_sizes, $optimize_for_32, $N_per_row)
238
239     if !$xmldoc
240         additional_params = ''
241         if $limit_sizes
242             additional_params += "limit-sizes='#{$limit_sizes}'"
243         end
244         if $optimize_for_32
245             additional_params += " optimize-for-32='true'"
246         end
247         if $N_per_row
248             additional_params += " thumbnails-per-row='#{$N_per_row}'"
249         end
250         $xmldoc = Document.new "<booh version='#{$VERSION}' source='#{utf8($source)}' destination='#{utf8($dest)}' theme='#{$theme}' #{additional_params}/>"
251         $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
252         $mode = 'gen_config'
253     end
254
255     if $mode == 'merge_config' || $mode == 'use_config_changetheme'
256         $xmldoc.root.add_attribute('theme', $theme)
257         $xmldoc.root.add_attribute('version', $VERSION)
258         if $limit_sizes
259             $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
260         else
261             $xmldoc.root.delete_attribute('limit-sizes')
262         end
263         if $optimize_for_32
264             $xmldoc.root.add_attribute('optimize-for-32', 'true')
265         else
266             $xmldoc.root.delete_attribute('optimize-for-32')
267         end
268         if $N_per_row
269             $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
270         else
271             $xmldoc.root.delete_attribute('thumbnails-per-row')
272         end
273     end
274 end
275
276 def read_config
277     $config = {}
278 end
279
280 def write_config
281 end
282
283 def info(value)
284     if $info_pipe
285         $info_pipe.puts(value)
286     end
287 end
288
289 def check_installation
290     if $no_check
291         return
292     end
293     missing = %w(convert identify exif transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
294     if missing != []
295         die _("The following programs are typically needed: `%s'. Re-run with --no-check if you're sure you're fine without them.") % missing.join(', ')
296     end
297 end
298
299 def replace_line(surround, keyword, line)
300     begin
301         contents = eval "$#{keyword}"
302         line.sub!(/#{surround}#{keyword}#{surround}/, contents)
303     rescue TypeError
304         die _("No `%s' found for substitution") % keyword
305     end
306 end
307
308 def build_html_skeletons
309     $html_images     = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
310     $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
311     $html_index      = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
312     for line in $html_images + $html_thumbnails + $html_index
313         while line =~ /~~~(\w+)~~~/
314             replace_line('~~~', $1, line)
315         end
316     end
317 end
318
319 def find_caption_value(xmldir, filename)
320     if cap = xmldir.elements["[@filename='#{utf8(filename)}']"].attributes['caption']
321         return cap.gsub("\n", '<br/>')
322     else
323         return nil
324     end
325 end
326
327 def find_captions(xmldir, images)
328     return images.collect { |img| find_caption_value(xmldir, img) }
329 end
330
331 #- stolen from CVSspam
332 def urlencode(text)
333   text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
334     "%#{sprintf('%2X', $&[0])}"
335   end
336 end
337
338 def all_images_sizes
339     return $limit_sizes =~ /original/ ? $images_size + [ { 'name' => 'original' } ] : $images_size
340 end
341
342 def html_reload_to_thumbnails
343     html_reload_to_thumbnails = $preferred_size_reloader.clone
344     html_reload_to_thumbnails.gsub!(/~~theme~~/, $theme)
345     html_reload_to_thumbnails.gsub!(/~~default_size~~/, $default_size['name'])
346     html_reload_to_thumbnails.gsub!(/~~all_sizes~~/, all_images_sizes.collect { |s| "\"#{size2js(s['name'])}\"" }.join(', '))
347     return html_reload_to_thumbnails
348 end
349
350 def discover_iterations(iterations, line)
351     if line =~ /~~iterate(\d)_open(_max(\d+|N))?~~/
352         for iter in iterations.values
353             if iter['open']
354                 iter['open'] = false
355                 iter['close_wait'] = $1.to_i
356             end
357         end
358         max = $3 == 'N' ? ($N_per_row || $default_N) : $3
359         iterations[$1.to_i] = { 'open' => true, 'max' => max, 'opening' => '', 'closing' => '' }
360         if $1.to_i == 1
361             line.sub!(/.*/, '~~thumbnails~~')
362         else
363             line.sub!(/.*/, '')
364         end
365     elsif line =~ /~~iterate(\d)_close~~/
366         iterations[$1.to_i]['open']  = false;
367         iterations[$1.to_i]['close'] = true;
368         line.sub!(/.*/, '')
369     else
370         for iter in iterations.values
371             if iter['open']
372                 iter['opening'] += line
373                 line.sub!(/.*/, '')
374             end
375             if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
376                 iter['closing'] += line
377                 line.sub!(/.*/, '')
378             end
379         end
380     end
381 end
382
383 def reset_iterations(iterations)
384     for iter in iterations.values
385         iter['value'] = 1
386     end
387 end
388
389 def run_iterations(iterations)
390     html = ''
391     for level in iterations.keys.sort
392         if iterations[level]['value'] == 1 || level == iterations.keys.max
393             html += iterations[level]['opening']
394         end
395         iterations[level]['value'] += 1
396         if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
397             iterations[level]['value'] = 1
398             iterations[level-1]['value'] = 1
399             html += iterations[level-1]['closing']
400         end
401     end
402     return html
403 end
404
405 def close_iterations(iterations)
406     html = ''
407     for level in iterations.keys.sort.reverse
408         html += iterations[level]['closing']
409     end
410     return html
411 end
412
413 def img_element(fullpath)
414     if size = get_image_size(fullpath)
415         sizespec = 'width="' + size[:x].to_s + '" height="' + size[:y].to_s + '"'
416     else
417         sizespec = ''
418     end
419     return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' class="image"/>'
420 end
421
422 def size2js(name)
423     return name.gsub(/-/, '')
424 end
425
426 def substitute_html_sizes(html, sizeobj, type)
427     sizestrings = []
428     if $images_size.length > 1 || (type == 'image' && $limit_sizes =~ /original/)
429         for sizeobj2 in $images_size
430             if sizeobj != sizeobj2
431                 if type == 'thumbnails'
432                     sizestrings << '<a href="thumbnails-' + size2js(sizeobj2['name']) + '.html">' + sizename(sizeobj2['name']) + '</a>'
433                 else
434                     sizestrings << '<a id="link' + size2js(sizeobj2['name']) + '">' + sizename(sizeobj2['name']) + '</a>'
435                 end
436             else
437                 sizestrings << sizename(sizeobj2['name'])
438             end
439         end
440         if type == 'image' && $limit_sizes =~ /original/
441             sizestrings << '<a id="linkoriginal" target="newframe">' + sizename('original') + '</a>'
442         end
443     end
444     html.sub!(/~~sizes~~(.+)~~/) { sizestrings.join($1) }
445 end
446
447 def xmldir2destdir(xmldir)
448     return make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))
449 end
450
451 def find_previous_album(xmldir)
452     relative_pos = ''
453     begin
454         #- move to previous dir element if exists
455         if prevelem = xmldir.previous_element_byname_notattr('dir', 'deleted')
456             xmldir = prevelem
457             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
458             child = nil
459             #- after having moved to previous dir, we need to go down last subdir until the last one
460             while child = xmldir.elements['dir']
461                 while nextchild = child.next_element_byname_notattr('dir', 'deleted')
462                     child = nextchild
463                 end
464                 relative_pos += xmldir2destdir(child) + '/'
465                 xmldir = child
466             end
467         else
468             #- previous dir doesn't exist, move to previous dir element if exists
469             xmldir = xmldir.parent
470             if xmldir.name == 'dir' && !xmldir.attributes['deleted']
471                 relative_pos += '../'
472             else
473                 return nil
474             end
475         end
476     end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
477     return File.reduce_path(relative_pos)
478 end
479
480 def find_next_album(xmldir)
481     relative_pos = ''
482     begin
483         #- first child dir element (catches when initial xmldir has both thumbnails and subdirs)
484         if firstchild = xmldir.child_byname_notattr('dir', 'deleted')
485             xmldir = firstchild
486             relative_pos += xmldir2destdir(xmldir) + '/'
487         #- next brother
488         elsif nextbro = xmldir.next_element_byname_notattr('dir', 'deleted')
489             xmldir = nextbro
490             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
491         else
492             #- go up until we have a next brother or we are finished
493             begin
494                 xmldir = xmldir.parent
495                 relative_pos += '../'
496             end while xmldir && !xmldir.next_element_byname_notattr('dir', 'deleted')
497             if xmldir
498                 xmldir = xmldir.next_element_byname('dir')
499                 relative_pos += '../' + xmldir2destdir(xmldir) + '/'
500             else
501                 return nil
502             end
503         end
504     end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
505     return File.reduce_path(relative_pos)
506 end
507
508 def sub_previous_next_album(previous_album, next_album, html)
509     if previous_album
510         html.gsub!(/~~previous_album~~/, '<a href="' + previous_album + 'thumbnails.html">' + utf8(_('previous album')) + '</a>')
511         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/) { $1 }
512     else
513         html.gsub!(/~~previous_album~~/, '')
514         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/, '')
515     end
516     if next_album
517         html.gsub!(/~~next_album~~/, '<a href="' + next_album + 'thumbnails.html">' + utf8(_('next album')) + '</a>')
518         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/) { $1 }
519     else
520         html.gsub!(/~~next_album~~/, '')
521         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/, '')
522     end
523     return html
524 end
525
526 def walk_source_dir
527
528     #- preprocess the path->dir, rexml is very slow with that; we seem to improve speed by 7%
529     optxpath = {}
530     $xmldoc.elements.each('//dir') { |elem|
531         optxpath[elem.attributes['path']] = elem
532     }
533
534     examined_dirs = $mode == 'merge_config_onedir' ? [ $onedir ] : `find '#{$source}' -type d -follow`.sort
535     info("directories: #{examined_dirs.length}, sizes: #{$images_size.length}")
536
537     examined_dirs.each { |dir|
538         dir.chomp!
539         if File.basename(dir) =~ /^\./
540             msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
541             next
542         end
543
544         if dir =~ /'/
545             die _("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir
546         end
547
548         #- place xml document on proper node if exists, else create
549         xmldir = optxpath[utf8(dir)]
550         if $mode == 'use_config' || $mode == 'use_config_changetheme'
551             if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
552                 info("walking: #{dir}|#{$source}, 0 elements")
553                 next
554             end
555         else
556             if $mode == 'gen_config' || ($mode == 'merge_config' && !xmldir)
557                 #- add the <dir..> element if necessary
558                 parent = File.dirname(dir)
559                 xmldir = $xmldoc.elements["//dir[@path='#{utf8(parent)}']"]
560                 if !xmldir
561                     xmldir = $xmldoc.root
562                 end
563                 xmldir = optxpath[utf8(dir)] = xmldir.add_element('dir', { 'path' => utf8(dir) })
564             end
565         end
566         xmldir.delete_attribute('already-generated')
567
568         #- read images/videos entries from config or from directories depending on mode
569         entries = []
570         if $mode == 'use_config' || $mode == 'use_config_changetheme'
571             msg 2, _("Handling %s from config list...") % dir
572             xmldir.elements.each { |element|
573                 if %w(image video).include?(element.name) && !element.attributes['deleted']
574                     entries << from_utf8(element.attributes['filename'])
575                 end
576             }
577         else
578             msg 2, _("Examining %s...") % dir
579             entries = Dir.entries(dir).sort
580             #- populate config in case of gen_config, add new files in case of merge_config
581             for file in entries
582                 if file =~ /['"\[\]]/
583                     msg 1, _("Ignoring %s, contains one of forbidden characters: '\"[]") % "#{dir}/#{file}"
584                 else
585                     type = entry2type(file)
586                     if type && !xmldir.elements["#{type}[@filename='#{utf8(file)}']"]
587                         xmldir.add_element type, { "filename" => utf8(file), "caption" => $empty_comments ? '' : utf8(file.sub(/\.[^\.]+$/, '')[0..17]) }
588                     end
589                 end
590             end
591             if $mode != 'gen_config'
592                 #- cleanup removed files from config and reread entries from config to get proper ordering
593                 entries = []
594                 xmldir.elements.each { |element|
595                     fullpath = "#{dir}/#{from_utf8(element.attributes['filename'])}"
596                     if %w(image video).include?(element.name)
597                         if !File.readable?(fullpath)
598                             msg 1, _("Config merge: removing %s from config; use the backup file to retrieve caption info if this was a mistake") % fullpath
599                             xmldir.delete(element)
600                         elsif !element.attributes['deleted']
601                             entries << from_utf8(element.attributes['filename'])
602                         end
603                     end
604                 }
605                 #- if there is no more elements here, there is no album here anymore
606                 if !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
607                     xmldir.delete_attribute('thumbnails-caption')
608                     xmldir.delete_attribute('thumbnails-captionfile')
609                 end
610             end
611         end
612         images = entries.find_all { |e| entry2type(e) == 'image' }
613         msg 3, _("\t%s images") % images.length
614         videos = entries.find_all { |e| entry2type(e) == 'video' }
615         msg 3, _("\t%s videos") % videos.length
616         info("walking: #{dir}|#{$source}, #{images.length + videos.length} elements")
617
618         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
619         system("mkdir -p '#{dest_dir}'")
620
621         #- pass through if there are no images and videos
622         if images.size == 0 && videos.size == 0
623             if !$forgui
624                 #- cleanup old images/videos, especially if this directory contained images/videos previously.
625                 themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
626                                 find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(e) }
627                 if $mode != 'gen_config'
628                     rightful_images = []
629                     if xmldir.attributes['thumbnails-caption']
630                         rightful_images << 'thumbnails-thumbnail.jpg'
631                     end
632                     xmldir.elements.each('dir') { |child|
633                         if child.attributes['deleted']
634                             next
635                         end
636                         subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
637                         rightful_images << "thumbnails-#{subdir}.jpg"
638                     }
639                     to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?("#{dest_dir}/#{e}") && !rightful_images.include?(e) } - themestuff
640                     if to_del.size > 0
641                         system("rm -f " + to_del.collect { |e| "#{dest_dir}/#{e}" }.join(' '))
642                     end
643                 end
644                 
645                 #- copy any resource file that goes with the theme (css, images..)
646                 themestuff.each { |entry|
647                     if !File.exists?("#{dest_dir}/#{entry}")
648                         psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
649                     end
650                 }
651             end
652             next
653         end
654
655         msg 2, _("Outputting in %s...") % dest_dir
656
657         #- populate data structure with sizes from theme
658         for sizeobj in $images_size
659             fullscreen_images ||= {}
660             fullscreen_images[sizeobj['name']] = []
661             thumbnail_images ||= {}
662             thumbnail_images[sizeobj['name']] = []
663             thumbnail_videos ||= {}
664             thumbnail_videos[sizeobj['name']] = []
665         end
666         if $limit_sizes =~ /original/
667             fullscreen_images['original'] = []
668         end
669
670         images.size >= 1 and msg 3, _("\tcreating images thumbnails...")
671
672         #- create thumbnails for images
673         images.each { |img|
674             info("processing element")
675             base_dest_img = dest_dir + '/' + make_dest_filename(img.sub(/\.[^\.]+$/, ''))
676             if $forgui
677                 thumbnail_dest_img = base_dest_img + "-#{$default_size['thumbnails']}.jpg"
678                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
679             else
680                 todo = []
681                 for sizeobj in $images_size
682                     size_fullscreen = sizeobj['fullscreen']
683                     size_thumbnails = sizeobj['thumbnails']
684                     fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
685                     thumbnail_dest_img  = base_dest_img + "-#{size_thumbnails}.jpg"
686                     fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
687                     thumbnail_images[sizeobj['name']]  << File.basename(thumbnail_dest_img)
688                     todo << { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen }
689                     todo << { 'filename' => thumbnail_dest_img,  'size' => size_thumbnails }
690                 end
691                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, todo)
692                 if $limit_sizes =~ /original/
693                     fullscreen_images['original'] << img
694                 end
695                 destimg = "#{dest_dir}/#{img}"
696                 if $limit_sizes =~ /original/ && !File.exists?(destimg)
697                     psys("cp '#{dir}/#{img}' '#{destimg}'")
698                 end
699             end
700         }
701
702         videos.size >= 1 and msg 3, _("\tcreating videos thumbnails...")
703
704         #- create thumbnails for videos
705         videos.each { |video|
706             info("processing element")
707             if $forgui
708                 thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{$default_size['thumbnails']}.jpg"
709                 gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
710             else
711                 todo = []
712                 for sizeobj in $images_size
713                     size_thumbnails = sizeobj['thumbnails']
714                     thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{size_thumbnails}.jpg"
715                     thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
716                     todo << { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails }
717                 end
718                 gen_thumbnails_element("#{dir}/#{video}", xmldir, true, todo)
719             end
720             destvideo = "#{dest_dir}/#{video}"
721             if !File.exists?(destvideo)
722                 psys("cp '#{dir}/#{video}' '#{destvideo}'")
723             end
724         }
725
726         if !$forgui
727             themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
728                              find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(e) }
729
730             #- cleanup old images/videos (for when removing elements or sizes)
731             all_elements = fullscreen_images.collect { |e| e[1] }.flatten.
732                      concat(thumbnail_images.collect { |e| e[1] }.flatten).
733                      concat(thumbnail_videos.collect { |e| e[1] }.flatten).
734                      concat(videos)
735             to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?("#{dest_dir}/#{e}") && !all_elements.include?(e) && e !~ /^thumbnails-\w+\.jpg/ } - themestuff
736             if to_del.size > 0
737                 system("rm -f " + to_del.collect { |e| "#{dest_dir}/#{e}" }.join(' '))
738             end
739
740             #- copy any resource file that goes with the theme (css, images..)
741             themestuff.each { |entry|
742                 if !File.exists?("#{dest_dir}/#{entry}")
743                     psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
744                 end
745             }
746
747             msg 3, _("\tgenerating HTML pages...")
748
749             #- generate thumbnails.html (page with thumbnails)
750             for sizeobj in $images_size
751                 info("processing size")
752                 html = $html_thumbnails.collect { |l| l.clone }
753                 iterations = {}
754                 for i in html
755                     i.sub!(/~~run_slideshow~~/, images.size <= 1 ? '' : '<a href="image-' + size2js(sizeobj['name']) + '.html#run_slideshow=1">' + utf8(_('Run slideshow!'))+'</a>')
756                     i.sub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
757                     substitute_html_sizes(i, sizeobj, 'thumbnails')
758                     discover_iterations(iterations, i)
759                 end
760                 html_thumbnails = ''
761                 reset_iterations(iterations)
762                 for file in entries
763                     type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
764                     if type
765                         html_thumbnails += run_iterations(iterations)
766                         if type == 'image'
767                             index = images.index(file)
768                             html_thumbnails.gsub!(/~~image_iteration~~/,
769                                                   '<a href="image-' + size2js(sizeobj['name']) + '.html#current=' + fullscreen_images[sizeobj['name']][index] +
770                                                       '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
771                                                       img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
772                             html_thumbnails.gsub!(/~~caption_iteration~~/,
773                                                   find_caption_value(xmldir, images[index]) || utf8(images[index]))
774                             html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
775                             html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
776                         elsif type == 'video'
777                             index = videos.index(file)
778                             if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
779                                 html_thumbnails.gsub!(/~~image_iteration~~/,
780                                                       '<a href="' + videos[index] + '">' + img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
781                             else
782                                 html_thumbnails.gsub!(/~~image_iteration~~/,
783                                                       '<a href="' + videos[index] + '">' + utf8(_("(no preview)")) + '</a>')
784                             end
785                             html_thumbnails.gsub!(/~~caption_iteration~~/,
786                                                   find_caption_value(xmldir, videos[index]) || utf8(videos[index]))
787                             html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
788                             html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
789                         end
790                     end
791                 end
792                 html_thumbnails += close_iterations(iterations)
793                 for i in html
794                     i.sub!(/~~thumbnails~~/, html_thumbnails)
795                     i.gsub!(/~~theme~~/, $theme)
796                     i.gsub!(/~~current_size~~/, sizeobj['name'])
797                     i.gsub!(/~~current_size_js~~/, size2js(sizeobj['name']))
798                 end
799                 ios = File.open("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}.html", "w")
800                 ios.write(html)
801                 ios.close
802             end
803
804             info("finished processing sizes")
805
806             #- generate "main" thumbnails.html page that will reload to correct size thanks to cookie
807             ios = File.open("#{dest_dir}/thumbnails.html", "w")
808             ios.write(html_reload_to_thumbnails)
809             ios.close
810
811             #- generate image.html (page with fullscreen images)
812             if images.size > 0
813                 captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\"') + '"' : '""' }.join(', ')
814
815                 for sizeobj in $images_size
816                     html = $html_images.collect { |l| l.clone }
817                     images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
818                     otherimages4js = ''
819                     othersizes = []
820                     for sizeobj2 in all_images_sizes
821                         if sizeobj != sizeobj2
822                             otherimages4js += "var images_#{size2js(sizeobj2['name'])} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
823                             othersizes << "\"#{size2js(sizeobj2['name'])}\""
824                         end
825                     end
826                     for i in html
827                         i.gsub!(/~~images~~/, images4js)
828                         i.gsub!(/~~other_images~~/, otherimages4js)
829                         i.gsub!(/~~other_sizes~~/, othersizes.join(', '))
830                         i.gsub!(/~~captions~~/, captions4js)
831                         i.gsub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
832                         i.gsub!(/~~thumbnails~~/, '<a href="thumbnails-' + size2js(sizeobj['name']) + '.html" id="thumbnails">' + utf8(_('return to thumbnails')) + '</a>')
833                         i.gsub!(/~~theme~~/, $theme)
834                         i.gsub!(/~~current_size~~/, size2js(sizeobj['name']))
835                         substitute_html_sizes(i, sizeobj, 'image')
836                     end
837                     ios = File.open("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html", "w")
838                     ios.write(html)
839                     ios.close
840                 end
841             end
842         end
843     }
844
845     msg 3, ''
846
847     #- add attributes to <dir..> elements needing so
848     if $mode != 'use_config'
849         msg 3, _("\tfixating configuration file...")
850         $xmldoc.elements.each('//dir') { |element|
851             path = captionpath = element.attributes['path']
852             descendant_element = element.elements['descendant::image'] || element.elements['descendant::video']
853             if !descendant_element
854                 msg 3, _("\t\tremoving %s, no element in it") % path
855                 element.remove  #- means we have a directory with nothing interesting in it
856             else
857                 captionfile = "#{descendant_element.parent.attributes['path']}/#{descendant_element.attributes['filename']}"
858                 basename = File.basename(path)
859                 if element.elements['dir']
860                     if !element.attributes['subdirs-caption']
861                         element.add_attribute('subdirs-caption', basename)
862                     end
863                     if !element.attributes['subdirs-captionfile']
864                         element.add_attribute('subdirs-captionfile', captionfile)
865                     end
866                 end
867                 if element.child_byname_notattr('image', 'deleted') || element.child_byname_notattr('video', 'deleted')
868                     if !element.attributes['thumbnails-caption']
869                         element.add_attribute('thumbnails-caption', basename)
870                     end
871                     if !element.attributes['thumbnails-captionfile']
872                         element.add_attribute('thumbnails-captionfile', captionfile)
873                     end
874                 end
875             end
876         }
877     end
878
879     #- write down to disk config if necessary
880     if $config_writeto
881         ios = File.open($config_writeto, "w")
882         $xmldoc.write(ios, 0)
883         ios.close
884     end
885
886     if $forgui
887         msg 3, _(" completed necessary stuff for GUI, exiting.")
888         return
889     end
890
891     #- second pass to create index.html files and previous/next links
892     info("creating index.html")
893     msg 3, _("\trescanning directories to generate all `index.html' files...")
894
895     #- recompute the memoization because elements mights have been removed (the ones with no element in them)
896     optxpath = {}
897     $xmldoc.elements.each('//dir') { |elem|
898         optxpath[elem.attributes['path']] = elem
899     }
900
901     examined_dirs.each { |dir|
902         dir.chomp!
903         info("index.html: #{dir}|#{$source}")
904
905         xmldir = optxpath[utf8(dir)]
906         if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
907             next
908         end
909         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
910
911         previous_album = find_previous_album(xmldir)
912         next_album = find_next_album(xmldir)
913
914         if xmldir.elements['dir']
915             html = $html_index.collect { |l| l.clone }
916             iterations = {}
917             for i in html
918                 caption = xmldir.attributes['subdirs-caption']
919                 i.gsub!(/~~title~~/, caption)
920                 if xmldir.parent.name == 'dir'
921                     nav = ''
922                     path = '..'
923                     parent = xmldir.parent
924                     while parent.name == 'dir'
925                         parentcaption = parent.attributes['subdirs-caption']
926                         nav = "<a href='#{path}/index.html'>#{parentcaption}</a> #{utf8(_(" > "))} #{nav}"
927                         path += '/..'
928                         parent = parent.parent
929                     end
930                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
931                     i.gsub!(/~~navigation~~/, nav + caption)
932                 else
933                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
934                 end
935                 discover_iterations(iterations, i)
936             end
937             
938             html_index = ''
939             reset_iterations(iterations)
940             
941             #- deal with "current" album (directs to "thumbnails" page)
942             if xmldir.attributes['thumbnails-caption']
943                 thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
944                 gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
945                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
946                 html_index += run_iterations(iterations)
947                 html_index.gsub!(/~~image_iteration~~/, "<a href='thumbnails.html'>" + img_element(thumbnail) + '</a>')
948                 html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
949             end
950
951             #- deal with sub-albums (direct to subdirs/index.html pages)
952             xmldir.elements.each('dir') { |child|
953                 if child.attributes['deleted']
954                     next
955                 end
956                 subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
957                 thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
958                 html_index += run_iterations(iterations)
959                 captionfile, caption = find_subalbum_caption_info(child)
960                 gen_thumbnails_subdir(captionfile, child, false,
961                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
962                 html_index.gsub!(/~~caption_iteration~~/, caption)
963                 html_index.gsub!(/~~image_iteration~~/, "<a href='#{make_dest_filename(subdir)}/index.html'>" + img_element(thumbnail) + '</a>')
964             }
965
966             html_index += close_iterations(iterations)
967
968             for i in html
969                 i.gsub!(/~~thumbnails~~/, html_index)
970             end
971             
972         else
973             html = html_reload_to_thumbnails
974         end
975
976         ios = File.open("#{dest_dir}/index.html", "w")
977         ios.write(html)
978         ios.close
979
980         #- substitute "return to albums" and previous/next correctly
981         if xmldir.child_byname_notattr('image', 'deleted') || xmldir.child_byname_notattr('video', 'deleted')
982             for sizeobj in $images_size
983                 substInFile("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}.html") { |line|
984                     sub_previous_next_album(previous_album, next_album, line)
985                     if xmldir.elements['dir']
986                         line.sub!(/~~return_to_albums~~/, '<a href="index.html">' + utf8(_('return to albums')) + '</a>')
987                     else
988                         if xmldir.parent.name == 'dir'
989                             line.sub!(/~~return_to_albums~~/, '<a href="../index.html">' + utf8(_('return to albums')) + '</a>')
990                         else
991                             line.sub!(/~~return_to_albums~~/, '')
992                         end
993                     end
994                     line
995                 }
996                 if xmldir.child_byname_notattr('image', 'deleted')
997                     substInFile("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html") { |line|
998                         sub_previous_next_album(previous_album, next_album, line)
999                     }
1000                 end
1001             end
1002         end
1003     }
1004
1005     msg 3, _(" all done.")
1006 end
1007
1008 handle_options
1009 read_config
1010 check_installation
1011
1012 build_html_skeletons
1013
1014 walk_source_dir