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