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