add popup menu on elements. provide a way to flip and swap colors for videos
[booh] / lib / booh / booh-lib.rb
1 #!/usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 require 'iconv'
23 require 'timeout'
24
25 require 'gettext'
26 include GetText
27 bindtextdomain("booh")
28
29 require 'booh/config.rb'
30 require 'booh/version.rb'
31
32 module Booh
33     $verbose_level = 2
34     $CURRENT_CHARSET = `locale charmap`.chomp
35     $convert = 'convert -interlace line +profile "*"'
36
37     def utf8(string)
38         return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string).to_s
39     end
40     
41     def from_utf8(string)
42         return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
43     end
44
45     def make_dest_filename(orig_filename)
46         #- we remove non alphanumeric characters but need to do that
47         #- cleverly to not end up with two similar dest filenames. we won't
48         #- urlencode because urldecode might happen in the browser.
49         return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("%2X", v) }.to_s
50     end
51
52     def msg(verbose_level, msg)
53         if verbose_level <= $verbose_level
54             if verbose_level == 0
55                 warn _("\t***ERROR***: %s\n") % msg
56             elsif verbose_level == 1
57                 warn _("\tWarning: %s\n") % msg
58             else
59                 puts msg
60             end
61         end
62     end
63
64     def msg_(verbose_level, msg)
65         if verbose_level <= $verbose_level
66             if verbose_level == 0
67                 warn _("\t***ERROR***: %s") % msg
68             elsif verbose_level == 1
69                 warn _("\tWarning: %s") % msg
70             else
71                 print msg
72             end
73         end
74     end
75
76     def die(msg)
77         puts msg
78         exit 1
79     end
80
81     def select_theme(name)
82         $theme = name
83         msg 3, _("Selecting theme `%s'") % $theme
84         themedir = "#{$FPATH}/themes/#{$theme}"
85         if !File.directory?(themedir)
86             die _("Theme was not found (tried %s directory).") % themedir
87         end
88         require "#{themedir}/parameters.rb"
89     end
90
91     def entry2type(entry)
92         if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i
93             return 'image'
94         elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i
95             #- might consider using file magic later..
96             return 'video'
97         else
98             return nil
99         end
100     end
101
102     def sys(cmd)
103         msg 2, cmd
104         system(cmd)
105     end
106
107     #- parallelizable sys
108     def psys(cmd)
109         if $mproc
110             if pid = fork
111                 $pids << pid
112             else
113                 msg 2, cmd + ' &'
114                 system(cmd)
115                 exit 0
116             end
117             if $pids.length == $mproc
118                 finished = Process.wait2
119                 $pids.delete(finished[0])
120                 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
121             end
122         else
123             sys(cmd)
124         end
125     end
126
127     #- grab the results of a command but don't sleep forever on a runaway process
128     def subproc_runaway_aware(command)
129         begin
130             timeout(5) {
131                 return `#{command}`
132             }
133         rescue Timeout::Error    
134             msg 1, _("forgetting runaway process (transcode sucks again?)")
135             #- todo should slay transcode but dunno how to do that
136             return nil
137         end
138     end
139
140     def get_image_size(fullpath)
141         if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
142             return { :x => $1, :y => $2 }
143         else
144             return nil
145         end
146     end
147
148     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
149     def commify(n)
150         n.to_s =~ /([^\.]*)(\..*)?/
151         int, dec = $1.reverse, $2 ? $2 : ""
152         while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
153         end
154         int.reverse + dec
155     end
156
157     def guess_rotate(filename)
158         orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
159         if orientation =~ /right - top/
160             angle = 90
161         elsif orientation =~ /left - bottom/
162             angle = -90
163         else
164             return 0
165         end
166         size = get_image_size(filename)
167         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
168         if size && size[:x] < size[:y]
169             return 0
170         else
171             return angle
172         end
173     end
174
175     def rotate_pixbuf(pixbuf, angle)
176         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
177                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
178                              angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
179                                             Gdk::Pixbuf::ROTATE_NONE)
180     end
181
182     def gen_thumbnails_element(orig, xmldir, allow_background, dests)
183         gen_thumbnails(orig, xmldir, allow_background, dests, xmldir.elements["[@filename='#{utf8(File.basename(orig))}']"], '')
184     end
185
186     def gen_thumbnails_subdir(orig, xmldir, allow_background, dests, type)
187         #- type can be `subdirs' or `thumbnails' 
188         gen_thumbnails(orig, xmldir, allow_background, dests, xmldir, type + '-')
189     end
190
191     def gen_thumbnails(orig, xmldir, allow_background, dests, felem, attributes_prefix)
192         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
193             return true
194         end
195
196         convert_options = ''
197
198         if entry2type(orig) == 'image'
199             if felem
200                 rotate = felem.attributes["#{attributes_prefix}rotate"]
201                 if !rotate
202                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
203                 end
204                 convert_options += "-rotate #{rotate} "
205             end
206             for dest in dests
207                 if !File.exists?(dest['filename'])
208                     cmd = "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
209                     if allow_background
210                         psys(cmd)
211                     else
212                         sys(cmd)
213                     end
214                 end
215             end
216             return true
217
218         elsif entry2type(orig) == 'video'
219             dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
220             if felem
221                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
222                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
223                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
224                     convert_options += "-rotate #{rotate} "
225                 end
226             end
227             frame_offset = (frame_offset || 5).to_i
228             for dest in dests
229                 if !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
230                     transcode_options = ''
231                     if felem
232                         if felem.attributes["#{attributes_prefix}color-swap"]
233                             transcode_options += '-k '
234                         end
235                     end
236                     cmd = "transcode -a 0 -c #{frame_offset-5}-#{frame_offset} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
237                     msg 2, cmd
238                     if subproc_runaway_aware(cmd) =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
239                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
240                         cmd = "mencoder '#{orig}' -nosound -ovc lavc -lavcopts vcodec=mjpeg -o '#{dest_dir}/foo.avi' -frames #{frame_offset} -fps 25 >/dev/null 2>/dev/null"
241                         msg 2, cmd
242                         system cmd
243                         if File.exists?("#{dest_dir}/foo.avi")
244                             cmd = "transcode -a 0 -c #{frame_offset-5}-#{frame_offset} -i '#{dest_dir}/foo.avi' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
245                             msg 2, cmd
246                             results = subproc_runaway_aware(cmd)
247                             system("rm -f '#{dest_dir}/foo.avi'")
248                             if results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
249                                 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/foo.avi"
250                                 return false
251                             end
252                         else
253                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
254                             return false
255                         end
256                     end
257
258                 end
259                 sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} #{dest_dir}/screenshot.jpg000004.jpg '#{dest['filename']}'")
260             end
261             return true
262         end
263     end
264
265     def invornil(obj, methodname)
266         if obj == nil
267             return nil
268         else
269             return obj.method(methodname).call
270         end
271     end
272
273     def find_subalbum_info_type(xmldir)
274         #- first look for subdirs info; if not, means there is no subdir
275         if xmldir.attributes['subdirs-caption']
276             return 'subdirs'
277         else
278             return 'thumbnails'
279         end
280     end
281
282     def find_subalbum_caption_info(xmldir)
283         type = find_subalbum_info_type(xmldir)
284         return from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"]
285     end
286 end