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