fix my xpath to make it work with ruby 1.8.3
[booh] / lib / booh / booh-lib.rb
1 #                         *  BOOH  *
2 #
3 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
4 #
5 # The acronyn sucks, however this is a tribute to Dragon Ball by
6 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
7 # Ball is named "Boo". But there was already a free software project
8 # called Boo, so this one will be it "Booh". Or whatever.
9 #
10 #
11 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
12 #
13 # This software may be freely redistributed under the terms of the GNU
14 # public license version 2.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
20 require 'iconv'
21 require 'timeout'
22
23 require 'rexml/document'
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                       'x-large' => utf8(_("x-large")), 'xx-large' => utf8(_("xx-large")),
46                       'original' => utf8(_("original")) }
47         return sizenames[key] || key
48     end
49     
50     def from_utf8(string)
51         return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
52     end
53
54     def make_dest_filename(orig_filename)
55         #- we remove non alphanumeric characters but need to do that
56         #- cleverly to not end up with two similar dest filenames. we won't
57         #- urlencode because urldecode might happen in the browser.
58         return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("%2X", v) }.to_s
59     end
60
61     def msg(verbose_level, msg)
62         if verbose_level <= $verbose_level
63             if verbose_level == 0
64                 warn _("\t***ERROR***: %s\n") % msg
65             elsif verbose_level == 1
66                 warn _("\tWarning: %s\n") % msg
67             else
68                 puts msg
69             end
70         end
71     end
72
73     def msg_(verbose_level, msg)
74         if verbose_level <= $verbose_level
75             if verbose_level == 0
76                 warn _("\t***ERROR***: %s") % msg
77             elsif verbose_level == 1
78                 warn _("\tWarning: %s") % msg
79             else
80                 print msg
81             end
82         end
83     end
84
85     def die(msg)
86         puts msg
87         exit 1
88     end
89
90     def select_theme(name, limit_sizes, optimizefor32, nperrow)
91         $theme = name
92         msg 3, _("Selecting theme `%s'") % $theme
93         themedir = "#{$FPATH}/themes/#{$theme}"
94         if !File.directory?(themedir)
95             die _("Theme was not found (tried %s directory).") % themedir
96         end
97         eval File.open("#{themedir}/metadata/parameters.rb").readlines.join
98
99         if limit_sizes
100             if limit_sizes != 'all'
101                 sizes = limit_sizes.split(/,/)
102                 $images_size = $images_size.find_all { |e| sizes.include?(e['name']) }
103                 if $images_size.length == 0
104                     die _("Can't carry on, no valid size selected.")
105                 end
106             end
107         else
108             $images_size = $images_size.find_all { |e| !e['optional'] }
109         end
110
111         if optimizefor32
112             $images_size.each { |e|
113                 e['fullscreen'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
114                 e['thumbnails'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
115             }
116             $albums_thumbnail_size.gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
117         end
118
119         if nperrow && nperrow != $default_N
120             ratio = nperrow.to_f / $default_N.to_f
121             $images_size.each { |e|
122                 e['thumbnails'].gsub!(/(\d+)x(\d+)/) { ($1.to_f/ratio).to_i.to_s + 'x' + ($2.to_f/ratio).to_i.to_s }
123             }
124         end
125
126         $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
127         if $default_size == nil
128             $default_size = $images_size[0]
129         end
130     end
131
132     def entry2type(entry)
133         if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i && entry !~ /['"\[\]]/
134             return 'image'
135         elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx|3gp|mp4)$/i && entry !~ /['"\[\]]/
136             #- might consider using file magic later..
137             return 'video'
138         else
139             return nil
140         end
141     end
142
143     def sys(cmd)
144         msg 2, cmd
145         system(cmd)
146     end
147
148     def waitjob
149         finished = Process.wait2
150         $pids.delete(finished[0])
151         $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
152     end
153
154     def waitjobs
155         while $pids && $pids.length > 0
156             waitjob
157         end
158     end
159
160     #- parallelizable sys
161     def psys(cmd)
162         if $mproc
163             if pid = fork
164                 $pids << pid
165             else
166                 msg 2, cmd + ' &'
167                 system(cmd)
168                 exit 0
169             end
170             if $pids.length == $mproc
171                 waitjob
172             end
173         else
174             sys(cmd)
175         end
176     end
177
178     #- grab the results of a command but don't sleep forever on a runaway process
179     def subproc_runaway_aware(command)
180         begin
181             timeout(10) {
182                 return `#{command}`
183             }
184         rescue Timeout::Error    
185             msg 1, _("forgetting runaway process (transcode sucks again?)")
186             #- todo should slay transcode but dunno how to do that
187             return nil
188         end
189     end
190
191     def get_image_size(fullpath)
192         if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
193             return { :x => $1.to_i, :y => $2.to_i }
194         else
195             return nil
196         end
197     end
198
199     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
200     def commify(n)
201         n.to_s =~ /([^\.]*)(\..*)?/
202         int, dec = $1.reverse, $2 ? $2 : ""
203         while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
204         end
205         int.reverse + dec
206     end
207
208     def guess_rotate(filename)
209         orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
210         if orientation =~ /right - top/
211             angle = 90
212         elsif orientation =~ /left - bottom/
213             angle = -90
214         else
215             return 0
216         end
217         size = get_image_size(filename)
218         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
219         if size && size[:x] < size[:y]
220             return 0
221         else
222             return angle
223         end
224     end
225
226     def rotate_pixbuf(pixbuf, angle)
227         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
228                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
229                              angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
230                                             Gdk::Pixbuf::ROTATE_NONE)
231     end
232
233     def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
234         if xmldirorelem.name == 'dir'
235             xmldirorelem = xmldirorelem.elements["*[@filename='#{utf8(File.basename(orig))}']"]
236         end
237         gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
238     end
239
240     def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
241         #- type can be `subdirs' or `thumbnails' 
242         gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
243     end
244
245     def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
246         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
247             return true
248         end
249
250         convert_options = ''
251         dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
252
253         if entry2type(orig) == 'image'
254             if felem
255                 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
256                     neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
257                     cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
258                     sys(cmd)
259                     if File.exists?(neworig)
260                         orig = neworig
261                     end
262                 end
263                 rotate = felem.attributes["#{attributes_prefix}rotate"]
264                 if !rotate
265                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
266                 end
267                 convert_options += "-rotate #{rotate} "
268                 if felem.attributes["#{attributes_prefix}enhance"]
269                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
270                 end
271             end
272             for dest in dests
273                 if !File.exists?(dest['filename'])
274                     cmd = nil
275                     #- don't resize if image is already smaller than destination size
276                     if size = get_image_size(orig)
277                         dest['size'] =~ /(\d+)x(\d+)/
278                         if (rotate == "90" || rotate == "270" || size[:x] < size[:y]) ? size[:y] < $1.to_i : size[:x] < $1.to_i
279                             cmd = "#{$convert} #{convert_options} '#{orig}' '#{dest['filename']}'"
280                         end
281                     end
282                     cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
283                     if allow_background
284                         psys(cmd)
285                     else
286                         sys(cmd)
287                     end
288                 end
289             end
290             if neworig
291                 if allow_background
292                     waitjobs
293                 end
294                 system("rm -f '#{neworig}'")
295             end
296             return true
297
298         elsif entry2type(orig) == 'video'
299             if felem
300                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
301                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
302                 if !frame_offset
303                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
304                 end
305                 frame_offset = frame_offset.to_i
306                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
307                     convert_options += "-rotate #{rotate} "
308                 end
309                 if felem.attributes["#{attributes_prefix}enhance"]
310                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
311                 end
312             end
313             orig_base = File.basename(dests[0]['filename']) + File.basename(orig)
314             orig_image = "#{dest_dir}/#{orig_base}.jpg000000.jpg"
315             system("rm -f '#{orig_image}'")
316             for dest in dests
317                 if !File.exists?(orig_image)
318                     transcode_options = ''
319                     if felem
320                         if felem.attributes["#{attributes_prefix}color-swap"]
321                             transcode_options += '-k '
322                         end
323                     end
324                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/#{orig_base}.jpg' #{transcode_options} 2>&1"
325                     msg 2, cmd
326                     results = subproc_runaway_aware(cmd)
327                     if results =~ /skipping frames/ && results =~ /encoded 0 frames/
328                         msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another problem. try another value.") %
329                                results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
330                         return false
331                     elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image)
332                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
333                         cmd = "mencoder '#{orig}' -nosound -ovc lavc -lavcopts vcodec=mjpeg -o '#{dest_dir}/#{orig_base}.avi' -frames #{frame_offset+26} -fps 25 >/dev/null 2>/dev/null"
334                         msg 2, cmd
335                         system cmd
336                         if File.exists?("#{dest_dir}/#{orig_base}.avi")
337                             cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{dest_dir}/#{orig_base}.avi' -y jpg -o '#{dest_dir}/#{orig_base}.jpg' #{transcode_options} 2>&1"
338                             msg 2, cmd
339                             results = subproc_runaway_aware(cmd)
340                             system("rm -f '#{dest_dir}/#{orig_base}.avi'")
341                             if results =~ /skipping frames/ && results =~ /encoded 0 frames/
342                                 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
343                                        results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
344                                 return false
345                             elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image)
346                                 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/#{orig_base}.avi"
347                                 return false
348                             end
349                         else
350                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
351                             return false
352                         end
353                     end
354                     if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
355                         if whitebalance.to_f != 0
356                             neworig = "#{dest_dir}/#{orig_base}-whitebalance#{whitebalance}.jpg"
357                             cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}"
358                             sys(cmd)
359                             if File.exists?(neworig)
360                                 orig_image = neworig
361                             end
362                         end
363                     end
364                 end
365                 if !File.exists?(dest['filename'])
366                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
367                 end
368             end
369             if neworig
370                 system("rm -f '#{neworig}'")
371             end
372             return true
373         end
374     end
375
376     def invornil(obj, methodname)
377         if obj == nil
378             return nil
379         else
380             return obj.method(methodname).call
381         end
382     end
383
384     def find_subalbum_info_type(xmldir)
385         #- first look for subdirs info; if not, means there is no subdir
386         if xmldir.attributes['subdirs-caption']
387             return 'subdirs'
388         else
389             return 'thumbnails'
390         end
391     end
392
393     def find_subalbum_caption_info(xmldir)
394         type = find_subalbum_info_type(xmldir)
395         return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
396     end
397
398     def file_size(path)
399         begin
400             return File.size(path)
401         rescue
402             return -1
403         end
404     end
405
406     def max(a, b)
407         a > b ? a : b
408     end
409
410     def substInFile(name)
411         newcontent = IO.readlines(name).collect { |l| yield l }
412         ios = File.open(name, "w")
413         ios.write(newcontent)
414         ios.close
415     end
416 end
417
418 class File
419     def File.reduce_path(path)
420         return path.gsub(/\w+\/\.\.\//, '')
421     end
422 end
423
424 class REXML::Element
425     def previous_element_byname(name)
426         n = self
427         while n = n.previous_element
428             if n.name == name
429                 return n
430             end
431         end
432         return nil
433     end
434
435     def previous_element_byname_notattr(name, attr)
436         n = self
437         while n = n.previous_element
438             if n.name == name && !n.attributes[attr]
439                 return n
440             end
441         end
442         return nil
443     end
444
445     def next_element_byname(name)
446         n = self
447         while n = n.next_element
448             if n.name == name
449                 return n
450             end
451         end
452         return nil
453     end
454
455     def next_element_byname_notattr(name, attr)
456         n = self
457         while n = n.next_element
458             if n.name == name && !n.attributes[attr]
459                 return n
460             end
461         end
462         return nil
463     end
464
465     def child_byname_notattr(name, attr)
466         elements.each(name) { |element|
467             if !element.attributes[attr]
468                 return element
469             end
470         }
471         return nil
472     end
473 end
474
475