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