move check for gimp and firefox to when needed, to allow soft require
[booh] / lib / booh / booh-lib.rb
1 #                         *  BOOH  *
2 #
3 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
4 #
5 # The acronyn sucks, however this is a tribute to Dragon Ball by
6 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
7 # Ball is named "Boo". But there was already a free software project
8 # called Boo, so this one will be it "Booh". Or whatever.
9 #
10 #
11 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
12 #
13 # This software may be freely redistributed under the terms of the GNU
14 # public license version 2.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
20 require 'iconv'
21 require 'timeout'
22 require 'tempfile'
23
24 require 'booh/rexml/document'
25
26 require 'gettext'
27 include GetText
28 bindtextdomain("booh")
29
30 require 'booh/config.rb'
31 require 'booh/version.rb'
32 begin
33     require 'gtk2'
34 rescue LoadError
35     $no_gtk2 = true
36 end
37 begin
38     require 'booh/libadds'
39 rescue LoadError
40     $no_libadds = true
41 end
42
43 module Booh
44     $verbose_level = 2
45     $CURRENT_CHARSET = `locale charmap`.chomp
46     #- check charset availability. a locale configuration of C or POSIX yields the unsupported 'ANSI_X3.4-1968'.
47     begin
48         REXML::XMLDecl.new(REXML::XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
49     rescue
50         $CURRENT_CHARSET = 'UTF-8'
51     end
52     $convert = 'convert -interlace line +profile "*"'
53     $convert_enhance = '-contrast -enhance -normalize'
54
55     def utf8(string)
56         return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string).to_s
57     end
58
59     def utf8cut(string, maxlen)
60         begin
61             return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string[0..maxlen-1]).to_s
62         rescue Iconv::InvalidCharacter
63             return utf8cut(string, maxlen-1)
64         end
65     end
66
67     def sizename(key, translate)
68         #- fake for gettext to find these; if themes need more sizes, english name for them should be added here
69         sizenames = { 'small' => N_("small"), 'medium' => N_("medium"), 'large' => N_("large"),
70                       'x-large' => N_("x-large"), 'xx-large' => N_("xx-large"),
71                       'original' => N_("original") }
72         sizename = sizenames[key] || key
73         if translate
74             return utf8(_(sizename))
75         else
76             return sizename
77         end
78     end
79
80     SUPPORTED_LANGUAGES = %w(en de fr ja eo)
81
82     def langname(lang)
83         langnames = { 'en' => _("english"), 'de' => _("german"), 'fr' => _("french"), 'ja' => _("japanese"), 'eo' => _("esperanto") }
84         return langnames[lang]
85     end
86     
87     def from_utf8(string)
88         return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
89     end
90
91     def from_utf8_safe(string)
92         begin
93             return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
94         rescue Iconv::IllegalSequence
95             return ''
96         end
97     end
98
99     def make_dest_filename_old(orig_filename)
100         #- we remove non alphanumeric characters but need to do that
101         #- cleverly to not end up with two similar dest filenames. we won't
102         #- urlencode because urldecode might happen in the browser.
103         return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("%2X", v) }.to_s
104     end
105
106     def make_dest_filename(orig_filename)
107         #- we remove non alphanumeric characters but need to do that
108         #- cleverly to not end up with two similar dest filenames. we won't
109         #- urlencode because urldecode might happen in the browser.
110         return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("~%02X", v) }.to_s
111     end
112
113     def msg(verbose_level, msg)
114         if verbose_level <= $verbose_level
115             if verbose_level == 0
116                 warn _("\t***ERROR***: %s\n") % msg
117             elsif verbose_level == 1
118                 warn _("\tWarning: %s\n") % msg
119             else
120                 puts msg
121             end
122         end
123     end
124
125     def msg_(verbose_level, msg)
126         if verbose_level <= $verbose_level
127             if verbose_level == 0
128                 warn _("\t***ERROR***: %s") % msg
129             elsif verbose_level == 1
130                 warn _("\tWarning: %s") % msg
131             else
132                 print msg
133             end
134         end
135     end
136
137     def die_(msg)
138         puts msg
139         exit 1
140     end
141
142     def select_theme(name, limit_sizes, optimizefor32, nperrow)
143         $theme = name
144         msg 3, _("Selecting theme '%s'") % $theme
145         $themedir = "#{$FPATH}/themes/#{$theme}"
146         if !File.directory?($themedir)
147             themedir2 = File.expand_path("~/.booh-themes/#{$theme}")
148             if !File.directory?(themedir2)
149                 die_ _("Theme was not found (tried %s and %s directories).") % [ $themedir, themedir2 ]
150             end
151             $themedir = themedir2
152         end
153         eval File.open("#{$themedir}/metadata/parameters.rb").readlines.join
154
155         if limit_sizes
156             if limit_sizes != 'all'
157                 sizes = limit_sizes.split(/,/)
158                 $images_size = $images_size.find_all { |e| sizes.include?(e['name']) }
159                 if $images_size.length == 0
160                     die_ _("Can't carry on, no valid size selected.")
161                 end
162             end
163         else
164             $images_size = $images_size.find_all { |e| !e['optional'] }
165         end
166
167         if optimizefor32
168             $images_size.each { |e|
169                 e['fullscreen'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
170                 e['thumbnails'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
171             }
172             $albums_thumbnail_size.gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
173         end
174
175         if nperrow && nperrow != $default_N
176             ratio = nperrow.to_f / $default_N.to_f
177             $images_size.each { |e|
178                 e['thumbnails'].gsub!(/(\d+)x(\d+)/) { ($1.to_f/ratio).to_i.to_s + 'x' + ($2.to_f/ratio).to_i.to_s }
179             }
180         end
181
182         $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
183         if $default_size == nil
184             $default_size = $images_size[0]
185         end
186     end
187
188     def entry2type(entry)
189         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-bmp.so
190         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-gif.so
191         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-ico.so
192         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-jpeg.so
193         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-png.so
194         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-pnm.so
195         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-ras.so
196         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-tiff.so
197         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-xbm.so
198         #- /usr/lib/gdk-pixbuf/loaders/libpixbufloader-xpm.so
199         if entry =~ /\.(bmp|gif|ico|jpg|jpe|png|pnm|tif|xbm|xpm)$/i && entry !~ /['"\[\]]/
200             return 'image'
201         elsif !$ignore_videos && entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx|3gp|mp4)$/i && entry !~ /['"\[\]]/
202             #- might consider using file magic later..
203             return 'video'
204         else
205             return nil
206         end
207     end
208
209     def sys(cmd)
210         msg 2, cmd
211         system(cmd)
212     end
213
214     def waitjob
215         finished = Process.wait2
216         $pids.delete(finished[0])
217         $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
218     end
219
220     def waitjobs
221         while $pids && $pids.length > 0
222             waitjob
223         end
224     end
225
226     #- parallelizable sys
227     def psys(cmd)
228         if $mproc
229             if pid = fork
230                 $pids << pid
231             else
232                 msg 2, cmd + ' &'
233                 system(cmd)
234                 exit 0
235             end
236             if $pids.length == $mproc
237                 waitjob
238             end
239         else
240             sys(cmd)
241         end
242     end
243
244     def get_image_size(fullpath)
245         if !$no_identify
246             if $sizes_cache.nil?
247                 $sizes_cache = {}
248             end
249             if $sizes_cache[fullpath].nil?
250                 #- identify is slow, try with gdk if available (negligible vs 35ms)
251                 if $no_gtk2
252                     if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
253                         $sizes_cache[fullpath] = { :x => $1.to_i, :y => $2.to_i }
254                     end
255                 else
256                     format, width, height = Gdk::Pixbuf.get_file_info(fullpath)
257                     if width
258                         $sizes_cache[fullpath] = { :x => width, :y => height }
259                     end
260                 end
261             end
262             return $sizes_cache[fullpath]
263         else
264             return nil
265         end
266     end
267
268     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
269     def commify(n)
270         n.to_s =~ /([^\.]*)(\..*)?/
271         int, dec = $1.reverse, $2 ? $2 : ""
272         sep = _(",")
273         while int.gsub!(/(#{Regexp.quote(sep)}|\.|^)(\d{3})(\d)/, '\1\2' + sep + '\3')
274         end
275         int.reverse + dec
276     end
277
278     def guess_rotate(filename)
279         #- identify is slow, try with libexiv2 if available (4ms vs 35ms)
280         if $no_libadds
281             if $no_identify
282                 return 0
283             end
284             orientation = `identify -format "%[EXIF:orientation]" '#{filename}'`.chomp.to_i
285         else
286             orientation = Exif.orientation(filename)
287         end
288
289         if orientation == 6
290             angle = 90
291         elsif orientation == 8
292             angle = -90
293         else
294             return 0
295         end
296
297         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
298         size = get_image_size(filename)
299         if size && size[:x] < size[:y]
300             return 0
301         else
302             return angle
303         end
304     end
305
306     def angle_to_exif_orientation(angle)
307         if angle == 90
308             return 6
309         elsif angle == 270 || angle == -90
310             return 8
311         else
312             return 0
313         end
314     end
315
316     def rotate_pixbuf(pixbuf, angle)
317         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
318                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
319                              (angle == 270 || angle == -90) ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
320                                             Gdk::Pixbuf::ROTATE_NONE)
321     end
322
323     def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
324         if xmldirorelem.name == 'dir'
325             xmldirorelem = xmldirorelem.elements["*[@filename='#{utf8(File.basename(orig))}']"]
326         end
327         gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
328     end
329
330     def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
331         #- type can be `subdirs' or `thumbnails' 
332         gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
333     end
334
335     def gen_video_thumbnail(orig, colorswap, seektime)
336         if colorswap
337             #- ignored for the moment. is mplayer subject to blue faces problem?
338         end
339         #- it's not possible to specify a basename for the output jpeg file with mplayer (file will be named 00000001.jpg); as this can
340         #- be called from multiple threads, we must come up with a unique directory where to put the file
341         tmpfile = Tempfile.new("boohvideotmp")
342         Thread.critical = true
343         tmpdirname = tmpfile.path
344         tmpfile.close!
345         begin
346             Dir.mkdir(tmpdirname)
347         rescue Errno::EEXIST
348             raise "Tmp directory #{tmpdirname} already exists"
349         ensure
350             Thread.critical = false
351         end
352         cmd = "mplayer '#{orig}' -nosound -vo jpeg:outdir='#{tmpdirname}' -frames 1 -ss #{seektime} -slave >/dev/null 2>/dev/null"
353         sys(cmd)
354         if ! File.exists?("#{tmpdirname}/00000001.jpg")
355             msg 0, _("specified seektime too large? that may also be another probleme. try another value.")
356             Dir.rmdir(tmpdirname)
357             return nil
358         end
359         return tmpdirname
360     end
361
362     def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
363         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
364             return true
365         end
366
367         convert_options = ''
368         dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
369
370         if entry2type(orig) == 'image'
371             if felem
372                 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
373                     neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
374                     cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
375                     sys(cmd)
376                     if File.exists?(neworig)
377                         orig = neworig
378                     end
379                 end
380                 if gammacorrect = felem.attributes["#{attributes_prefix}gamma-correction"]
381                     neworig = "#{dest_dir}/#{File.basename(orig)}-gammacorrect#{gammacorrect}.jpg"
382                     cmd = "booh-gamma-correction '#{orig}' '#{neworig}' #{gammacorrect}"
383                     sys(cmd)
384                     if File.exists?(neworig)
385                         orig = neworig
386                     end
387                 end
388                 rotate = felem.attributes["#{attributes_prefix}rotate"]
389                 if !rotate
390                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_s)
391                 end
392                 convert_options += "-rotate #{rotate} "
393                 if felem.attributes["#{attributes_prefix}enhance"]
394                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
395                 end
396             end
397             for dest in dests
398                 if !File.exists?(dest['filename'])
399                     cmd = nil
400                     cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize '#{dest['size']}>' '#{orig}' '#{dest['filename']}'"
401                     if allow_background
402                         psys(cmd)
403                     else
404                         sys(cmd)
405                     end
406                 end
407             end
408             if neworig
409                 if allow_background
410                     waitjobs
411                 end
412                 begin
413                     File.delete(neworig)
414                 rescue Errno::ENOENT
415                     #- can happen on race conditions for generating multiple times a thumbnail for a given image. for the moment,
416                     #- silently ignore, it is not a so big deal.
417                 end
418             end
419             return true
420
421         elsif entry2type(orig) == 'video'
422             if felem
423                 #- seektime is an attribute that allows to specify where the frame to use for the thumbnail must be taken
424                 seektime = felem.attributes["#{attributes_prefix}seektime"]
425                 if ! seektime
426                     felem.add_attribute("#{attributes_prefix}seektime", seektime = "0")
427                 end
428                 seektime = seektime.to_f
429                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
430                     convert_options += "-rotate #{rotate} "
431                 end
432                 if felem.attributes["#{attributes_prefix}enhance"]
433                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
434                 end
435             end
436             for dest in dests
437                 if ! File.exists?(dest['filename'])
438                     tmpdir = gen_video_thumbnail(orig, felem && felem.attributes["#{attributes_prefix}color-swap"], seektime)
439                     if tmpdir.nil?
440                         return false
441                     end
442                     tmpfile = "#{tmpdir}/00000001.jpg"
443                     alltmpfiles = [ tmpfile ]
444                     if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
445                         if whitebalance.to_f != 0
446                             neworig = "#{tmpdir}/whitebalance#{whitebalance}.jpg"
447                             cmd = "booh-fix-whitebalance '#{tmpfile}' '#{neworig}' #{whitebalance}"
448                             sys(cmd)
449                             if File.exists?(neworig)
450                                 tmpfile = neworig
451                                 alltmpfiles << neworig
452                             end
453                         end
454                     end
455                     if felem && gammacorrect = felem.attributes["#{attributes_prefix}gamma-correction"]
456                         if gammacorrect.to_f != 0
457                             neworig = "#{tmpdir}/gammacorrect#{gammacorrect}.jpg"
458                             cmd = "booh-gamma-correction '#{tmpfile}' '#{neworig}' #{gammacorrect}"
459                             sys(cmd)
460                             if File.exists?(neworig)
461                                 tmpfile = neworig
462                                 alltmpfiles << neworig
463                             end
464                         end
465                     end
466                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{tmpfile}' '#{dest['filename']}'")
467                     alltmpfiles.each { |file| File.delete(file) }
468                     Dir.rmdir(tmpdir)
469                 end
470             end
471             return true
472         end
473     end
474
475     def invornil(obj, methodname)
476         if obj == nil
477             return nil
478         else
479             return obj.method(methodname).call
480         end
481     end
482
483     def find_subalbum_info_type(xmldir)
484         #- first look for subdirs info; if not, means there is no subdir
485         if xmldir.attributes['subdirs-caption']
486             return 'subdirs'
487         else
488             return 'thumbnails'
489         end
490     end
491
492     def find_subalbum_caption_info(xmldir)
493         type = find_subalbum_info_type(xmldir)
494         return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
495     end
496
497     def file_size(path)
498         begin
499             return File.size(path)
500         rescue
501             return -1
502         end
503     end
504
505     def max(a, b)
506         a > b ? a : b
507     end
508
509     def clamp(n, a, b)
510         n < a ? a : n > b ? b : n
511     end
512
513     def pano_amount(elem)
514         if pano_amount = elem.attributes['pano-amount']
515             if $N_per_row
516                 return clamp(pano_amount.to_f, 1, $N_per_row.to_i)
517             else
518                 return clamp(pano_amount.to_f, 1, $default_N.to_i)
519             end
520         else
521             return nil
522         end
523     end
524
525     def substInFile(name)
526         newcontent = IO.readlines(name).collect { |l| yield l }
527         ios = File.open(name, "w")
528         ios.write(newcontent)
529         ios.close
530     end
531
532     def check_browser
533         browser_binary = $config['browser'].split.first
534         if browser_binary && !File.executable?(browser_binary)
535             show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
536 You should fix this in Edit/Preferences so that you can open URLs.
537
538 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
539             return false
540         else
541             return true
542         end
543     end
544
545     def open_url(url)
546         if check_browser
547             cmd = $config['browser'].gsub('%f', "'#{url}'") + ' &'
548             msg 2, cmd
549             system(cmd)
550         end
551     end
552
553     def get_license
554         return <<"EOF"
555                     GNU GENERAL PUBLIC LICENSE
556                        Version 2, June 1991
557
558  Copyright (C) 1989, 1991 Free Software Foundation, Inc.
559                           675 Mass Ave, Cambridge, MA 02139, USA
560  Everyone is permitted to copy and distribute verbatim copies
561  of this license document, but changing it is not allowed.
562
563                             Preamble
564
565   The licenses for most software are designed to take away your
566 freedom to share and change it.  By contrast, the GNU General Public
567 License is intended to guarantee your freedom to share and change free
568 software--to make sure the software is free for all its users.  This
569 General Public License applies to most of the Free Software
570 Foundation's software and to any other program whose authors commit to
571 using it.  (Some other Free Software Foundation software is covered by
572 the GNU Library General Public License instead.)  You can apply it to
573 your programs, too.
574
575   When we speak of free software, we are referring to freedom, not
576 price.  Our General Public Licenses are designed to make sure that you
577 have the freedom to distribute copies of free software (and charge for
578 this service if you wish), that you receive source code or can get it
579 if you want it, that you can change the software or use pieces of it
580 in new free programs; and that you know you can do these things.
581
582   To protect your rights, we need to make restrictions that forbid
583 anyone to deny you these rights or to ask you to surrender the rights.
584 These restrictions translate to certain responsibilities for you if you
585 distribute copies of the software, or if you modify it.
586
587   For example, if you distribute copies of such a program, whether
588 gratis or for a fee, you must give the recipients all the rights that
589 you have.  You must make sure that they, too, receive or can get the
590 source code.  And you must show them these terms so they know their
591 rights.
592
593   We protect your rights with two steps: (1) copyright the software, and
594 (2) offer you this license which gives you legal permission to copy,
595 distribute and/or modify the software.
596
597   Also, for each author's protection and ours, we want to make certain
598 that everyone understands that there is no warranty for this free
599 software.  If the software is modified by someone else and passed on, we
600 want its recipients to know that what they have is not the original, so
601 that any problems introduced by others will not reflect on the original
602 authors' reputations.
603
604   Finally, any free program is threatened constantly by software
605 patents.  We wish to avoid the danger that redistributors of a free
606 program will individually obtain patent licenses, in effect making the
607 program proprietary.  To prevent this, we have made it clear that any
608 patent must be licensed for everyone's free use or not licensed at all.
609
610   The precise terms and conditions for copying, distribution and
611 modification follow.
612
613
614                     GNU GENERAL PUBLIC LICENSE
615    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
616
617   0. This License applies to any program or other work which contains
618 a notice placed by the copyright holder saying it may be distributed
619 under the terms of this General Public License.  The "Program", below,
620 refers to any such program or work, and a "work based on the Program"
621 means either the Program or any derivative work under copyright law:
622 that is to say, a work containing the Program or a portion of it,
623 either verbatim or with modifications and/or translated into another
624 language.  (Hereinafter, translation is included without limitation in
625 the term "modification".)  Each licensee is addressed as "you".
626
627 Activities other than copying, distribution and modification are not
628 covered by this License; they are outside its scope.  The act of
629 running the Program is not restricted, and the output from the Program
630 is covered only if its contents constitute a work based on the
631 Program (independent of having been made by running the Program).
632 Whether that is true depends on what the Program does.
633
634   1. You may copy and distribute verbatim copies of the Program's
635 source code as you receive it, in any medium, provided that you
636 conspicuously and appropriately publish on each copy an appropriate
637 copyright notice and disclaimer of warranty; keep intact all the
638 notices that refer to this License and to the absence of any warranty;
639 and give any other recipients of the Program a copy of this License
640 along with the Program.
641
642 You may charge a fee for the physical act of transferring a copy, and
643 you may at your option offer warranty protection in exchange for a fee.
644
645   2. You may modify your copy or copies of the Program or any portion
646 of it, thus forming a work based on the Program, and copy and
647 distribute such modifications or work under the terms of Section 1
648 above, provided that you also meet all of these conditions:
649
650     a) You must cause the modified files to carry prominent notices
651     stating that you changed the files and the date of any change.
652
653     b) You must cause any work that you distribute or publish, that in
654     whole or in part contains or is derived from the Program or any
655     part thereof, to be licensed as a whole at no charge to all third
656     parties under the terms of this License.
657
658     c) If the modified program normally reads commands interactively
659     when run, you must cause it, when started running for such
660     interactive use in the most ordinary way, to print or display an
661     announcement including an appropriate copyright notice and a
662     notice that there is no warranty (or else, saying that you provide
663     a warranty) and that users may redistribute the program under
664     these conditions, and telling the user how to view a copy of this
665     License.  (Exception: if the Program itself is interactive but
666     does not normally print such an announcement, your work based on
667     the Program is not required to print an announcement.)
668
669
670 These requirements apply to the modified work as a whole.  If
671 identifiable sections of that work are not derived from the Program,
672 and can be reasonably considered independent and separate works in
673 themselves, then this License, and its terms, do not apply to those
674 sections when you distribute them as separate works.  But when you
675 distribute the same sections as part of a whole which is a work based
676 on the Program, the distribution of the whole must be on the terms of
677 this License, whose permissions for other licensees extend to the
678 entire whole, and thus to each and every part regardless of who wrote it.
679
680 Thus, it is not the intent of this section to claim rights or contest
681 your rights to work written entirely by you; rather, the intent is to
682 exercise the right to control the distribution of derivative or
683 collective works based on the Program.
684
685 In addition, mere aggregation of another work not based on the Program
686 with the Program (or with a work based on the Program) on a volume of
687 a storage or distribution medium does not bring the other work under
688 the scope of this License.
689
690   3. You may copy and distribute the Program (or a work based on it,
691 under Section 2) in object code or executable form under the terms of
692 Sections 1 and 2 above provided that you also do one of the following:
693
694     a) Accompany it with the complete corresponding machine-readable
695     source code, which must be distributed under the terms of Sections
696     1 and 2 above on a medium customarily used for software interchange; or,
697
698     b) Accompany it with a written offer, valid for at least three
699     years, to give any third party, for a charge no more than your
700     cost of physically performing source distribution, a complete
701     machine-readable copy of the corresponding source code, to be
702     distributed under the terms of Sections 1 and 2 above on a medium
703     customarily used for software interchange; or,
704
705     c) Accompany it with the information you received as to the offer
706     to distribute corresponding source code.  (This alternative is
707     allowed only for noncommercial distribution and only if you
708     received the program in object code or executable form with such
709     an offer, in accord with Subsection b above.)
710
711 The source code for a work means the preferred form of the work for
712 making modifications to it.  For an executable work, complete source
713 code means all the source code for all modules it contains, plus any
714 associated interface definition files, plus the scripts used to
715 control compilation and installation of the executable.  However, as a
716 special exception, the source code distributed need not include
717 anything that is normally distributed (in either source or binary
718 form) with the major components (compiler, kernel, and so on) of the
719 operating system on which the executable runs, unless that component
720 itself accompanies the executable.
721
722 If distribution of executable or object code is made by offering
723 access to copy from a designated place, then offering equivalent
724 access to copy the source code from the same place counts as
725 distribution of the source code, even though third parties are not
726 compelled to copy the source along with the object code.
727
728
729   4. You may not copy, modify, sublicense, or distribute the Program
730 except as expressly provided under this License.  Any attempt
731 otherwise to copy, modify, sublicense or distribute the Program is
732 void, and will automatically terminate your rights under this License.
733 However, parties who have received copies, or rights, from you under
734 this License will not have their licenses terminated so long as such
735 parties remain in full compliance.
736
737   5. You are not required to accept this License, since you have not
738 signed it.  However, nothing else grants you permission to modify or
739 distribute the Program or its derivative works.  These actions are
740 prohibited by law if you do not accept this License.  Therefore, by
741 modifying or distributing the Program (or any work based on the
742 Program), you indicate your acceptance of this License to do so, and
743 all its terms and conditions for copying, distributing or modifying
744 the Program or works based on it.
745
746   6. Each time you redistribute the Program (or any work based on the
747 Program), the recipient automatically receives a license from the
748 original licensor to copy, distribute or modify the Program subject to
749 these terms and conditions.  You may not impose any further
750 restrictions on the recipients' exercise of the rights granted herein.
751 You are not responsible for enforcing compliance by third parties to
752 this License.
753
754   7. If, as a consequence of a court judgment or allegation of patent
755 infringement or for any other reason (not limited to patent issues),
756 conditions are imposed on you (whether by court order, agreement or
757 otherwise) that contradict the conditions of this License, they do not
758 excuse you from the conditions of this License.  If you cannot
759 distribute so as to satisfy simultaneously your obligations under this
760 License and any other pertinent obligations, then as a consequence you
761 may not distribute the Program at all.  For example, if a patent
762 license would not permit royalty-free redistribution of the Program by
763 all those who receive copies directly or indirectly through you, then
764 the only way you could satisfy both it and this License would be to
765 refrain entirely from distribution of the Program.
766
767 If any portion of this section is held invalid or unenforceable under
768 any particular circumstance, the balance of the section is intended to
769 apply and the section as a whole is intended to apply in other
770 circumstances.
771
772 It is not the purpose of this section to induce you to infringe any
773 patents or other property right claims or to contest validity of any
774 such claims; this section has the sole purpose of protecting the
775 integrity of the free software distribution system, which is
776 implemented by public license practices.  Many people have made
777 generous contributions to the wide range of software distributed
778 through that system in reliance on consistent application of that
779 system; it is up to the author/donor to decide if he or she is willing
780 to distribute software through any other system and a licensee cannot
781 impose that choice.
782
783 This section is intended to make thoroughly clear what is believed to
784 be a consequence of the rest of this License.
785
786
787   8. If the distribution and/or use of the Program is restricted in
788 certain countries either by patents or by copyrighted interfaces, the
789 original copyright holder who places the Program under this License
790 may add an explicit geographical distribution limitation excluding
791 those countries, so that distribution is permitted only in or among
792 countries not thus excluded.  In such case, this License incorporates
793 the limitation as if written in the body of this License.
794
795   9. The Free Software Foundation may publish revised and/or new versions
796 of the General Public License from time to time.  Such new versions will
797 be similar in spirit to the present version, but may differ in detail to
798 address new problems or concerns.
799
800 Each version is given a distinguishing version number.  If the Program
801 specifies a version number of this License which applies to it and "any
802 later version", you have the option of following the terms and conditions
803 either of that version or of any later version published by the Free
804 Software Foundation.  If the Program does not specify a version number of
805 this License, you may choose any version ever published by the Free Software
806 Foundation.
807
808   10. If you wish to incorporate parts of the Program into other free
809 programs whose distribution conditions are different, write to the author
810 to ask for permission.  For software which is copyrighted by the Free
811 Software Foundation, write to the Free Software Foundation; we sometimes
812 make exceptions for this.  Our decision will be guided by the two goals
813 of preserving the free status of all derivatives of our free software and
814 of promoting the sharing and reuse of software generally.
815
816                             NO WARRANTY
817
818   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
819 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
820 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
821 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
822 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
823 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
824 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
825 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
826 REPAIR OR CORRECTION.
827
828   12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
829 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
830 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
831 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
832 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
833 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
834 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
835 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
836 POSSIBILITY OF SUCH DAMAGES.
837 EOF
838     end
839
840     def call_about
841         Gtk::AboutDialog.set_url_hook { |dialog, url| open_url(url) }
842         Gtk::AboutDialog.show($main_window, { :name => 'booh',
843                                               :version => $VERSION,
844                                               :copyright => 'Copyright (c) 2005-2008 Guillaume Cottenceau',
845                                               :license => get_license,
846                                               :website => 'http://booh.org/',
847                                               :authors => [ 'Guillaume Cottenceau' ],
848                                               :artists => [ 'Ayo73' ],
849                                               :comments => utf8(_("''The Web-Album of choice for discriminating Linux users''")),
850                                               :translator_credits => utf8(_('Esperanto: Stephane Fillod
851 Japanese: Masao Mutoh
852 German: Roland Eckert
853 French: Guillaume Cottenceau')),
854                                               :logo => Gdk::Pixbuf.new("#{$FPATH}/images/logo.png") })
855     end
856
857     def smartsort(entries, sort_criterions)
858         #- sort "entries" according to "sort_criterions" but find a good fallback for all entries without a
859         #- criterion value (still next to the item they were next to)
860         sorted_entries = sort_criterions.keys.sort { |a,b| sort_criterions[a] <=> sort_criterions[b] }
861         for i in 0 .. entries.size - 1
862             if ! sorted_entries.include?(entries[i])
863                 j = i - 1
864                 while j > 0 && ! sorted_entries.include?(entries[j])
865                     j -= 1
866                 end
867                 sorted_entries[(sorted_entries.index(entries[j]) || -1 ) + 1, 0] = entries[i]
868             end
869         end
870         return sorted_entries
871     end
872
873     def defer_translation(msg)
874         return "@@#{msg}@@"
875     end
876
877     def create_window
878         w = Gtk::Window.new
879         w.icon_list = [ Gdk::Pixbuf.new("#{$FPATH}/images/booh-16x16.png"),
880                         Gdk::Pixbuf.new("#{$FPATH}/images/booh-32x32.png"),
881                         Gdk::Pixbuf.new("#{$FPATH}/images/booh-48x48.png") ]
882         return w
883     end
884
885 end
886
887 class Object
888     def to_b
889         if !self || self.to_s == 'false'
890             return false
891         else
892             return true
893         end
894     end
895 end
896
897 class File
898     def File.reduce_path(path)
899         return path.gsub(/\w+\/\.\.\//, '')
900     end
901 end
902
903 module Enumerable
904     def collect_with_index
905         out = []
906         each_with_index { |e,i|
907             out << yield(e,i)
908         }
909         return out
910     end
911 end
912
913 class Array
914     def sum
915         retval = 0
916         each { |v| retval += v.to_i }
917         return retval
918     end
919 end
920
921 class REXML::Element
922     def previous_element_byname(name)
923         n = self
924         while n = n.previous_element
925             if n.name == name
926                 return n
927             end
928         end
929         return nil
930     end
931
932     def previous_element_byname_notattr(name, attr)
933         n = self
934         while n = n.previous_element
935             if n.name == name && !n.attributes[attr]
936                 return n
937             end
938         end
939         return nil
940     end
941
942     def next_element_byname(name)
943         n = self
944         while n = n.next_element
945             if n.name == name
946                 return n
947             end
948         end
949         return nil
950     end
951
952     def next_element_byname_notattr(name, attr)
953         n = self
954         while n = n.next_element
955             if n.name == name && !n.attributes[attr]
956                 return n
957             end
958         end
959         return nil
960     end
961
962     def child_byname_notattr(name, attr)
963         elements.each(name) { |element|
964             if !element.attributes[attr]
965                 return element
966             end
967         }
968         return nil
969     end
970 end
971
972