don't limit load amount
[booh] / bin / booh-backend
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'gettext'
24 include GetText
25 require 'rexml/document'
26 include REXML
27
28 require 'booh/booh-lib'
29 require 'booh/html-merges'
30
31 #- bind text domain as soon as possible because some _() functions are called early to build data structures
32 bindtextdomain("booh")
33
34 #- options
35 $options = [
36     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
37     [ '--version',       '-V', GetoptLong::NO_ARGUMENT,       _("Print version and exit") ],
38
39     [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original images/videos as files or subdirs") ],
40     [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album") ],
41 #    [ '--clean',         '-c', GetoptLong::NO_ARGUMENT,       _("Clean destination directory") ],
42
43     [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
44     [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
45     [ '--config-skel',   '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
46     [ '--merge-config',  '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new/removed images/videos from --source, and change theme info") ],
47     [ '--merge-config-onedir',  '-O', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the subdir specified with --dir") ],
48     [ '--merge-config-subdirs', '-U', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the new subdirs down the subdir specified with --dir") ],
49     [ '--dir',           '-D', GetoptLong::REQUIRED_ARGUMENT, _("Directory for merge with --merge-config-onedir or --merge-config-subdirs") ],
50     [ '--use-config',    '-u', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to change theme info") ],
51     [ '--force',         '-f', GetoptLong::NO_ARGUMENT,       _("Force generation of album even if the GUI marked some directories as already generated") ],
52
53     [ '--sizes',         '-S', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of images sizes to use instead of all specified in the theme (this is a comma-separated list)") ],
54     [ '--thumbnails-per-row', '-T', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per row in the thumbnails page (if applicable in theme)") ],
55     [ '--thumbnails-per-page', '-p', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per page in the thumbnails page, after which split occurs") ],
56     [ '--optimize-for-32', '-o', GetoptLong::NO_ARGUMENT,       _("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)") ],
57     [ '--index-link',    '-l', GetoptLong::REQUIRED_ARGUMENT, _("Specify the HTML markup to use on the bottom of pages for a small link returning to wherever you see fit in your website (or somewhere else)") ],
58     [ '--made-with',     '-n', GetoptLong::REQUIRED_ARGUMENT, _("Specify the HTML markup to use on the bottom of pages for a small 'made with' message") ],
59     [ '--comments-format','-c', GetoptLong::REQUIRED_ARGUMENT, _("Specify comments format to use for images instead of only filename when creating new albums; use ImageMagick's format") ],
60
61     [ '--mproc',         '-m', GetoptLong::REQUIRED_ARGUMENT, _("Specify the number of processors for multi-processors machines") ],
62
63     [ '--for-gui',       '-g', GetoptLong::NO_ARGUMENT,       _("Do the minimum work to be able to see the album under the GUI (don't generate all thumbnails)") ],
64
65     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
66     [ '--info-pipe',     '-i', GetoptLong::REQUIRED_ARGUMENT, _("Name a file where to write information about what's going on (used by the GUI)") ],
67 ]
68
69 #- default values for some globals 
70 $switches = []
71 $stdout.sync = true
72 $no_identify = false
73 $ignore_videos = false
74 $forgui = false
75 $hardlinks_ok = true
76
77 def usage
78     puts _("Usage: %s [OPTION]...") % File.basename($0)
79     $options.each { |ary|
80         printf " %3s, %-18s %s\n", ary[1], ary[0], ary[3]
81     }
82 end
83
84 def handle_options
85     parser = GetoptLong.new
86     parser.set_options(*$options.collect { |ary| ary[0..2] })
87     begin
88         parser.each_option do |name, arg|
89             case name
90             when '--help'
91                 usage
92                 exit(0)
93
94             when '--version'
95                 puts _("Booh version %s
96
97 Copyright (c) 2005 Guillaume Cottenceau.
98 This is free software; see the source for copying conditions.  There is NO
99 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
100
101                 exit(0)
102
103             when '--source'
104                 $source = File.expand_path(arg.sub(%r|/$|, ''))
105                 if !File.directory?($source)
106                     die _("Argument to --source must be a directory")
107                 end
108             when '--destination'
109                 $dest = File.expand_path(arg.sub(%r|/$|, ''))
110                 if File.exists?($dest) && !File.directory?($dest)
111                     die _("If --destination exists, it must be a directory")
112                 end
113                 if $dest != make_dest_filename($dest)
114                     die _("Sorry, destination directory can't contain non simple alphanumeric characters.")
115                 end
116 #            when '--clean'
117 #                system("rm -rf #{$dest}")
118
119             when '--theme'
120                 $theme = arg
121             when '--config'
122                 arg = File.expand_path(arg)
123                 if File.readable?(arg)
124                     $xmldoc = REXML::Document.new File.new(arg)
125                     $mode = 'use_config'
126                 else
127                     die _('Config file does not exist or is unreadable.')
128                 end
129             when '--config-skel'
130                 arg = File.expand_path(arg)
131                 if File.exists?(arg)
132                     if File.directory?(arg)
133                         die _("Config skeleton file (%s) already exists and is a directory! Please change the filename.") % arg
134                     else
135                         msg 1, _("Config skeleton file already exists, backuping to %s.backup") % arg
136                         File.rename(arg, "#{arg}.backup")
137                     end
138                 end
139                 $config_writeto = arg
140                 $mode = 'gen_config'
141             when '--merge-config'
142                 arg = File.expand_path(arg)
143                 if File.readable?(arg)
144                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
145                     $xmldoc = REXML::Document.new File.new(arg)
146                     File.rename(arg, "#{arg}.backup")
147                     $config_writeto = arg
148                     $mode = 'merge_config'
149                 else
150                     die _('Config file does not exist or is unreadable.')
151                 end
152             when '--merge-config-onedir'
153                 arg = File.expand_path(arg)
154                 if File.readable?(arg)
155                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
156                     $xmldoc = REXML::Document.new File.new(arg)
157                     File.rename(arg, "#{arg}.backup")
158                     $config_writeto = arg
159                     $mode = 'merge_config_onedir'
160                 else
161                     die _('Config file does not exist or is unreadable.')
162                 end
163             when '--merge-config-subdirs'
164                 arg = File.expand_path(arg)
165                 if File.readable?(arg)
166                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
167                     $xmldoc = REXML::Document.new File.new(arg)
168                     File.rename(arg, "#{arg}.backup")
169                     $config_writeto = arg
170                     $mode = 'merge_config_subdirs'
171                 else
172                     die _('Config file does not exist or is unreadable.')
173                 end
174             when '--dir'
175                 arg = File.expand_path(arg)
176                 if !File.readable?(arg)
177                     die _('Specified directory to merge with --dir is not readable')
178                 else
179                     $onedir = arg
180                 end
181             when '--use-config'
182                 arg = File.expand_path(arg)
183                 if File.readable?(arg)
184                     msg 2, _("Use config notice: backuping current config file to %s.backup") % arg
185                     $xmldoc = REXML::Document.new File.new(arg)
186                     File.rename(arg, "#{arg}.backup")
187                     $config_writeto = arg
188                     $mode = 'use_config_changetheme'
189                 else
190                     die _('Config file does not exist or is unreadable.')
191                 end
192
193             when '--sizes'
194                 $limit_sizes = arg
195
196             when '--thumbnails-per-row'
197                 $N_per_row = arg
198
199             when '--thumbnails-per-page'
200                 $N_per_page = arg
201
202             when '--optimize-for-32'
203                 $optimize_for_32 = true
204
205             when '--made-with'
206                 $madewith = arg
207
208             when '--index-link'
209                 $indexlink = arg
210
211             when '--comments-format'
212                 $commentsformat = arg
213
214             when '--force'
215                 $force = true
216
217             when '--mproc'
218                 $mproc = arg.to_i
219                 $pids = []
220
221             when '--for-gui'
222                 $forgui = true
223
224             when '--verbose-level'
225                 $verbose_level = arg.to_i
226
227             when '--info-pipe'
228                 $info_pipe = File.open(arg, File::WRONLY)
229                 $info_pipe.sync = true
230             end
231         end
232     rescue
233         puts $!
234         usage
235         exit(1)
236     end
237
238     if !$source && $xmldoc
239         $source = from_utf8($xmldoc.root.attributes['source']).sub(%r|/$|, '')
240         $dest = from_utf8($xmldoc.root.attributes['destination']).sub(%r|/$|, '')
241         $theme ||= $xmldoc.root.attributes['theme']
242         $limit_sizes ||= $xmldoc.root.attributes['limit-sizes']
243         if $mode == 'use_config' || $mode =~ /^merge_config/
244             $optimize_for_32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
245             $N_per_row = $xmldoc.root.attributes['thumbnails-per-row']
246             $N_per_page = $xmldoc.root.attributes['thumbnails-per-page']
247             $madewith = $xmldoc.root.attributes['made-with']
248             $indexlink = $xmldoc.root.attributes['index-link']
249         end
250     end
251
252     if $mode == 'merge_config_onedir' && !$onedir
253         die _("Missing --dir for --merge-config-onedir")
254     end
255     if $mode == 'merge_config_subdirs' && !$onedir
256         die _("Missing --dir for --merge-config-subdirs")
257     end
258
259     if !$source
260         usage
261         exit(0)
262     end
263     if !$dest
264         die _("Missing --destination parameter.")
265     end
266     if !$theme
267         $theme = 'simple'
268     end
269
270     select_theme($theme, $limit_sizes, $optimize_for_32, $N_per_row)
271
272     if !$xmldoc
273         additional_params = ''
274         if $limit_sizes
275             additional_params += "limit-sizes='#{$limit_sizes}'"
276         end
277         if $optimize_for_32
278             additional_params += " optimize-for-32='true'"
279         end
280         if $N_per_row
281             additional_params += " thumbnails-per-row='#{$N_per_row}'"
282         end
283         if $N_per_page
284             additional_params += " thumbnails-per-page='#{$N_per_page}'"
285         end
286         if $madewith
287             additional_params += " made-with='#{$madewith}'"
288         end
289         if $indexlink
290             additional_params += " index-link='#{$indexlink}'"
291         end
292         $xmldoc = Document.new "<booh version='#{$VERSION}' source='#{utf8($source)}' destination='#{utf8($dest)}' theme='#{$theme}' #{additional_params}/>"
293         $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
294         $mode = 'gen_config'
295     end
296
297     if $mode == 'merge_config' || $mode == 'use_config_changetheme'
298         $xmldoc.root.add_attribute('theme', $theme)
299         $xmldoc.root.add_attribute('version', $VERSION)
300         if $limit_sizes
301             $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
302         else
303             $xmldoc.root.delete_attribute('limit-sizes')
304         end
305         if $optimize_for_32
306             $xmldoc.root.add_attribute('optimize-for-32', 'true')
307         else
308             $xmldoc.root.delete_attribute('optimize-for-32')
309         end
310         if $N_per_row
311             $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
312         else
313             $xmldoc.root.delete_attribute('thumbnails-per-row')
314         end
315         if $N_per_page
316             $xmldoc.root.add_attribute('thumbnails-per-page', $N_per_page)
317         else
318             $xmldoc.root.delete_attribute('thumbnails-per-page')
319         end
320         if $madewith
321             $xmldoc.root.add_attribute('made-with', $madewith)
322         else
323             $xmldoc.root.delete_attribute('made-with')
324         end
325         if $indexlink
326             $xmldoc.root.add_attribute('index-link', $indexlink)
327         else
328             $xmldoc.root.delete_attribute('index-link')
329         end
330     end
331
332     if $madewith
333         $madewith = $madewith.gsub('%booh', '"http://booh.org/"')
334     end
335 end
336
337 def read_config
338     $config = {}
339 end
340
341 def write_config
342 end
343
344 def info(value)
345     if $info_pipe
346         $info_pipe.puts(value)
347     end
348 end
349
350 def die(value)
351     if $info_pipe
352         $info_pipe.puts("die: " + value)
353     end
354     die_ value
355 end
356
357 def check_installation
358     if !system("which convert >/dev/null 2>/dev/null")
359         die _("The program 'convert' is needed. Please install it. 
360 It is generally available with the 'ImageMagick' software package.")
361     end
362     if !system("which identify >/dev/null 2>/dev/null")
363         msg 1, _("the program 'identify' is needed to get images sizes and EXIF data. Please install it.
364 It is generally available with the 'ImageMagick' software package.")
365         $no_identify = true
366     end
367     missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
368     if missing != []
369         msg 1, _("the following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')
370         $ignore_videos = true
371     end
372 end
373
374 def replace_line(surround, keyword, line)
375     begin
376         contents = eval "$#{keyword}"
377         line.sub!(/#{surround}#{keyword}#{surround}/, contents)
378     rescue TypeError
379         die _("No '%s' found for substitution") % keyword
380     end
381 end
382
383 def build_html_skeletons
384     $html_images     = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
385     $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
386     $html_index      = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
387     for line in $html_images + $html_thumbnails + $html_index
388         while line =~ /~~~(\w+)~~~/
389             replace_line('~~~', $1, line)
390         end
391     end
392 end
393
394 def find_caption_value(xmldir, filename)
395     if cap = xmldir.elements["*[@filename='#{utf8(filename)}']"].attributes['caption']
396         return cap.gsub("\n", '<br/>')
397     else
398         return nil
399     end
400 end
401
402 def find_captions(xmldir, images)
403     return images.collect { |img| find_caption_value(xmldir, img) }
404 end
405
406 #- stolen from CVSspam
407 def urlencode(text)
408   text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
409     "%#{sprintf('%2X', $&[0])}"
410   end
411 end
412
413 def all_images_sizes
414     return $limit_sizes =~ /original/ ? $images_size + [ { 'name' => 'original' } ] : $images_size
415 end
416
417 def html_reload_to_thumbnails
418     html_reload_to_thumbnails = $preferred_size_reloader.clone
419     html_reload_to_thumbnails.gsub!(/~~theme~~/, $theme)
420     html_reload_to_thumbnails.gsub!(/~~default_size~~/, $default_size['name'])
421     html_reload_to_thumbnails.gsub!(/~~all_sizes~~/, all_images_sizes.collect { |s| "\"#{size2js(s['name'])}\"" }.join(', '))
422     size_auto_chooser = '';
423     all_images_sizes.find_all { |s| s.has_key?('optimizedforwidth') }.
424                      sort { |a,b| b['optimizedforwidth'].to_i <=> a['optimizedforwidth'].to_i }.
425                      each { |s| size_auto_chooser += "if (w + 50 > #{s['optimizedforwidth']}) { return 'thumbnails-#{size2js(s['name'])}-0.html'; }\n" }
426     html_reload_to_thumbnails.gsub!(/~~size_auto_chooser~~/, size_auto_chooser)
427     return html_reload_to_thumbnails
428 end
429
430 def discover_iterations(iterations, line)
431     if line =~ /~~iterate(\d)_open(_max(\d+|N))?~~/
432         for iter in iterations.values
433             if iter['open']
434                 iter['open'] = false
435                 iter['close_wait'] = $1.to_i
436             end
437         end
438         max = $3 == 'N' ? ($N_per_row || $default_N) : $3
439         iterations[$1.to_i] = { 'open' => true, 'max' => max, 'opening' => '', 'closing' => '' }
440         if $1.to_i == 1
441             line.sub!(/.*/, '~~thumbnails~~')
442         else
443             line.sub!(/.*/, '')
444         end
445     elsif line =~ /~~iterate(\d)_close~~/
446         iterations[$1.to_i]['open']  = false;
447         iterations[$1.to_i]['close'] = true;
448         line.sub!(/.*/, '')
449     else
450         for iter in iterations.values
451             if iter['open']
452                 iter['opening'] += line
453                 line.sub!(/.*/, '')
454             end
455             if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
456                 iter['closing'] += line
457                 line.sub!(/.*/, '')
458             end
459         end
460     end
461 end
462
463 def reset_iterations(iterations)
464     for iter in iterations.values
465         iter['value'] = 0
466     end
467 end
468
469 def run_iterations(iterations, amount)
470     html = ''
471     should_rerun = false
472     for level in iterations.keys.sort
473         if iterations[level]['value'] == 0
474             html += iterations[level]['opening']
475         elsif level == iterations.keys.max
476             if !iterations[level]['max'] || iterations[level]['max'] && iterations[level]['value'] + amount <= iterations[level]['max'].to_i
477                 html += iterations[level]['opening']
478             else
479                 should_rerun = true
480             end
481         end
482         iterations[level]['value'] += amount
483         if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
484             iterations[level]['value'] = 0
485             iterations[level-1]['value'] = 0
486             html += iterations[level-1]['closing']
487         end
488     end
489     if should_rerun
490         return html + run_iterations(iterations, amount)
491     else
492         return html
493     end
494 end
495
496 def close_iterations(iterations)
497     html = ''
498     for level in iterations.keys.sort.reverse
499         html += iterations[level]['closing']
500     end
501     return html
502 end
503
504 def img_element(fullpath)
505     if size = get_image_size(fullpath)
506         sizespec = 'width="' + size[:x].to_s + '" height="' + size[:y].to_s + '"'
507     else
508         sizespec = ''
509     end
510     return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' alt="image"/>'
511 end
512
513 def size2js(name)
514     return name.gsub(/-/, '')
515 end
516
517 def substitute_html_sizes(html, sizeobj, type, suffix)
518     sizestrings = []
519     if $images_size.length > 1 || (type == 'image' && $limit_sizes =~ /original/)
520         for sizeobj2 in $images_size
521             sizejs = size2js(sizeobj2['name'])
522             sizen = sizename(sizeobj2['name'])
523             if sizeobj != sizeobj2
524                 if type == 'thumbnails'
525                     sizestrings << '<a href="thumbnails-' + sizejs + suffix + '.html" onclick="set_preferred_size(\'' + sizejs + '\')">' + sizen + '</a>'
526                 else
527                     sizestrings << '<a id="link' + sizejs + '" onclick="set_preferred_size(\'' + sizejs + '\')">' + sizen + '</a>'
528                 end
529             else
530                 sizestrings << sizen
531             end
532         end
533         if type == 'image' && $limit_sizes =~ /original/
534             sizestrings << '<a id="linkoriginal" target="newframe">' + sizename('original') + '</a>'
535         end
536     end
537     html.sub!(/~~sizes~~(.+)~~/) { sizestrings.join($1) }
538 end
539
540 def xmldir2destdir(xmldir)
541     return make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))
542 end
543
544 def find_previous_album(xmldir)
545     relative_pos = ''
546     begin
547         #- move to previous dir element if exists
548         if prevelem = xmldir.previous_element_byname_notattr('dir', 'deleted')
549             xmldir = prevelem
550             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
551             child = nil
552             #- after having moved to previous dir, we need to go down last subdir until the last one
553             while child = xmldir.elements['dir']
554                 while nextchild = child.next_element_byname_notattr('dir', 'deleted')
555                     child = nextchild
556                 end
557                 relative_pos += xmldir2destdir(child) + '/'
558                 xmldir = child
559             end
560         else
561             #- previous dir doesn't exist, move to previous dir element if exists
562             xmldir = xmldir.parent
563             if xmldir.name == 'dir' && !xmldir.attributes['deleted']
564                 relative_pos += '../'
565             else
566                 return nil
567             end
568         end
569     end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
570     return File.reduce_path(relative_pos)
571 end
572
573 def find_next_album(xmldir)
574     relative_pos = ''
575     begin
576         #- first child dir element (catches when initial xmldir has both thumbnails and subdirs)
577         if firstchild = xmldir.child_byname_notattr('dir', 'deleted')
578             xmldir = firstchild
579             relative_pos += xmldir2destdir(xmldir) + '/'
580         #- next brother
581         elsif nextbro = xmldir.next_element_byname_notattr('dir', 'deleted')
582             xmldir = nextbro
583             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
584         else
585             #- go up until we have a next brother or we are finished
586             begin
587                 xmldir = xmldir.parent
588                 relative_pos += '../'
589             end while xmldir && !xmldir.next_element_byname_notattr('dir', 'deleted')
590             if xmldir
591                 xmldir = xmldir.next_element_byname('dir')
592                 relative_pos += '../' + xmldir2destdir(xmldir) + '/'
593             else
594                 return nil
595             end
596         end
597     end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
598     return File.reduce_path(relative_pos)
599 end
600
601 def sub_previous_next_album(previous_album, next_album, html)
602     if previous_album
603         html.gsub!(/~~previous_album~~/, '<a href="' + previous_album + 'thumbnails.html">' + utf8(_('previous album')) + '</a>')
604         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/) { $1 }
605     else
606         html.gsub!(/~~previous_album~~/, '')
607         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/, '')
608     end
609     if next_album
610         html.gsub!(/~~next_album~~/, '<a href="' + next_album + 'thumbnails.html">' + utf8(_('next album')) + '</a>')
611         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/) { $1 }
612     else
613         html.gsub!(/~~next_album~~/, '')
614         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/, '')
615     end
616     return html
617 end
618
619 def walk_source_dir
620
621     #- preprocess the path->dir, rexml is very slow with that; we seem to improve speed by 7%
622     optxpath = {}
623     $xmldoc.elements.each('//dir') { |elem|
624         optxpath[elem.attributes['path']] = elem
625     }
626
627     examined_dirs = nil
628     if $mode == 'merge_config_onedir'
629         examined_dirs = [ $onedir ]
630     elsif $mode == 'merge_config_subdirs'
631         examined_dirs = `find '#{$onedir}' -type d -follow`.sort.collect { |v| v.chomp }.delete_if { |v| optxpath.has_key?(utf8(v)) }
632     else
633         examined_dirs = `find '#{$source}' -type d -follow`.sort.collect { |v| v.chomp }
634         if $mode == 'merge_config'
635             $xmldoc.elements.each('//dir') { |elem|
636                 if ! examined_dirs.include?(elem.attributes['path'])
637                     msg 2, _("Merging config: removing directory %s from config, isn't on filesystem anymore") % elem.attributes['path']
638                     elem.remove
639                 end
640             }
641         end
642     end
643     info("directories: #{examined_dirs.length}, sizes: #{$images_size.length}")
644
645     examined_dirs.each { |dir|
646         if dir =~ /'/
647             die _("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir
648         end
649         if $mode !~ /^use_config/
650             Dir.entries(dir).each { |file|
651                 if file =~ /['"\[\]]/
652                     die _("Files can't contain any of the characters ', \", [ or ], sorry: %s") % "#{dir}/#{file}"
653                 end
654             }
655         end
656     }
657
658     examined_dirs.each { |dir|
659         if File.basename(dir) =~ /^\./
660             msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
661             next
662         end
663
664         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
665
666         #- place xml document on proper node if exists, else create
667         xmldir = optxpath[utf8(dir)]
668         if $mode == 'use_config' || $mode == 'use_config_changetheme'
669             if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
670                 info("walking: #{dir}|#{$source}, 0 elements")
671                 if xmldir && xmldir.attributes['deleted']
672                     system("rm -rf '#{dest_dir}'")
673                 end
674                 next
675             end
676         else
677             if $mode == 'gen_config' || (($mode == 'merge_config' || $mode == 'merge_config_subdirs') && !xmldir)
678                 #- add the <dir..> element if necessary
679                 parent = File.dirname(dir)
680                 xmldir = $xmldoc.elements["//dir[@path='#{utf8(parent)}']"]
681                 if !xmldir
682                     xmldir = $xmldoc.root
683                 end
684                 #- need to remove the already-generated mark of the parent because of the sub-albums page containing now one more element
685                 xmldir.delete_attribute('already-generated')
686                 xmldir = optxpath[utf8(dir)] = xmldir.add_element('dir', { 'path' => utf8(dir) })
687             end
688         end
689         xmldir.delete_attribute('already-generated')
690
691         #- read images/videos entries from config or from directories depending on mode
692         entries = []
693         if $mode == 'use_config' || $mode == 'use_config_changetheme'
694             msg 2, _("Handling %s from config list...") % dir
695             xmldir.elements.each { |element|
696                 if %w(image video).include?(element.name) && !element.attributes['deleted']
697                     entries << from_utf8(element.attributes['filename'])
698                 end
699             }
700         else
701             msg 2, _("Examining %s...") % dir
702             entries = Dir.entries(dir).sort
703             #- populate config in case of gen_config, add new files in case of merge_config
704             for file in entries
705                 if file =~ /['"\[\]]/
706                     msg 1, _("Ignoring %s, contains one of forbidden characters: '\"[]") % "#{dir}/#{file}"
707                 else
708                     type = entry2type(file)
709                     if type && !xmldir.elements["#{type}[@filename='#{utf8(file)}']"]
710                         #- hack: don't run identify (which is slow) if format only contains default %t
711                         if $commentsformat && type == 'image' && $commentsformat != '%t'
712                             comment = utf8(`identify -format "#{$commentsformat}" '#{dir}/#{file}'`.chomp.sub(/\.$/, ''))
713                         else
714                             comment = utf8cut(file.sub(/\.[^\.]+$/, ''), 18)
715                         end
716                         xmldir.add_element type, { "filename" => utf8(file), "caption" => comment }
717                     end
718                 end
719             end
720             if $mode != 'gen_config'
721                 #- cleanup removed files from config and reread entries from config to get proper ordering
722                 entries = []
723                 xmldir.elements.each { |element|
724                     fullpath = "#{dir}/#{from_utf8(element.attributes['filename'])}"
725                     if %w(image video).include?(element.name)
726                         if !File.readable?(fullpath)
727                             msg 1, _("Config merge: removing %s from config; use the backup file to retrieve caption info if this was a mistake") % fullpath
728                             xmldir.delete(element)
729                         elsif !element.attributes['deleted']
730                             entries << from_utf8(element.attributes['filename'])
731                         end
732                     end
733                 }
734                 #- if there is no more elements here, there is no album here anymore
735                 if !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
736                     xmldir.delete_attribute('thumbnails-caption')
737                     xmldir.delete_attribute('thumbnails-captionfile')
738                 end
739             end
740         end
741         images = entries.find_all { |e| entry2type(e) == 'image' }
742         msg 3, _("\t%s images") % images.length
743         videos = entries.find_all { |e| entry2type(e) == 'video' }
744         msg 3, _("\t%s videos") % videos.length
745         info("walking: #{dir}|#{$source}, #{images.length + videos.length} elements")
746
747         system("mkdir -p '#{dest_dir}'")
748
749         #- generate .htaccess file
750         if !$forgui
751             ios = File.open("#{dest_dir}/.htaccess", "w")
752             ios.write("AddCharset UTF-8 .html\n")
753             if auth_user_file = xmldir.attributes['password-protect']
754                 msg 3, _("\tgenerating password protection file #{dest_dir}/.htaccess")
755                 ios.write("AuthType Basic\nAuthName \"protected area\"\nAuthUserFile #{auth_user_file}\nrequire valid-user\n")
756             end
757             ios.close
758         end
759
760         #- pass through if there are no images and videos
761         if images.size == 0 && videos.size == 0
762             if !$forgui
763                 #- cleanup old images/videos, especially if this directory contained images/videos previously.
764                 themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
765                                 find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(e) }
766                 if $mode != 'gen_config'
767                     rightful_images = [ '.htaccess' ]
768                     if xmldir.attributes['thumbnails-caption']
769                         rightful_images << 'thumbnails-thumbnail.jpg'
770                     end
771                     xmldir.elements.each('dir') { |child|
772                         if child.attributes['deleted']
773                             next
774                         end
775                         subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
776                         rightful_images << "thumbnails-#{subdir}.jpg"
777                     }
778                     to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?("#{dest_dir}/#{e}") && !rightful_images.include?(e) } - themestuff
779                     if to_del.size > 0
780                         system("rm -f " + to_del.collect { |e| "'#{dest_dir}/#{e}'" }.join(' '))
781                     end
782                 end
783                 
784                 #- copy any resource file that goes with the theme (css, images..)
785                 themestuff.each { |entry|
786                     if !File.exists?("#{dest_dir}/#{entry}")
787                         psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
788                     end
789                 }
790             end
791             next
792         end
793
794         msg 2, _("Outputting in %s...") % dest_dir
795
796         #- populate data structure with sizes from theme
797         for sizeobj in $images_size
798             fullscreen_images ||= {}
799             fullscreen_images[sizeobj['name']] = []
800             thumbnail_images ||= {}
801             thumbnail_images[sizeobj['name']] = []
802             thumbnail_videos ||= {}
803             thumbnail_videos[sizeobj['name']] = []
804         end
805         #- a special dummy size to keep 'references' to thumbnails in case of panorama, because the GUI will use the regular thumbnails
806         thumbnail_images['dont-delete-file-for-gui'] = []
807         if $limit_sizes =~ /original/
808             fullscreen_images['original'] = []
809         end
810
811         images.size >= 1 and msg 3, _("\tcreating images thumbnails...")
812
813         #- create thumbnails for images
814         images.each { |img|
815             info("processing element")
816             base_dest_img = dest_dir + '/' + make_dest_filename(img.sub(/\.[^\.]+$/, ''))
817             if $forgui
818                 thumbnail_dest_img = base_dest_img + "-#{$default_size['thumbnails']}.jpg"
819                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
820             else
821                 todo = []
822                 elem = xmldir.elements["image[@filename='#{utf8(img)}']"]
823                 for sizeobj in $images_size
824                     size_fullscreen = sizeobj['fullscreen']
825                     size_thumbnails = sizeobj['thumbnails']
826                     fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
827                     fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
828                     todo << { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen }
829                     if pano = pano_amount(elem)
830                         thumbnail_images['dont-delete-file-for-gui'] << File.basename(base_dest_img + "-#{size_thumbnails}.jpg")
831                         size_thumbnails = size_thumbnails.sub(/(\d+)/) { ($1.to_i * pano).to_i }
832                     end
833                     thumbnail_dest_img = base_dest_img + "-#{size_thumbnails}.jpg"
834                     thumbnail_images[sizeobj['name']] << File.basename(thumbnail_dest_img)
835                     todo << { 'filename' => thumbnail_dest_img,  'size' => size_thumbnails }
836                 end
837                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, todo)
838                 if $limit_sizes =~ /original/
839                     fullscreen_images['original'] << img
840                 end
841                 destimg = "#{dest_dir}/#{img}"
842                 if $limit_sizes =~ /original/ && !File.exists?(destimg)
843                     if $hardlinks_ok
844                         if ! sys("ln '#{dir}/#{img}' '#{destimg}'")
845                             $hardlinks_ok = false
846                         end
847                     end
848                     if ! $hardlinks_ok
849                         psys("cp '#{dir}/#{img}' '#{destimg}'")
850                     end
851                 end
852             end
853         }
854
855         videos.size >= 1 and msg 3, _("\tcreating videos thumbnails...")
856
857         #- create thumbnails for videos
858         videos.each { |video|
859             info("processing element")
860             if $forgui
861                 thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{$default_size['thumbnails']}.jpg"
862                 gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
863             else
864                 todo = []
865                 for sizeobj in $images_size
866                     size_thumbnails = sizeobj['thumbnails']
867                     thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{size_thumbnails}.jpg"
868                     thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
869                     todo << { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails }
870                 end
871                 gen_thumbnails_element("#{dir}/#{video}", xmldir, true, todo)
872             end
873             destvideo = "#{dest_dir}/#{video}"
874             if !File.exists?(destvideo)
875                 if $hardlinks_ok
876                     if ! sys("ln '#{dir}/#{video}' '#{destvideo}'")
877                         $hardlinks_ok = false
878                     end
879                 end
880                 if ! $hardlinks_ok
881                     psys("cp '#{dir}/#{video}' '#{destvideo}'")
882                 end
883             end
884         }
885
886         if !$forgui
887             themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
888                              find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(e) }
889
890             #- cleanup old images/videos (for when removing elements or sizes)
891             all_elements = fullscreen_images.collect { |e| e[1] }.flatten.
892                      concat(thumbnail_images.collect { |e| e[1] }.flatten).
893                      concat(thumbnail_videos.collect { |e| e[1] }.flatten).
894                      concat(videos).
895                      push('.htaccess')
896             to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?("#{dest_dir}/#{e}") && !all_elements.include?(e) && e !~ /^thumbnails-\w+\.jpg/ } - themestuff
897             if to_del.size > 0
898                 msg 3, _("\tcleaning up: #{to_del.join(', ')}")
899                 system("rm -f " + to_del.collect { |e| "#{dest_dir}/#{e}" }.join(' '))
900             end
901
902             #- copy any resource file that goes with the theme (css, images..)
903             themestuff.each { |entry|
904                 if !File.exists?("#{dest_dir}/#{entry}")
905                     psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
906                 end
907             }
908
909             msg 3, _("\tgenerating HTML pages...")
910             #- fixup max per page
911             if $N_per_page && $N_per_row
912                 $N_per_page = $N_per_page.to_i / $N_per_row.to_i * $N_per_row.to_i
913             end
914
915             #- generate thumbnails*.html (page with thumbnails)
916             image2thumbnailpage4js = []
917             for sizeobj in $images_size
918                 info("processing size")
919                 html = $html_thumbnails.collect { |l| l.clone }
920                 iterations = {}
921                 for i in html
922                     i.sub!(/~~title~~/,
923                            xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
924                     discover_iterations(iterations, i)
925                 end
926                 all_pages = []
927                 html_thumbnails = ''
928                 html_thumbnails_nojs = ''
929                 counter = 0
930                 pagecount = 0
931                 reset_iterations(iterations)
932                 #- preprocess the @filename->elem, rexml is very slow with that; we dramatically improve this part of the processing
933                 optfilename = {}
934                 xmldir.elements.each('image') { |elem|
935                     optfilename[elem.attributes['filename']] = elem
936                 }
937                 for file in entries
938                     type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
939                     if type
940                         if type == 'image' && elem = optfilename[utf8(file)]
941                             if pano = pano_amount(elem)
942                                 html_elem = run_iterations(iterations, pano)
943                                 counter += count = pano.ceil
944                                 html_elem.gsub!(/~~colspan~~/) { "colspan=\"#{count}\"" }
945                             else
946                                 html_elem = run_iterations(iterations, 1)
947                                 counter += 1
948                                 html_elem.gsub!(/~~colspan~~/, '')
949                             end
950                         else 
951                             html_elem = run_iterations(iterations, 1)
952                             counter += 1
953                             html_elem.gsub!(/~~colspan~~/, '')
954                         end
955                         if type == 'image'
956                             index = images.index(file)
957                             html_elem.gsub!(/~~caption_iteration~~/,
958                                             find_caption_value(xmldir, images[index]) || utf8(images[index]))
959                             html_elem.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
960                             html_elem.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
961                         elsif type == 'video'
962                             index = videos.index(file)
963                             if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
964                                 html_elem.gsub!(/~~image_iteration~~/,
965                                                 '<a href="' + videos[index] + '">' + img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
966                             else
967                                 html_elem.gsub!(/~~image_iteration~~/,
968                                                 '<a href="' + videos[index] + '">' + utf8(_("(no preview)")) + '</a>')
969                             end
970                             html_elem.gsub!(/~~caption_iteration~~/, find_caption_value(xmldir, videos[index]) || utf8(videos[index]))
971                             html_elem.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
972                             html_elem.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
973                         end
974                         html_thumbnails += html_elem
975                         html_thumbnails_nojs += html_elem
976                         if type == 'image'
977                             html_thumbnails.gsub!(/~~image_iteration~~/,
978                                                   '<a href="image-' + size2js(sizeobj['name']) + '.html#current=' + fullscreen_images[sizeobj['name']][index] +
979                                                       '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
980                                                       img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
981                             html_thumbnails_nojs.gsub!(/~~image_iteration~~/, 
982                                                        '<a href="' + fullscreen_images[sizeobj['name']][index] + '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
983                                                        img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
984                             #- remember in which thumbnails page is this element, for image->thumbnail link
985                             if sizeobj == $images_size[0]
986                                 image2thumbnailpage4js << pagecount
987                             end
988                         end
989
990                         if counter == $N_per_page
991                             html_thumbnails      += close_iterations(iterations)
992                             html_thumbnails_nojs += close_iterations(iterations)
993                             all_pages << [ html_thumbnails, html_thumbnails_nojs ]
994                             html_thumbnails = ''
995                             html_thumbnails_nojs = ''
996                             counter = 0
997                             reset_iterations(iterations)
998                             pagecount += 1
999                         end
1000                     end
1001                 end
1002                 if counter > 0
1003                     html_thumbnails      += close_iterations(iterations)
1004                     html_thumbnails_nojs += close_iterations(iterations)
1005                     all_pages << [ html_thumbnails, html_thumbnails_nojs ]
1006                 end
1007                 for i in html
1008                     i.gsub!(/~~theme~~/, $theme)
1009                     i.gsub!(/~~current_size~~/, sizeobj['name'])
1010                     i.gsub!(/~~current_size_js~~/, size2js(sizeobj['name']))
1011                     i.gsub!(/~~madewith~~/, $madewith || '')
1012                     i.gsub!(/~~indexlink~~/, $indexlink || '')
1013                 end
1014                 html_nojs = html.collect { |l| l.clone }
1015                 pagecount = 0
1016                 for page in all_pages
1017                     html_thumbnails, html_thumbnails_nojs = page
1018                     final_html = html.collect { |l| l.clone }
1019                     mstuff = utf8(_("Pages: %s") % (pagecount > 0 ? "<a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{pagecount - 1}.html\">" + _("<- Previous") + "</a> " : '') +
1020                                                    all_pages.collect_with_index { |p,idx| page == p ? idx + 1 : "<a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{idx}.html\">#{idx + 1}</a>" }.join(', ') +
1021                                                    (pagecount < all_pages.size - 1 ? " <a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{pagecount + 1}.html\">" + _("Next ->") + "</a> " : ''))
1022                     for i in final_html
1023                         i.sub!(/~~run_slideshow~~/,
1024                                images.size <= 1 ? '' : '<a href="image-' + size2js(sizeobj['name']) + '.html#run_slideshow=1">' + utf8(_("Run slideshow!"))+'</a>')
1025                         i.sub!(/~~thumbnails~~/, html_thumbnails)
1026                         if all_pages.size == 1
1027                             i.gsub!(/~~ifmultiplepages\?~~.*~~fi~~/, '')
1028                         else
1029                             i.gsub!(/~~ifmultiplepages\?~~(.+?)~~fi~~/) { $1 }
1030                             i.gsub!(/~~multiplepagesstuff~~/, mstuff.gsub('%nojs', ''))
1031                         end
1032                         substitute_html_sizes(i, sizeobj, 'thumbnails', "-#{pagecount}")
1033                     end
1034                     ios = File.open("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}-#{pagecount}.html", "w")
1035                     ios.write(final_html)
1036                     ios.close
1037                     final_html_nojs = html_nojs.collect { |l| l.clone }
1038                     for i in final_html_nojs
1039                         i.sub!(/~~run_slideshow~~/, utf8(_("<i>Click on an image to view it larger</i>")))
1040                         i.sub!(/~~thumbnails~~/, html_thumbnails_nojs)
1041                         if all_pages.size == 1
1042                             i.gsub!(/~~ifmultiplepages\?~~.*~~fi~~/, '')
1043                         else
1044                             i.gsub!(/~~ifmultiplepages\?~~(.+?)~~fi~~/) { $1 }
1045                             i.gsub!(/~~multiplepagesstuff~~/, mstuff.gsub('%nojs', '-nojs'))
1046                         end
1047                         substitute_html_sizes(i, sizeobj, 'thumbnails', "-nojs-#{pagecount}")
1048                     end
1049                     ios = File.open("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}-nojs-#{pagecount}.html", "w")
1050                     ios.write(final_html_nojs)
1051                     ios.close
1052                     pagecount += 1
1053                 end
1054             end
1055
1056             info("finished processing sizes")
1057
1058             #- generate "main" thumbnails.html page that will reload to correct size thanks to cookie
1059             ios = File.open("#{dest_dir}/thumbnails.html", "w")
1060             ios.write(html_reload_to_thumbnails)
1061             ios.close
1062
1063             #- generate image.html (page with fullscreen images)
1064             if images.size > 0
1065                 captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\"') + '"' : '""' }.join(', ')
1066                 thumbnailspage4js = image2thumbnailpage4js.collect { |e| "\"#{e}\"" }.join(', ')
1067                 for sizeobj in $images_size
1068                     html = $html_images.collect { |l| l.clone }
1069                     images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
1070                     otherimages4js = ''
1071                     othersizes = []
1072                     for sizeobj2 in all_images_sizes
1073                         if sizeobj != sizeobj2
1074                             otherimages4js += "var images_#{size2js(sizeobj2['name'])} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
1075                             othersizes << "\"#{size2js(sizeobj2['name'])}\""
1076                         end
1077                     end
1078                     for i in html
1079                         i.gsub!(/~~images~~/, images4js)
1080                         i.gsub!(/~~other_images~~/, otherimages4js)
1081                         i.gsub!(/~~thumbnailspages~~/, thumbnailspage4js)
1082                         i.gsub!(/~~other_sizes~~/, othersizes.join(', '))
1083                         i.gsub!(/~~captions~~/, captions4js)
1084                         i.gsub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
1085                         i.gsub!(/~~thumbnails~~/, '<a href="thumbnails-' + size2js(sizeobj['name']) + '.html" id="thumbnails">' + utf8(_('return to thumbnails')) + '</a>')
1086                         i.gsub!(/~~theme~~/, $theme)
1087                         i.gsub!(/~~current_size~~/, size2js(sizeobj['name']))
1088                         i.gsub!(/~~madewith~~/, $madewith || '')
1089                         i.gsub!(/~~indexlink~~/, $indexlink || '')
1090                         substitute_html_sizes(i, sizeobj, 'image', '')
1091                     end
1092                     ios = File.open("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html", "w")
1093                     ios.write(html)
1094                     ios.close
1095                 end
1096             end
1097         end
1098     }
1099
1100     msg 3, ''
1101
1102     #- add attributes to <dir..> elements needing so
1103     if $mode != 'use_config'
1104         msg 3, _("\tfixating configuration file...")
1105         $xmldoc.elements.each('//dir') { |element|
1106             path = captionpath = element.attributes['path']
1107             descendant_element = element.elements['descendant::image'] || element.elements['descendant::video']
1108             if !descendant_element
1109                 msg 3, _("\t\tremoving %s, no element in it") % path
1110                 element.remove  #- means we have a directory with nothing interesting in it
1111             else
1112                 captionfile = "#{descendant_element.parent.attributes['path']}/#{descendant_element.attributes['filename']}"
1113                 basename = File.basename(path)
1114                 if element.elements['dir']
1115                     if !element.attributes['subdirs-caption']
1116                         element.add_attribute('subdirs-caption', basename)
1117                     end
1118                     if !element.attributes['subdirs-captionfile']
1119                         element.add_attribute('subdirs-captionfile', captionfile)
1120                     end
1121                 end
1122                 if element.child_byname_notattr('image', 'deleted') || element.child_byname_notattr('video', 'deleted')
1123                     if !element.attributes['thumbnails-caption']
1124                         element.add_attribute('thumbnails-caption', basename)
1125                     end
1126                     if !element.attributes['thumbnails-captionfile']
1127                         element.add_attribute('thumbnails-captionfile', captionfile)
1128                     end
1129                 end
1130             end
1131         }
1132     end
1133
1134     #- write down to disk config if necessary
1135     if $config_writeto
1136         ios = File.open($config_writeto, "w")
1137         $xmldoc.write(ios, 0)
1138         ios.close
1139     end
1140
1141     if $forgui
1142         msg 3, _(" completed necessary stuff for GUI, exiting.")
1143         return
1144     end
1145
1146     #- second pass to create index.html files and previous/next links
1147     info("creating index.html")
1148     msg 3, _("\trescanning directories to generate all 'index.html' files...")
1149
1150     #- recompute the memoization because elements mights have been removed (the ones with no element in them)
1151     optxpath = {}
1152     $xmldoc.elements.each('//dir') { |elem|
1153         optxpath[elem.attributes['path']] = elem
1154     }
1155
1156     examined_dirs.each { |dir|
1157         info("index.html: #{dir}|#{$source}")
1158
1159         xmldir = optxpath[utf8(dir)]
1160         if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
1161             next
1162         end
1163         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
1164
1165         previous_album = find_previous_album(xmldir)
1166         next_album = find_next_album(xmldir)
1167
1168         if xmldir.elements['dir']
1169             html = $html_index.collect { |l| l.clone }
1170             iterations = {}
1171             for i in html
1172                 caption = xmldir.attributes['subdirs-caption']
1173                 i.gsub!(/~~title~~/, caption)
1174                 if xmldir.parent.name == 'dir'
1175                     nav = ''
1176                     path = '..'
1177                     parent = xmldir.parent
1178                     while parent.name == 'dir'
1179                         parentcaption = parent.attributes['subdirs-caption']
1180                         nav = "<a href=\"#{path}/index.html\">#{parentcaption}</a> #{utf8(_(" > "))} #{nav}"
1181                         path += '/..'
1182                         parent = parent.parent
1183                     end
1184                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
1185                     i.gsub!(/~~navigation~~/, nav + caption)
1186                 else
1187                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
1188                 end
1189                 discover_iterations(iterations, i)
1190             end
1191             
1192             html_index = ''
1193             reset_iterations(iterations)
1194             
1195             #- deal with "current" album (directs to "thumbnails" page)
1196             if xmldir.attributes['thumbnails-caption']
1197                 thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
1198                 gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
1199                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
1200                 html_index += run_iterations(iterations, 1)
1201                 html_index.gsub!(/~~image_iteration~~/, "<a href=\"thumbnails.html\">" + img_element(thumbnail) + '</a>')
1202                 html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
1203             end
1204
1205             #- deal with sub-albums (direct to subdirs/index.html pages)
1206             xmldir.elements.each('dir') { |child|
1207                 if child.attributes['deleted']
1208                     next
1209                 end
1210                 subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
1211                 thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
1212                 html_index += run_iterations(iterations, 1)
1213                 captionfile, caption = find_subalbum_caption_info(child)
1214                 gen_thumbnails_subdir(captionfile, child, false,
1215                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
1216                 html_index.gsub!(/~~caption_iteration~~/, caption)
1217                 html_index.gsub!(/~~image_iteration~~/, "<a href=\"#{subdir}/index.html\"'>" + img_element(thumbnail) + '</a>')
1218             }
1219
1220             html_index += close_iterations(iterations)
1221
1222             for i in html
1223                 i.gsub!(/~~thumbnails~~/, html_index)
1224                 i.gsub!(/~~madewith~~/, $madewith || '')
1225                 i.gsub!(/~~indexlink~~/, $indexlink || '')
1226             end
1227             
1228         else
1229             html = html_reload_to_thumbnails
1230         end
1231
1232         ios = File.open("#{dest_dir}/index.html", "w")
1233         ios.write(html)
1234         ios.close
1235
1236         #- substitute multiple "return to albums", previous/next correctly
1237         #- the following two statements are dramatical optimizations to executing for each substInFile callback
1238         dirpresent = xmldir.elements['dir']
1239         parentname = xmldir.parent.name
1240         if xmldir.child_byname_notattr('image', 'deleted') || xmldir.child_byname_notattr('video', 'deleted')
1241             for suffix in [ '', '-nojs' ]
1242                 for sizeobj in $images_size
1243                     Dir.glob("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}#{suffix}-*.html") do |file|
1244                         substInFile(file) { |line|
1245                             sub_previous_next_album(previous_album, next_album, line)
1246                             if dirpresent
1247                                 line.sub!(/~~return_to_albums~~/, '<a href="index.html">' + utf8(_('return to albums')) + '</a>')
1248                             else
1249                                 if parentname == 'dir'
1250                                     line.sub!(/~~return_to_albums~~/, '<a href="../index.html">' + utf8(_('return to albums')) + '</a>')
1251                                 else
1252                                     line.sub!(/~~return_to_albums~~/, '')
1253                                 end
1254                             end
1255                             line
1256                         }
1257                         if suffix == '' && xmldir.child_byname_notattr('image', 'deleted')
1258                             substInFile("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html") { |line|
1259                                 sub_previous_next_album(previous_album, next_album, line)
1260                             }
1261                         end
1262                     end
1263                 end
1264             end
1265         end
1266     }
1267
1268     msg 3, _(" all done.")
1269 end
1270
1271 handle_options
1272 read_config
1273 check_installation
1274
1275 build_html_skeletons
1276
1277 walk_source_dir