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