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