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