add navigation from subalbums to upper albums
[booh] / booh
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) 2004 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 require 'timeout'
28
29 require 'html_merges'
30
31 #- install location
32 $FPATH = '.'
33
34 #- bind text domain as soon as possible because some _() functions are called early to build data structures
35 bindtextdomain("booh")
36
37 #- options
38 $options = [
39     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
40
41     [ '--no-check',      '-n', GetoptLong::NO_ARGUMENT,       _("Don't check for needed external programs at startup") ],
42
43     [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original images/videos as files or subdirs") ],
44     [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album") ],
45     [ '--clean',         '-c', GetoptLong::NO_ARGUMENT,       _("Clean destination directory") ],
46
47     [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
48     [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
49     [ '--config-skel',   '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
50     [ '--merge-config',  '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new images/videos from --source") ],
51
52     [ '--sort-files',    '-S', GetoptLong::NO_ARGUMENT,       _("In --config-skel mode, alphabetically sort the files") ],
53
54     [ '--mproc',         '-m', GetoptLong::REQUIRED_ARGUMENT, _("Specify the number of processors for multi-processors machines") ],
55
56     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
57 ]
58
59 #- default values for some globals 
60 $VERSION = 1.0
61 $convert = 'convert -interlace line +profile "*"'
62 $verbose_level = 2
63 $switches = []
64
65 def __(string, *args)
66     if args.size == 0
67         _(string)
68     elsif args.size == 1
69         sprintf(_(string), args[0])
70     elsif args.size == 2
71         sprintf(_(string), args[0], args[1])
72     end
73 end
74
75 def usage
76     puts __("Usage: %s [OPTION]...", File.basename($0))
77     $options.each { |ary|
78         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
79     }
80 end
81
82 def msg(verbose_level, msg)
83     if verbose_level <= $verbose_level
84         if verbose_level == 0
85             warn __("\t***ERROR***: %s\n", msg)
86         elsif verbose_level == 1
87             warn __("\tWarning: %s\n", msg)
88         else
89             puts msg
90         end
91     end
92 end
93
94 def die(msg)
95     puts msg
96     exit 1
97 end
98
99 def handle_options
100     parser = GetoptLong.new
101     parser.set_options(*$options.collect { |ary| ary[0..2] })
102     begin
103         parser.each_option do |name, arg|
104             case name
105             when '--help'
106                 usage
107                 exit(0)
108
109             when '--no-check'
110                 $no_check = true
111
112             when '--source'
113                 $source = arg.sub(%r|/$|, '')
114                 if !File.directory?($source)
115                     die __("Argument to --source must be a directory")
116                 end
117             when '--destination'
118                 $dest = arg.sub(%r|/$|, '')
119                 if File.exists?($dest) && !File.directory?($dest)
120                     die __("If --destination exists, it must be a directory")
121                 end
122             when '--clean'
123                 system("rm -rf #{$dest}")
124
125             when '--theme'
126                 select_theme(arg)
127             when '--config'
128                 if File.readable?(arg)
129                     $xmldoc = REXML::Document.new File.new(arg)
130                     $mode = 'use_config'
131                 else
132                     die __('Config file does not exist or is unreadable.')
133                 end
134             when '--config-skel'
135                 if File.exists?(arg)
136                     msg 1, __("Config skeleton file already exists, backuping to #{arg}.backup")
137                     File.rename(arg, "#{arg}.backup")
138                 end
139                 $config_writeto = arg
140                 $xmldoc = Document.new "<booh version='#{$VERSION}'/>"
141                 $xmldoc << XMLDecl.new
142                 $mode = 'gen_config'
143             when '--merge-config'
144                 if File.readable?(arg)
145                     msg 2, __("Merge config notice: backuping current config file to #{arg}.backup")
146                     $xmldoc = REXML::Document.new File.new(arg)
147                     File.rename(arg, "#{arg}.backup")
148                     $config_writeto = arg
149                     $mode = 'merge_config'
150                 else
151                     die __('Config file does not exist or is unreadable.')
152                 end
153
154             when '--sort-files'
155                 $switches << 'sort-files'
156
157             when '--mproc'
158                 $mproc = arg.to_i
159                 $pids = []
160
161             when '--verbose-level'
162                 $verbose_level = arg.to_i
163
164             end
165         end
166     rescue
167         puts $!
168         usage
169         exit(1)
170     end
171
172     if !$source
173         die __("Missing --source parameter.")
174     end
175     if !$dest
176         die __("Missing --destination parameter.")
177     end
178     if !$xmldoc
179         die __("One of --config, --config-skel or --merge-config is needed.")
180     end
181
182     if !$theme
183         select_theme('simple')
184     end
185
186 end
187
188 def select_theme(name)
189     $theme = name
190     msg 3, __("Selecting theme `%s'", $theme)
191     themedir = "#{$FPATH}/themes/#{$theme}"
192     if !File.directory?(themedir)
193         die __("Theme was not found (tried %s directory).", themedir)
194     end
195     require "#{themedir}/parameters.rb"
196 end
197
198 def check_installation
199     if $no_check
200         return
201     end
202     %w(convert identify exif transcode mencoder).each { |prg|
203         if !system("which #{prg} >/dev/null")
204             die __("The `%s' program is typically needed. Re-run with --no-check if you're sure you're fine without it.", prg)
205         end
206     }
207 end
208
209 def replace_line(surround, keyword, line)
210     begin
211         contents = eval "$#{keyword}"
212         line.sub!(/#{surround}#{keyword}#{surround}/, contents)
213     rescue NameError
214         die __("No `%s' found for substitution", keyword)
215     end
216 end
217
218 def build_html_skeletons
219     $html_images     = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
220     $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
221     $html_index      = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
222     for line in $html_images + $html_thumbnails + $html_index
223         while line =~ /~~~(\w+)~~~/
224             replace_line('~~~', $1, line)
225         end
226     end
227 end
228
229 def sys(cmd)
230     msg 2, cmd
231     system(cmd)
232 end
233
234 #- parallelizable sys
235 def psys(cmd)
236     if $mproc
237         if pid = fork
238             $pids << pid
239         else
240             msg 2, cmd + ' &'
241             system(cmd)
242             exit 0
243         end
244         if $pids.length == $mproc
245             finished = Process.wait2
246             $pids.delete(finished[0])
247             $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
248         end
249     else
250         sys(cmd)
251     end
252 end
253
254 def find_caption_value(xmldir, filename)
255     xmldir.elements["[@filename='#{filename}']"].attributes['caption']
256 end
257
258 def find_captions(xmldir, images)
259     return images.collect { |img| find_caption_value(xmldir, img) }
260 end
261
262 def entry2type(entry)
263     if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i
264         return 'image'
265     elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i
266         #- might consider using file magic later..
267         return 'video'
268     else
269         return nil
270     end
271 end
272
273 #- grab the results of a command but don't sleep forever on a runaway process
274 def subproc_runaway_aware(command)
275     begin
276         timeout(5) {
277             return `#{command}`
278         }
279     rescue Timeout::Error    
280         msg 1, _("forgetting runaway process (transcode sucks again?)")
281         #- todo should slay transcode but dunno how to do that
282         return nil
283     end
284 end
285
286 def gen_thumbnails(orig, dests)
287     if !dests.detect { |dest| !File.exists?(dest['filename']) } 
288         return true
289     end
290
291     if entry2type(orig) == 'image'
292         convert_options = ''
293         orientation = `exif '#{orig}'`.detect { |line| line =~ /^Orientation/ }
294         if orientation =~ /right - top/
295             convert_options += '-rotate 90 '
296         end
297         if orientation =~ /left - bottom/
298             convert_options += '-rotate -90 '
299         end
300         for dest in dests
301             if !File.exists?(dest['filename'])
302                 psys("#{$convert} #{convert_options}-geometry #{dest['size']} '#{orig}' '#{dest['filename']}'")
303             end
304         end
305         return true
306
307     elsif entry2type(orig) == 'video'
308         dest_dir = File.dirname(dests[0]['filename'])
309         for dest in dests
310             if !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
311                 cmd = "transcode -a 0 -c 0-5 -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' 2>&1"
312                 msg 2, cmd
313                 if subproc_runaway_aware(cmd) =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
314                     msg 2, __("* could not extract first image of video %s with transcode, will try first converting with mencoder", orig)
315                     cmd = "mencoder '#{orig}' -nosound -ovc lavc -lavcopts vcodec=mpeg4 -o '#{dest_dir}/foo.avi' -frames 5 >/dev/null 2>/dev/null"
316                     msg 2, cmd
317                     system cmd
318                     if File.exists?("#{dest_dir}/foo.avi")
319                         cmd = "transcode -a 0 -c 0-5 -i '#{dest_dir}/foo.avi' -y jpg -o '#{dest_dir}/screenshot.jpg' 2>&1"
320                         msg 2, cmd
321                         results = subproc_runaway_aware(cmd)
322                         system("rm -f '#{dest_dir}/foo.avi'")
323                         if results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
324                             msg 0, __("could not extract first image of video %s encoded by mencoder", "#{dest_dir}/foo.avi")
325                             return false
326                         end
327                     else
328                         msg 0, __("could not make mencoder to encode %s to mpeg4", "#{orig}")
329                         return false
330                     end
331                 end
332
333             end
334             sys("#{$convert} -geometry #{dest['size']} #{dest_dir}/screenshot.jpg000004.jpg '#{dest['filename']}'")
335         end
336         system("rm -f #{dest_dir}/screenshot.jpg00000*")
337         return true
338     end
339 end
340
341 #- stolen from CVSspam
342 def urlencode(text)
343   text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
344     "%#{sprintf('%2X', $&[0])}"
345   end
346 end
347
348 def html_refresh(target)
349     return "<html><head><META http-equiv='refresh' content='0;URL=#{target}'></head><body></body><html>"
350 end
351
352 def discover_iterations(iterations, line)
353     if line =~ /~~iterate(\d)_open(_max(\d+))?~~/
354         for iter in iterations.values
355             if iter['open']
356                 iter['open'] = false
357                 iter['close_wait'] = $1.to_i
358             end
359         end
360         iterations[$1.to_i] = { 'open' => true, 'max' => $3, 'opening' => '', 'closing' => '' }
361         if $1.to_i == 1
362             line.sub!(/.*/, '~~thumbnails~~')
363         else
364             line.sub!(/.*/, '')
365         end
366     elsif line =~ /~~iterate(\d)_close~~/
367         iterations[$1.to_i]['open']  = false;
368         iterations[$1.to_i]['close'] = true;
369         line.sub!(/.*/, '')
370     else
371         for iter in iterations.values
372             if iter['open']
373                 iter['opening'] += line
374                 line.sub!(/.*/, '')
375             end
376             if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
377                 iter['closing'] += line
378                 line.sub!(/.*/, '')
379             end
380         end
381     end
382 end
383
384 def reset_iterations(iterations)
385     for iter in iterations.values
386         iter['value'] = 1
387     end
388 end
389
390 def run_iterations(iterations)
391     html = ''
392     for level in iterations.keys.sort
393         if iterations[level]['value'] == 1 || level == iterations.keys.max
394             html += iterations[level]['opening']
395         end
396         iterations[level]['value'] += 1
397         if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
398             iterations[level]['value'] = 1
399             iterations[level-1]['value'] = 1
400             html += iterations[level-1]['closing']
401         end
402     end
403     return html
404 end
405
406 def close_iterations(iterations)
407     html = ''
408     for level in iterations.keys.sort.reverse
409         html += iterations[level]['closing']
410     end
411     return html
412 end
413
414 def img_element(fullpath)
415     sizespec = `identify #{fullpath}` =~ / JPEG (\d+)x(\d+) / ? 'width="' + $1 + '" height="' + $2 + '"' : ''
416     return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' border="0"/>'
417 end
418
419 def walk_source_dir
420
421     `find #{$source} -type d`.each { |dir|
422         dir.chomp!
423
424         #- place xml document on proper node if exists, else create
425         xmldir = $xmldoc.elements["//dir[@path='#{dir}']"]
426         if $mode == 'use_config'
427             if !xmldir
428                 next
429             end
430         else
431             if $mode == 'gen_config' || ($mode == 'merge_config' && !xmldir)
432                 #- add the <dir..> element if necessary
433                 parent = File.dirname(dir)
434                 xmldir = $xmldoc.elements["//dir[@path='#{parent}']"]
435                 if !xmldir
436                     xmldir = $xmldoc.root
437                 end
438                 xmldir = xmldir.add_element 'dir', { 'path' => dir, 'new' => 1 }
439             end
440         end
441
442         #- read images/videos entries from config or from directories depending on mode
443         entries = []
444         if $mode == 'use_config'
445             msg 2, __("Handling %s from config list...", dir)
446             xmldir.elements.each { |element|
447                 if %w(image video).include?(element.name)
448                     entries << element.attributes['filename']
449                 end
450             }
451         else
452             msg 2, __("Examining %s...", dir)
453             entries = Dir.entries(dir)
454             if $switches.include?('sort-files')
455                 entries.sort!
456             end
457             #- populate config in case of gen_config, add new files in case of merge_config
458             for file in entries
459                 type = entry2type(file)
460                 if type && !xmldir.elements["#{type}[@filename='#{file}']"]
461                     xmldir.add_element type, { "filename" => file }
462                 end
463             end
464             if $mode == 'merge_config'
465                 #- cleanup removed files from config and reread entries from config to get proper ordering
466                 entries = []
467                 xmldir.elements.each { |element|
468                     fullpath = "#{dir}/#{element.attributes['filename']}"
469                     if %w(image video).include?(element.name)
470                         if !File.readable?(fullpath)
471                             msg 1, __("Config merge: removing #{fullpath} from config; use the backup file to retrieve caption info if this was a mistake")
472                             xmldir.delete(element)
473                         else
474                             entries << element.attributes['filename']
475                         end
476                     end
477                 }
478             end
479         end
480         images = entries.find_all { |e| entry2type(e) == 'image' }
481         msg 3, __("\t%s images", images.length)
482         videos = entries.find_all { |e| entry2type(e) == 'video' }
483         msg 3, __("\t%s videos", videos.length)
484
485         dest_dir = dir.sub(/^#{Regexp.quote($source)}/, $dest)
486         system("mkdir -p '#{dest_dir}'")
487
488         #- copy any resource file that goes with the theme (css, images..)
489         for entry in Dir.entries("#{$FPATH}/themes/#{$theme}")
490             if !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html parameters.rb CVS).include?(entry)
491                 if !File.exists?("#{dest_dir}/#{entry}")
492                     psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
493                 end
494             end
495         end
496
497         #- pass through if there are no images and videos
498         if images.size == 0 && videos.size == 0
499             next
500         end
501
502         msg 2, __("Outputting in %s...", dest_dir)
503
504         #- populate data structure with sizes from theme
505         for sizeobj in $images_size
506             fullscreen_images ||= {}
507             fullscreen_images[sizeobj['name']] = []
508             thumbnail_images ||= {}
509             thumbnail_images[sizeobj['name']] = []
510             thumbnail_videos ||= {}
511             thumbnail_videos[sizeobj['name']] = []
512         end
513
514         #- create thumbnails for images
515         images.each { |img|
516             base_dest_img = dest_dir + '/' + img.sub(/\.[^\.]+$/, '')
517             for sizeobj in $images_size
518                 size_fullscreen = sizeobj['fullscreen']
519                 size_thumbnails = sizeobj['thumbnails']
520                 fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
521                 thumbnail_dest_img  = base_dest_img + "-#{size_thumbnails}.jpg"
522                 fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
523                 thumbnail_images[sizeobj['name']]  << File.basename(thumbnail_dest_img)
524                 gen_thumbnails("#{dir}/#{img}", [ { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen },
525                                                   { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails } ])
526             end
527         }
528
529         #- create thumbnails for videos
530         videos.each { |video|
531             thumbnail_ok = true
532             for sizeobj in $images_size
533                 size_thumbnails = sizeobj['thumbnails']
534                 thumbnail_dest_img = dest_dir + '/' + video.sub(/\.[^\.]+$/, '') + "-#{size_thumbnails}.jpg"
535                 thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
536                 thumbnail_ok &&= gen_thumbnails("#{dir}/#{video}", [ { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails } ])
537             end
538             destvideo = "#{dest_dir}/#{video}"
539             if !File.exists?(destvideo)
540                 psys("cp '#{dir}/#{video}' '#{destvideo}'")
541             end
542         }
543
544         #- fake for gettext to find these; if themes need more sizes, english name for them should be added here
545         sizenames = { 'small' => utf8(_("small")), 'medium' => utf8(_("medium")), 'large' => utf8(_("large")) }
546         
547         #- generate thumbnails.html (page with thumbnails)
548         for sizeobj in $images_size
549             html = $html_thumbnails.collect { |l| l.clone }
550             iterations = {}
551             for i in html
552                 i.sub!(/~~run_slideshow~~/, '<a href="image-' + sizeobj['name'] + '.html?run_slideshow">' + utf8(_('Run slideshow!')) + '</a>')
553                 i.sub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || File.basename(dir))
554                 for sizeobj2 in $images_size
555                     if sizeobj != sizeobj2
556                         i.sub!(/~~size_#{sizeobj2['name']}~~/, '<a href="thumbnails-' + sizeobj2['name'] + '.html">' + sizenames[sizeobj2['name']] + '</a>')
557                     else
558                         i.sub!(/~~size_#{sizeobj2['name']}~~/, sizenames[sizeobj2['name']])
559                     end
560                 end
561                 discover_iterations(iterations, i)
562             end
563             html_thumbnails = ''
564             reset_iterations(iterations)
565             for file in entries
566                 type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
567                 if type
568                     html_thumbnails += run_iterations(iterations)
569                     if type == 'image'
570                         index = images.index(file)
571                         html_thumbnails.gsub!(/~~image_iteration~~/,
572                                               '<a href="image-' + sizeobj['name'] + '.html?current=' + fullscreen_images[sizeobj['name']][index] + '">' +
573                                                   img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
574                         html_thumbnails.gsub!(/~~caption_iteration~~/,
575                                               find_caption_value(xmldir, images[index]) || images[index])
576                         html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
577                         html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
578                     elsif type == 'video'
579                         index = videos.index(file)
580                         if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
581                             html_thumbnails.gsub!(/~~image_iteration~~/,
582                                                   '<a href="' + videos[index] + '">' + img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
583                         else
584                             html_thumbnails.gsub!(/~~image_iteration~~/,
585                                                   '<a href="' + videos[index] + '">' + utf8(_("(no preview)")) + '</a>')
586                         end
587                         html_thumbnails.gsub!(/~~caption_iteration~~/,
588                                               find_caption_value(xmldir, videos[index]) || videos[index])
589                         html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
590                         html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
591                     end
592                 end
593             end
594             html_thumbnails += close_iterations(iterations)
595             for i in html
596                 i.sub!(/~~thumbnails~~/, html_thumbnails)
597             end
598             ios = File.open("#{dest_dir}/thumbnails-#{sizeobj['name']}.html", "w")
599             ios.write(html)
600             ios.close
601         end
602
603         #- generate image.html (page with fullscreen images)
604         #- don't ask me why I need so many backslashes... the aim is to print \\\" for each " in the javascript source
605         captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\\\\\\\\\\\\\\\\\"' ) + '"' : '""' }.join(', ')
606         for sizeobj in $images_size
607             html = $html_images.collect { |l| l.clone }
608             images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
609             otherimages4js = ''
610             othersizes = []
611             for sizeobj2 in $images_size
612                 if sizeobj != sizeobj2
613                     otherimages4js += "var images_#{sizeobj2['name']} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
614                     othersizes << "\"#{sizeobj2['name']}\""
615                 end
616             end
617             for i in html
618                 i.sub!(/~~images~~/, images4js)
619                 i.sub!(/~~other_images~~/, otherimages4js)
620                 i.sub!(/~~other_sizes~~/, othersizes.join(', '))
621                 i.sub!(/~~captions~~/, captions4js)
622                 i.sub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || File.basename(dir))
623                 i.sub!(/~~thumbnails~~/, '<a href="thumbnails-' + sizeobj['name'] + '.html">' + utf8(_('Return to thumbnails')) + '</a>')
624                 for sizeobj2 in $images_size
625                     if sizeobj != sizeobj2
626                         i.sub!(/~~size_#{sizeobj2['name']}~~/,
627                                '<a href="image-' + sizeobj2['name'] + '.html" id="link' + sizeobj2['name'] + '">' + sizenames[sizeobj2['name']] + '</a>')
628                     else
629                         i.sub!(/~~size_#{sizeobj2['name']}~~/,
630                                sizenames[sizeobj2['name']])
631                     end
632                 end
633             end
634             ios = File.open("#{dest_dir}/image-#{sizeobj['name']}.html", "w")
635             ios.write(html)
636             ios.close
637         end
638     }
639
640     #- add attributes to <dir..> elements needing so
641     if $mode != 'use_config'
642         $xmldoc.elements.each('//dir[@new]') { |element|
643             path = captionpath = element.attributes['path']
644             child = element
645             captionfile = nil
646             while true
647                 child = child.elements[1]
648                 if !child
649                     element.remove  #- means we have a directory with nothing interesting in it
650                     break
651                 elsif child.name == 'dir'
652                     captionpath = child.attributes['path']
653                 else
654                     captionfile = "#{captionpath}/#{child.attributes['filename']}"
655                     break
656                 end
657             end
658             basename = File.basename(path)
659             if element.elements['dir']
660                 element.add_attribute('subdirs-caption', basename)
661                 element.add_attribute('subdirs-captionfile', captionfile)
662             end
663             if element.elements['image'] || element.elements['video']
664                 element.add_attribute('thumbnails-caption', basename)
665                 element.add_attribute('thumbnails-captionfile', captionfile)
666             end
667             element.delete_attribute('new')
668         }
669     end
670
671     #- write down to disk config if necessary
672     if $config_writeto
673         ios = File.open($config_writeto, "w")
674         $xmldoc.write(ios, 0)
675         ios.close
676     end
677
678     #- second pass to create index.html files
679     default_thumbnails = $images_size.detect { |sizeobj| sizeobj['default'] }
680     if !default_thumbnails
681         die __("Theme `%s' has no default size.", $theme)
682     else
683         default_thumbnails = default_thumbnails['name']
684     end
685
686     `find #{$source} -type d`.each { |dir|
687         dir.chomp!
688         xmldir = $xmldoc.elements["//dir[@path='#{dir}']"]
689         if !xmldir
690             next
691         end
692         dest_dir = dir.sub(/^#{Regexp.quote($source)}/, $dest)
693
694         html = $html_index.collect { |l| l.clone }
695         iterations = {}
696         for i in html
697             caption = xmldir.attributes['subdirs-caption'] || xmldir.attributes['thumbnails-caption'] || File.basename(dir)
698             i.gsub!(/~~title~~/, caption)
699             if xmldir.parent.name == 'dir'
700                 nav = ''
701                 path = '..'
702                 parent = xmldir.parent
703                 while parent.name == 'dir'
704                     parentcaption = parent.attributes['subdirs-caption'] || parent.attributes['thumbnails-caption'] || File.basename(parent.attributes['path'])
705                     nav = "<a href='#{path}/index.html'>#{parentcaption}</a> #{utf8(_(" > "))} #{nav}"
706                     path += '/..'
707                     parent = parent.parent
708                 end
709                 i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
710                 i.gsub!(/~~navigation~~/, nav + caption)
711             else
712                 i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
713             end
714             discover_iterations(iterations, i)
715         end
716
717         html_index = ''
718         reset_iterations(iterations)
719
720         if xmldir.elements['dir']
721             #- deal with "current" album (directs to "thumbnails" page)
722             if xmldir.attributes['thumbnails-caption']
723                 thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
724                 gen_thumbnails(xmldir.attributes['thumbnails-captionfile'], [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ])
725                 html_index += run_iterations(iterations)
726                 html_index.gsub!(/~~image_iteration~~/, "<a href='thumbnails-#{default_thumbnails}.html'>" + img_element(thumbnail) + '</a>')
727                 html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
728             end
729
730             #- deal with sub-albums (direct to subdirs/index.html pages)
731             xmldir.elements.each('dir') { |child|
732                 subdir = File.basename(child.attributes['path'])
733                 thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
734                 html_index += run_iterations(iterations)
735                 #- first look for subdirs info; if not, means there is no subdir
736                 caption = child.attributes['subdirs-caption']
737                 if caption
738                     gen_thumbnails(child.attributes['subdirs-captionfile'], [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ])
739                     html_index.gsub!(/~~caption_iteration~~/, caption)
740                 else
741                     gen_thumbnails(child.attributes['thumbnails-captionfile'], [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ])
742                     html_index.gsub!(/~~caption_iteration~~/, child.attributes['thumbnails-caption'])
743                 end
744                 html_index.gsub!(/~~image_iteration~~/, "<a href='#{subdir}/index.html'>" + img_element(thumbnail) + '</a>')
745             }
746
747         else
748             html = html_refresh("thumbnails-#{default_thumbnails}.html")
749         end
750
751         html_index += close_iterations(iterations)
752         for i in html
753             i.gsub!(/~~thumbnails~~/, html_index)
754         end
755
756         ios = File.open("#{dest_dir}/index.html", "w")
757         ios.write(html)
758         ios.close
759
760         #- substitute "return to albums" correctly
761         `find #{dest_dir} -maxdepth 1 -name "thumbnails*.html"`.each { |thumbnails|
762             thumbnails.chomp!
763             contents = File.open(thumbnails.chomp).readlines
764             for i in contents
765                 if xmldir.elements['dir']
766                     i.sub!(/~~return_to_albums~~/, '<a href="index.html">' + utf8(_('Return to albums')) + '</a>')
767                 else
768                     if xmldir.parent.name == 'dir'
769                         i.sub!(/~~return_to_albums~~/, '<a href="../index.html">' + utf8(_('Return to albums')) + '</a>')
770                     else
771                         i.sub!(/~~return_to_albums~~/, '')
772                     end
773                 end
774             end
775             ios = File.open(thumbnails, "w")
776             ios.write(contents)
777             ios.close
778         }
779     }
780 end
781
782 handle_options
783 check_installation
784
785 build_html_skeletons
786
787 walk_source_dir