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