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