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