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