efbf1ea4925c6eca443767cc2e5c38eb364ce270
[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     $convert_enhance = '-contrast -enhance -normalize'
37
38     def utf8(string)
39         return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string).to_s
40     end
41     
42     def from_utf8(string)
43         return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
44     end
45
46     def make_dest_filename(orig_filename)
47         #- we remove non alphanumeric characters but need to do that
48         #- cleverly to not end up with two similar dest filenames. we won't
49         #- urlencode because urldecode might happen in the browser.
50         return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("%2X", v) }.to_s
51     end
52
53     def msg(verbose_level, msg)
54         if verbose_level <= $verbose_level
55             if verbose_level == 0
56                 warn _("\t***ERROR***: %s\n") % msg
57             elsif verbose_level == 1
58                 warn _("\tWarning: %s\n") % msg
59             else
60                 puts msg
61             end
62         end
63     end
64
65     def msg_(verbose_level, msg)
66         if verbose_level <= $verbose_level
67             if verbose_level == 0
68                 warn _("\t***ERROR***: %s") % msg
69             elsif verbose_level == 1
70                 warn _("\tWarning: %s") % msg
71             else
72                 print msg
73             end
74         end
75     end
76
77     def die(msg)
78         puts msg
79         exit 1
80     end
81
82     def select_theme(name)
83         $theme = name
84         msg 3, _("Selecting theme `%s'") % $theme
85         themedir = "#{$FPATH}/themes/#{$theme}"
86         if !File.directory?(themedir)
87             die _("Theme was not found (tried %s directory).") % themedir
88         end
89         require "#{themedir}/metadata/parameters.rb"
90         $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
91     end
92
93     def entry2type(entry)
94         if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i
95             return 'image'
96         elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i
97             #- might consider using file magic later..
98             return 'video'
99         else
100             return nil
101         end
102     end
103
104     def sys(cmd)
105         msg 2, cmd
106         system(cmd)
107     end
108
109     #- parallelizable sys
110     def psys(cmd)
111         if $mproc
112             if pid = fork
113                 $pids << pid
114             else
115                 msg 2, cmd + ' &'
116                 system(cmd)
117                 exit 0
118             end
119             if $pids.length == $mproc
120                 finished = Process.wait2
121                 $pids.delete(finished[0])
122                 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
123             end
124         else
125             sys(cmd)
126         end
127     end
128
129     #- grab the results of a command but don't sleep forever on a runaway process
130     def subproc_runaway_aware(command)
131         begin
132             timeout(5) {
133                 return `#{command}`
134             }
135         rescue Timeout::Error    
136             msg 1, _("forgetting runaway process (transcode sucks again?)")
137             #- todo should slay transcode but dunno how to do that
138             return nil
139         end
140     end
141
142     def get_image_size(fullpath)
143         if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
144             return { :x => $1, :y => $2 }
145         else
146             return nil
147         end
148     end
149
150     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
151     def commify(n)
152         n.to_s =~ /([^\.]*)(\..*)?/
153         int, dec = $1.reverse, $2 ? $2 : ""
154         while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
155         end
156         int.reverse + dec
157     end
158
159     def guess_rotate(filename)
160         orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
161         if orientation =~ /right - top/
162             angle = 90
163         elsif orientation =~ /left - bottom/
164             angle = -90
165         else
166             return 0
167         end
168         size = get_image_size(filename)
169         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
170         if size && size[:x] < size[:y]
171             return 0
172         else
173             return angle
174         end
175     end
176
177     def rotate_pixbuf(pixbuf, angle)
178         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
179                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
180                              angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
181                                             Gdk::Pixbuf::ROTATE_NONE)
182     end
183
184     def gen_thumbnails_element(orig, xmldir, allow_background, dests)
185         gen_thumbnails(orig, xmldir, allow_background, dests, xmldir.elements["[@filename='#{utf8(File.basename(orig))}']"], '')
186     end
187
188     def gen_thumbnails_subdir(orig, xmldir, allow_background, dests, type)
189         #- type can be `subdirs' or `thumbnails' 
190         gen_thumbnails(orig, xmldir, allow_background, dests, xmldir, type + '-')
191     end
192
193     def gen_thumbnails(orig, xmldir, allow_background, dests, felem, attributes_prefix)
194         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
195             return true
196         end
197
198         convert_options = ''
199
200         if entry2type(orig) == 'image'
201             if felem
202                 rotate = felem.attributes["#{attributes_prefix}rotate"]
203                 if !rotate
204                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
205                 end
206                 convert_options += "-rotate #{rotate} "
207                 if felem.attributes["#{attributes_prefix}enhance"]
208                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
209                 end
210             end
211             for dest in dests
212                 if !File.exists?(dest['filename'])
213                     cmd = "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
214                     if allow_background
215                         psys(cmd)
216                     else
217                         sys(cmd)
218                     end
219                 end
220             end
221             return true
222
223         elsif entry2type(orig) == 'video'
224             dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
225             if felem
226                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
227                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
228                 if !frame_offset
229                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
230                 end
231                 frame_offset = frame_offset.to_i
232                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
233                     convert_options += "-rotate #{rotate} "
234                 end
235                 if felem.attributes["#{attributes_prefix}enhance"]
236                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
237                 end
238             end
239             for dest in dests
240                 if !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
241                     transcode_options = ''
242                     if felem
243                         if felem.attributes["#{attributes_prefix}color-swap"]
244                             transcode_options += '-k '
245                         end
246                     end
247                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
248                     msg 2, cmd
249                     results = subproc_runaway_aware(cmd)
250                     if results =~ /skipping frames/ && results =~ /encoded 0 frames/
251                         msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
252                                results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
253                         return false
254                     elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
255                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
256                         cmd = "mencoder '#{orig}' -nosound -ovc lavc -lavcopts vcodec=mjpeg -o '#{dest_dir}/foo.avi' -frames #{frame_offset+1} -fps 25 >/dev/null 2>/dev/null"
257                         msg 2, cmd
258                         system cmd
259                         if File.exists?("#{dest_dir}/foo.avi")
260                             cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{dest_dir}/foo.avi' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
261                             msg 2, cmd
262                             results = subproc_runaway_aware(cmd)
263                             system("rm -f '#{dest_dir}/foo.avi'")
264                             if results =~ /skipping frames/ && results =~ /encoded 0 frames/
265                                 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
266                                        results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
267                                 return false
268                             elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
269                                 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/foo.avi"
270                                 return false
271                             end
272                         else
273                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
274                             return false
275                         end
276                     end
277
278                 end
279                 if !File.exists?(dest['filename'])
280                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} #{dest_dir}/screenshot.jpg000000.jpg '#{dest['filename']}'")
281                 end
282             end
283             return true
284         end
285     end
286
287     def invornil(obj, methodname)
288         if obj == nil
289             return nil
290         else
291             return obj.method(methodname).call
292         end
293     end
294
295     def find_subalbum_info_type(xmldir)
296         #- first look for subdirs info; if not, means there is no subdir
297         if xmldir.attributes['subdirs-caption']
298             return 'subdirs'
299         else
300             return 'thumbnails'
301         end
302     end
303
304     def find_subalbum_caption_info(xmldir)
305         type = find_subalbum_info_type(xmldir)
306         return from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"]
307     end
308 end