remember preferred size of user with a cookie
[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         if $default_size == nil
92             die _("Theme `%s' has no default size.") % $theme
93         end
94     end
95
96     def entry2type(entry)
97         if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i
98             return 'image'
99         elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i
100             #- might consider using file magic later..
101             return 'video'
102         else
103             return nil
104         end
105     end
106
107     def sys(cmd)
108         msg 2, cmd
109         system(cmd)
110     end
111
112     #- parallelizable sys
113     def psys(cmd)
114         if $mproc
115             if pid = fork
116                 $pids << pid
117             else
118                 msg 2, cmd + ' &'
119                 system(cmd)
120                 exit 0
121             end
122             if $pids.length == $mproc
123                 finished = Process.wait2
124                 $pids.delete(finished[0])
125                 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
126             end
127         else
128             sys(cmd)
129         end
130     end
131
132     #- grab the results of a command but don't sleep forever on a runaway process
133     def subproc_runaway_aware(command)
134         begin
135             timeout(5) {
136                 return `#{command}`
137             }
138         rescue Timeout::Error    
139             msg 1, _("forgetting runaway process (transcode sucks again?)")
140             #- todo should slay transcode but dunno how to do that
141             return nil
142         end
143     end
144
145     def get_image_size(fullpath)
146         if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
147             return { :x => $1, :y => $2 }
148         else
149             return nil
150         end
151     end
152
153     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
154     def commify(n)
155         n.to_s =~ /([^\.]*)(\..*)?/
156         int, dec = $1.reverse, $2 ? $2 : ""
157         while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
158         end
159         int.reverse + dec
160     end
161
162     def guess_rotate(filename)
163         orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
164         if orientation =~ /right - top/
165             angle = 90
166         elsif orientation =~ /left - bottom/
167             angle = -90
168         else
169             return 0
170         end
171         size = get_image_size(filename)
172         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
173         if size && size[:x] < size[:y]
174             return 0
175         else
176             return angle
177         end
178     end
179
180     def rotate_pixbuf(pixbuf, angle)
181         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
182                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
183                              angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
184                                             Gdk::Pixbuf::ROTATE_NONE)
185     end
186
187     def gen_thumbnails_element(orig, xmldir, allow_background, dests)
188         gen_thumbnails(orig, xmldir, allow_background, dests, xmldir.elements["[@filename='#{utf8(File.basename(orig))}']"], '')
189     end
190
191     def gen_thumbnails_subdir(orig, xmldir, allow_background, dests, type)
192         #- type can be `subdirs' or `thumbnails' 
193         gen_thumbnails(orig, xmldir, allow_background, dests, xmldir, type + '-')
194     end
195
196     def gen_thumbnails(orig, xmldir, allow_background, dests, felem, attributes_prefix)
197         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
198             return true
199         end
200
201         convert_options = ''
202
203         if entry2type(orig) == 'image'
204             if felem
205                 rotate = felem.attributes["#{attributes_prefix}rotate"]
206                 if !rotate
207                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
208                 end
209                 convert_options += "-rotate #{rotate} "
210                 if felem.attributes["#{attributes_prefix}enhance"]
211                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
212                 end
213             end
214             for dest in dests
215                 if !File.exists?(dest['filename'])
216                     cmd = "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
217                     if allow_background
218                         psys(cmd)
219                     else
220                         sys(cmd)
221                     end
222                 end
223             end
224             return true
225
226         elsif entry2type(orig) == 'video'
227             dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
228             if felem
229                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
230                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
231                 if !frame_offset
232                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
233                 end
234                 frame_offset = frame_offset.to_i
235                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
236                     convert_options += "-rotate #{rotate} "
237                 end
238                 if felem.attributes["#{attributes_prefix}enhance"]
239                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
240                 end
241             end
242             for dest in dests
243                 if !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
244                     transcode_options = ''
245                     if felem
246                         if felem.attributes["#{attributes_prefix}color-swap"]
247                             transcode_options += '-k '
248                         end
249                     end
250                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
251                     msg 2, cmd
252                     results = subproc_runaway_aware(cmd)
253                     if results =~ /skipping frames/ && results =~ /encoded 0 frames/
254                         msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
255                                results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
256                         return false
257                     elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
258                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
259                         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"
260                         msg 2, cmd
261                         system cmd
262                         if File.exists?("#{dest_dir}/foo.avi")
263                             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"
264                             msg 2, cmd
265                             results = subproc_runaway_aware(cmd)
266                             system("rm -f '#{dest_dir}/foo.avi'")
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 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/foo.avi"
273                                 return false
274                             end
275                         else
276                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
277                             return false
278                         end
279                     end
280
281                 end
282                 if !File.exists?(dest['filename'])
283                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} #{dest_dir}/screenshot.jpg000000.jpg '#{dest['filename']}'")
284                 end
285             end
286             return true
287         end
288     end
289
290     def invornil(obj, methodname)
291         if obj == nil
292             return nil
293         else
294             return obj.method(methodname).call
295         end
296     end
297
298     def find_subalbum_info_type(xmldir)
299         #- first look for subdirs info; if not, means there is no subdir
300         if xmldir.attributes['subdirs-caption']
301             return 'subdirs'
302         else
303             return 'thumbnails'
304         end
305     end
306
307     def find_subalbum_caption_info(xmldir)
308         type = find_subalbum_info_type(xmldir)
309         return from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"]
310     end
311 end