fix previous/next not subtituted in case of not using --config
[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, xmldirorelem, allow_background, dests)
211         if xmldirorelem.name == 'dir'
212             xmldirorelem = xmldirorelem.elements["[@filename='#{utf8(File.basename(orig))}']"]
213         end
214         gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
215     end
216
217     def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
218         #- type can be `subdirs' or `thumbnails' 
219         gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
220     end
221
222     def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
223         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
224             return true
225         end
226
227         convert_options = ''
228         dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
229
230         if entry2type(orig) == 'image'
231             if felem
232                 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
233                     neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
234                     cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
235                     sys(cmd)
236                     if File.exists?(neworig)
237                         orig = neworig
238                         allow_background = false
239                     end
240                 end
241                 rotate = felem.attributes["#{attributes_prefix}rotate"]
242                 if !rotate
243                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
244                 end
245                 convert_options += "-rotate #{rotate} "
246                 if felem.attributes["#{attributes_prefix}enhance"]
247                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
248                 end
249             end
250             for dest in dests
251                 if !File.exists?(dest['filename'])
252                     cmd = nil
253                     #- don't resize if image is already smaller than destination size
254                     if size = get_image_size(orig)
255                         dest['size'] =~ /(\d+)x(\d+)/
256                         if (rotate == "90" || rotate == "270" || size[:x] < size[:y]) ? size[:y] < $1.to_i : size[:x] < $1.to_i
257                             cmd = "#{$convert} #{convert_options} '#{orig}' '#{dest['filename']}'"
258                         end
259                     end
260                     cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
261                     if allow_background
262                         psys(cmd)
263                     else
264                         sys(cmd)
265                     end
266                 end
267             end
268             if neworig
269                 system("rm -f '#{neworig}'")
270             end
271             return true
272
273         elsif entry2type(orig) == 'video'
274             if felem
275                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
276                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
277                 if !frame_offset
278                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
279                 end
280                 frame_offset = frame_offset.to_i
281                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
282                     convert_options += "-rotate #{rotate} "
283                 end
284                 if felem.attributes["#{attributes_prefix}enhance"]
285                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
286                 end
287             end
288             orig_image = "#{dest_dir}/screenshot.jpg000000.jpg"
289             for dest in dests
290                 if !File.exists?(orig_image)
291                     transcode_options = ''
292                     if felem
293                         if felem.attributes["#{attributes_prefix}color-swap"]
294                             transcode_options += '-k '
295                         end
296                     end
297                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
298                     msg 2, cmd
299                     results = subproc_runaway_aware(cmd)
300                     if results =~ /skipping frames/ && results =~ /encoded 0 frames/
301                         msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another problem. try another value.") %
302                                results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
303                         return false
304                     elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
305                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
306                         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"
307                         msg 2, cmd
308                         system cmd
309                         if File.exists?("#{dest_dir}/foo.avi")
310                             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"
311                             msg 2, cmd
312                             results = subproc_runaway_aware(cmd)
313                             system("rm -f '#{dest_dir}/foo.avi'")
314                             if results =~ /skipping frames/ && results =~ /encoded 0 frames/
315                                 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
316                                        results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
317                                 return false
318                             elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
319                                 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/foo.avi"
320                                 return false
321                             end
322                         else
323                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
324                             return false
325                         end
326                     end
327                     if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
328                         if whitebalance.to_f != 0
329                             neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
330                             cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}"
331                             sys(cmd)
332                             if File.exists?(neworig)
333                                 orig_image = neworig
334                             end
335                         end
336                     end
337                 end
338                 if !File.exists?(dest['filename'])
339                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
340                 end
341             end
342             if neworig
343                 system("rm -f '#{neworig}'")
344             end
345             return true
346         end
347     end
348
349     def invornil(obj, methodname)
350         if obj == nil
351             return nil
352         else
353             return obj.method(methodname).call
354         end
355     end
356
357     def find_subalbum_info_type(xmldir)
358         #- first look for subdirs info; if not, means there is no subdir
359         if xmldir.attributes['subdirs-caption']
360             return 'subdirs'
361         else
362             return 'thumbnails'
363         end
364     end
365
366     def find_subalbum_caption_info(xmldir)
367         type = find_subalbum_info_type(xmldir)
368         return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
369     end
370
371     def file_size(path)
372         begin
373             return File.size(path)
374         rescue
375             return -1
376         end
377     end
378
379     def max(a, b)
380         a > b ? a : b
381     end
382
383     def substInFile(name)
384         newcontent = IO.readlines(name).collect { |l| yield l }
385         ios = File.open(name, "w")
386         ios.write(newcontent)
387         ios.close
388     end
389 end
390
391 class File
392     def File.reduce_path(path)
393         return path.gsub(/\w+\/\.\.\//, '')
394     end
395 end
396
397 class REXML::Element
398     def previous_element_byname(name)
399         n = self
400         while n = n.previous_element
401             if n.name == name
402                 return n
403             end
404         end
405         return nil
406     end
407
408     def next_element_byname(name)
409         n = self
410         while n = n.next_element
411             if n.name == name
412                 return n
413             end
414         end
415         return nil
416     end
417 end
418
419