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