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