support rotation (typically, of portrait images that came with no EXIF) from GUI...
[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 __(string, *args)
53         if args.size == 0
54             _(string)
55         elsif args.size == 1
56             sprintf(_(string), args[0])
57         elsif args.size == 2
58             sprintf(_(string), args[0], args[1])
59         end
60     end
61
62     def msg(verbose_level, msg)
63         if verbose_level <= $verbose_level
64             if verbose_level == 0
65                 warn __("\t***ERROR***: %s\n", msg)
66             elsif verbose_level == 1
67                 warn __("\tWarning: %s\n", msg)
68             else
69                 puts msg
70             end
71         end
72     end
73
74     def msg_(verbose_level, msg)
75         if verbose_level <= $verbose_level
76             if verbose_level == 0
77                 warn __("\t***ERROR***: %s", msg)
78             elsif verbose_level == 1
79                 warn __("\tWarning: %s", msg)
80             else
81                 print msg
82             end
83         end
84     end
85
86     def die(msg)
87         puts msg
88         exit 1
89     end
90
91     def select_theme(name)
92         $theme = name
93         msg 3, __("Selecting theme `%s'", $theme)
94         themedir = "#{$FPATH}/themes/#{$theme}"
95         if !File.directory?(themedir)
96             die __("Theme was not found (tried %s directory).", themedir)
97         end
98         require "#{themedir}/parameters.rb"
99     end
100
101     def entry2type(entry)
102         if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i
103             return 'image'
104         elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i
105             #- might consider using file magic later..
106             return 'video'
107         else
108             return nil
109         end
110     end
111
112     def sys(cmd)
113         msg 2, cmd
114         system(cmd)
115     end
116
117     #- parallelizable sys
118     def psys(cmd)
119         if $mproc
120             if pid = fork
121                 $pids << pid
122             else
123                 msg 2, cmd + ' &'
124                 system(cmd)
125                 exit 0
126             end
127             if $pids.length == $mproc
128                 finished = Process.wait2
129                 $pids.delete(finished[0])
130                 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
131             end
132         else
133             sys(cmd)
134         end
135     end
136
137     #- grab the results of a command but don't sleep forever on a runaway process
138     def subproc_runaway_aware(command)
139         begin
140             timeout(5) {
141                 return `#{command}`
142             }
143         rescue Timeout::Error    
144             msg 1, _("forgetting runaway process (transcode sucks again?)")
145             #- todo should slay transcode but dunno how to do that
146             return nil
147         end
148     end
149
150     def gen_thumbnails(orig, xmldir, dests)
151         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
152             return true
153         end
154
155         felem = xmldir.elements["[@filename='#{utf8(File.basename(orig))}']"]
156
157         if entry2type(orig) == 'image'
158             convert_options = ''
159             if felem
160                 rotate = felem.attributes['rotate']
161                 if !rotate
162                     orientation = `exif '#{orig}'`.detect { |line| line =~ /^Orientation/ }
163                     if orientation =~ /right - top/
164                         rotate = '90'
165                     end
166                     if orientation =~ /left - bottom/
167                         rotate = '-90'
168                     end
169                     rotate ||= '0'
170                     felem.add_attribute('rotate', rotate)
171                 end
172                 convert_options += "-rotate #{rotate} "
173             end
174             for dest in dests
175                 if !File.exists?(dest['filename'])
176                     psys("#{$convert} #{convert_options}-geometry #{dest['size']} '#{orig}' '#{dest['filename']}'")
177                 end
178             end
179             return true
180
181         elsif entry2type(orig) == 'video'
182             dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
183             #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail;
184             #- first try in dir to allow for: <dir .. subdirs-captionfile='/tmp/src2/people/pict0008.mov' pict0008.mov-frame-offset='100' ..>
185             frame_offset = xmldir.attributes["#{utf8(File.basename(orig))}-frame-offset"]
186             if !frame_offset
187                 #- then try in elements to allow for: <video filename='bourrique.mov' frame-offset='200'/>
188                 felem and frame_offset = felem.attributes['frame-offset']
189             end
190             frame_offset = (frame_offset || 5).to_i
191             for dest in dests
192                 if !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
193                     cmd = "transcode -a 0 -c #{frame_offset-5}-#{frame_offset} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' 2>&1"
194                     msg 2, cmd
195                     if subproc_runaway_aware(cmd) =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
196                         msg 2, __("* could not extract first image of video %s with transcode, will try first converting with mencoder", orig)
197                         cmd = "mencoder '#{orig}' -nosound -ovc lavc -lavcopts vcodec=mjpeg -o '#{dest_dir}/foo.avi' -frames #{frame_offset} -fps 25 >/dev/null 2>/dev/null"
198                         msg 2, cmd
199                         system cmd
200                         if File.exists?("#{dest_dir}/foo.avi")
201                             cmd = "transcode -a 0 -c #{frame_offset-5}-#{frame_offset} -i '#{dest_dir}/foo.avi' -y jpg -o '#{dest_dir}/screenshot.jpg' 2>&1"
202                             msg 2, cmd
203                             results = subproc_runaway_aware(cmd)
204                             system("rm -f '#{dest_dir}/foo.avi'")
205                             if results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000004.jpg")
206                                 msg 0, __("could not extract first image of video %s encoded by mencoder", "#{dest_dir}/foo.avi")
207                                 return false
208                             end
209                         else
210                             msg 0, __("could not make mencoder to encode %s to mpeg4", "#{orig}")
211                             return false
212                         end
213                     end
214
215                 end
216                 sys("#{$convert} -geometry #{dest['size']} #{dest_dir}/screenshot.jpg000004.jpg '#{dest['filename']}'")
217             end
218             return true
219         end
220     end
221
222 end