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