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