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