*** empty log message ***
[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("_%02X", 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 !$ignore_videos && 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 !$no_identify && `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         if $no_identify
210             return 0
211         end
212
213         orientation = `identify -format "%[EXIF:orientation]" '#{filename}'`.chomp
214         if orientation == '6'
215             angle = 90
216         elsif orientation == '8'
217             angle = -90
218         else
219             return 0
220         end
221         size = get_image_size(filename)
222         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
223         if size && size[:x] < size[:y]
224             return 0
225         else
226             return angle
227         end
228     end
229
230     def rotate_pixbuf(pixbuf, angle)
231         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
232                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
233                              angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
234                                             Gdk::Pixbuf::ROTATE_NONE)
235     end
236
237     def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
238         if xmldirorelem.name == 'dir'
239             xmldirorelem = xmldirorelem.elements["*[@filename='#{utf8(File.basename(orig))}']"]
240         end
241         gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
242     end
243
244     def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
245         #- type can be `subdirs' or `thumbnails' 
246         gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
247     end
248
249     def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
250         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
251             return true
252         end
253
254         convert_options = ''
255         dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
256
257         if entry2type(orig) == 'image'
258             if felem
259                 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
260                     neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
261                     cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
262                     sys(cmd)
263                     if File.exists?(neworig)
264                         orig = neworig
265                     end
266                 end
267                 rotate = felem.attributes["#{attributes_prefix}rotate"]
268                 if !rotate
269                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
270                 end
271                 convert_options += "-rotate #{rotate} "
272                 if felem.attributes["#{attributes_prefix}enhance"]
273                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
274                 end
275             end
276             for dest in dests
277                 if !File.exists?(dest['filename'])
278                     cmd = nil
279                     cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize '#{dest['size']}>' '#{orig}' '#{dest['filename']}'"
280                     if allow_background
281                         psys(cmd)
282                     else
283                         sys(cmd)
284                     end
285                 end
286             end
287             if neworig
288                 if allow_background
289                     waitjobs
290                 end
291                 system("rm -f '#{neworig}'")
292             end
293             return true
294
295         elsif entry2type(orig) == 'video'
296             if felem
297                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
298                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
299                 if !frame_offset
300                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
301                 end
302                 frame_offset = frame_offset.to_i
303                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
304                     convert_options += "-rotate #{rotate} "
305                 end
306                 if felem.attributes["#{attributes_prefix}enhance"]
307                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
308                 end
309             end
310             orig_base = File.basename(dests[0]['filename']) + File.basename(orig)
311             orig_image = "#{dest_dir}/#{orig_base}.jpg000000.jpg"
312             system("rm -f '#{orig_image}'")
313             for dest in dests
314                 if !File.exists?(orig_image)
315                     transcode_options = ''
316                     if felem
317                         if felem.attributes["#{attributes_prefix}color-swap"]
318                             transcode_options += '-k '
319                         end
320                     end
321                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/#{orig_base}.jpg' #{transcode_options} 2>&1"
322                     msg 2, cmd
323                     results = subproc_runaway_aware(cmd)
324                     if results =~ /skipping frames/ && results =~ /encoded 0 frames/
325                         msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another problem. try another value.") %
326                                results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
327                         return false
328                     elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image)
329                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
330                         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"
331                         msg 2, cmd
332                         system cmd
333                         if File.exists?("#{dest_dir}/#{orig_base}.avi")
334                             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"
335                             msg 2, cmd
336                             results = subproc_runaway_aware(cmd)
337                             system("rm -f '#{dest_dir}/#{orig_base}.avi'")
338                             if results =~ /skipping frames/ && results =~ /encoded 0 frames/
339                                 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
340                                        results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
341                                 return false
342                             elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image)
343                                 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/#{orig_base}.avi"
344                                 return false
345                             end
346                         else
347                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
348                             return false
349                         end
350                     end
351                     if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
352                         if whitebalance.to_f != 0
353                             neworig = "#{dest_dir}/#{orig_base}-whitebalance#{whitebalance}.jpg"
354                             cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}"
355                             sys(cmd)
356                             if File.exists?(neworig)
357                                 orig_image = neworig
358                             end
359                         end
360                     end
361                 end
362                 if !File.exists?(dest['filename'])
363                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
364                 end
365             end
366             if neworig
367                 system("rm -f '#{neworig}'")
368             end
369             return true
370         end
371     end
372
373     def invornil(obj, methodname)
374         if obj == nil
375             return nil
376         else
377             return obj.method(methodname).call
378         end
379     end
380
381     def find_subalbum_info_type(xmldir)
382         #- first look for subdirs info; if not, means there is no subdir
383         if xmldir.attributes['subdirs-caption']
384             return 'subdirs'
385         else
386             return 'thumbnails'
387         end
388     end
389
390     def find_subalbum_caption_info(xmldir)
391         type = find_subalbum_info_type(xmldir)
392         return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
393     end
394
395     def file_size(path)
396         begin
397             return File.size(path)
398         rescue
399             return -1
400         end
401     end
402
403     def max(a, b)
404         a > b ? a : b
405     end
406
407     def clamp(n, a, b)
408         n < a ? a : n > b ? b : n
409     end
410
411     def pano_amount(elem)
412         if pano_amount = elem.attributes['pano-amount']
413             if $N_per_row
414                 return clamp(pano_amount.to_f, 1, $N_per_row.to_i)
415             else
416                 return clamp(pano_amount.to_f, 1, $default_N.to_i)
417             end
418         else
419             return nil
420         end
421     end
422
423     def substInFile(name)
424         newcontent = IO.readlines(name).collect { |l| yield l }
425         ios = File.open(name, "w")
426         ios.write(newcontent)
427         ios.close
428     end
429 end
430
431 class File
432     def File.reduce_path(path)
433         return path.gsub(/\w+\/\.\.\//, '')
434     end
435 end
436
437 class REXML::Element
438     def previous_element_byname(name)
439         n = self
440         while n = n.previous_element
441             if n.name == name
442                 return n
443             end
444         end
445         return nil
446     end
447
448     def previous_element_byname_notattr(name, attr)
449         n = self
450         while n = n.previous_element
451             if n.name == name && !n.attributes[attr]
452                 return n
453             end
454         end
455         return nil
456     end
457
458     def next_element_byname(name)
459         n = self
460         while n = n.next_element
461             if n.name == name
462                 return n
463             end
464         end
465         return nil
466     end
467
468     def next_element_byname_notattr(name, attr)
469         n = self
470         while n = n.next_element
471             if n.name == name && !n.attributes[attr]
472                 return n
473             end
474         end
475         return nil
476     end
477
478     def child_byname_notattr(name, attr)
479         elements.each(name) { |element|
480             if !element.attributes[attr]
481                 return element
482             end
483         }
484         return nil
485     end
486 end
487
488