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