3 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
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.
11 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
13 # This software may be freely redistributed under the terms of the GNU
14 # public license version 2.
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.
23 require 'rexml/document'
27 bindtextdomain("booh")
29 require 'booh/config.rb'
30 require 'booh/version.rb'
37 require 'booh/libadds'
44 $CURRENT_CHARSET = `locale charmap`.chomp
45 $convert = 'convert -interlace line +profile "*"'
46 $convert_enhance = '-contrast -enhance -normalize'
49 return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string).to_s
52 def utf8cut(string, maxlen)
54 return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string[0..maxlen-1]).to_s
55 rescue Iconv::InvalidCharacter
56 return utf8cut(string, maxlen-1)
61 #- fake for gettext to find these; if themes need more sizes, english name for them should be added here
62 sizenames = { 'small' => utf8(_("small")), 'medium' => utf8(_("medium")), 'large' => utf8(_("large")),
63 'x-large' => utf8(_("x-large")), 'xx-large' => utf8(_("xx-large")),
64 'original' => utf8(_("original")) }
65 return sizenames[key] || key
69 return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
72 def from_utf8_safe(string)
74 return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
75 rescue Iconv::IllegalSequence
80 def make_dest_filename_old(orig_filename)
81 #- we remove non alphanumeric characters but need to do that
82 #- cleverly to not end up with two similar dest filenames. we won't
83 #- urlencode because urldecode might happen in the browser.
84 return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("%2X", v) }.to_s
87 def make_dest_filename(orig_filename)
88 #- we remove non alphanumeric characters but need to do that
89 #- cleverly to not end up with two similar dest filenames. we won't
90 #- urlencode because urldecode might happen in the browser.
91 return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("~%02X", v) }.to_s
94 def msg(verbose_level, msg)
95 if verbose_level <= $verbose_level
97 warn _("\t***ERROR***: %s\n") % msg
98 elsif verbose_level == 1
99 warn _("\tWarning: %s\n") % msg
106 def msg_(verbose_level, msg)
107 if verbose_level <= $verbose_level
108 if verbose_level == 0
109 warn _("\t***ERROR***: %s") % msg
110 elsif verbose_level == 1
111 warn _("\tWarning: %s") % msg
123 def select_theme(name, limit_sizes, optimizefor32, nperrow)
125 msg 3, _("Selecting theme '%s'") % $theme
126 themedir = "#{$FPATH}/themes/#{$theme}"
127 if !File.directory?(themedir)
128 die _("Theme was not found (tried %s directory).") % themedir
130 eval File.open("#{themedir}/metadata/parameters.rb").readlines.join
133 if limit_sizes != 'all'
134 sizes = limit_sizes.split(/,/)
135 $images_size = $images_size.find_all { |e| sizes.include?(e['name']) }
136 if $images_size.length == 0
137 die _("Can't carry on, no valid size selected.")
141 $images_size = $images_size.find_all { |e| !e['optional'] }
145 $images_size.each { |e|
146 e['fullscreen'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
147 e['thumbnails'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
149 $albums_thumbnail_size.gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s }
152 if nperrow && nperrow != $default_N
153 ratio = nperrow.to_f / $default_N.to_f
154 $images_size.each { |e|
155 e['thumbnails'].gsub!(/(\d+)x(\d+)/) { ($1.to_f/ratio).to_i.to_s + 'x' + ($2.to_f/ratio).to_i.to_s }
159 $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
160 if $default_size == nil
161 $default_size = $images_size[0]
165 def entry2type(entry)
166 if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i && entry !~ /['"\[\]]/
168 elsif !$ignore_videos && entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx|3gp|mp4)$/i && entry !~ /['"\[\]]/
169 #- might consider using file magic later..
182 finished = Process.wait2
183 $pids.delete(finished[0])
184 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
188 while $pids && $pids.length > 0
193 #- parallelizable sys
203 if $pids.length == $mproc
211 #- grab the results of a command but don't sleep forever on a runaway process
212 def subproc_runaway_aware(command)
217 rescue Timeout::Error
218 msg 1, _("forgetting runaway process (transcode sucks again?)")
219 #- todo should slay transcode but dunno how to do that
224 def get_image_size(fullpath)
229 if $sizes_cache[fullpath].nil?
230 #- identify is slow, try with gdk if available (negligible vs 35ms)
232 if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
233 $sizes_cache[fullpath] = { :x => $1.to_i, :y => $2.to_i }
236 format, width, height = Gdk::Pixbuf.get_file_info(fullpath)
238 $sizes_cache[fullpath] = { :x => width, :y => height }
242 return $sizes_cache[fullpath]
248 #- commify from http://pleac.sourceforge.net/ (pleac rulz)
250 n.to_s =~ /([^\.]*)(\..*)?/
251 int, dec = $1.reverse, $2 ? $2 : ""
252 while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
257 def guess_rotate(filename)
258 #- identify is slow, try with libexif if available (4ms vs 35ms)
263 orientation = `identify -format "%[EXIF:orientation]" '#{filename}'`.chomp.to_i
265 orientation = Exif.orientation(filename)
270 elsif orientation == 8
276 #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
277 size = get_image_size(filename)
278 if size && size[:x] < size[:y]
285 def rotate_pixbuf(pixbuf, angle)
286 return pixbuf.rotate(angle == 90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
287 angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
288 (angle == 270 || angle == -90) ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
289 Gdk::Pixbuf::ROTATE_NONE)
292 def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
293 if xmldirorelem.name == 'dir'
294 xmldirorelem = xmldirorelem.elements["*[@filename='#{utf8(File.basename(orig))}']"]
296 gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
299 def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
300 #- type can be `subdirs' or `thumbnails'
301 gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
304 def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
305 if !dests.detect { |dest| !File.exists?(dest['filename']) }
310 dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
312 if entry2type(orig) == 'image'
314 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
315 neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
316 cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
318 if File.exists?(neworig)
322 if gammacorrect = felem.attributes["#{attributes_prefix}gamma-correction"]
323 neworig = "#{dest_dir}/#{File.basename(orig)}-gammacorrect#{gammacorrect}.jpg"
324 cmd = "booh-gamma-correction '#{orig}' '#{neworig}' #{gammacorrect}"
326 if File.exists?(neworig)
330 rotate = felem.attributes["#{attributes_prefix}rotate"]
332 felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_s)
334 convert_options += "-rotate #{rotate} "
335 if felem.attributes["#{attributes_prefix}enhance"]
336 convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
340 if !File.exists?(dest['filename'])
342 cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize '#{dest['size']}>' '#{orig}' '#{dest['filename']}'"
354 system("rm -f '#{neworig}'")
358 elsif entry2type(orig) == 'video'
360 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
361 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
363 felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
365 frame_offset = frame_offset.to_i
366 if rotate = felem.attributes["#{attributes_prefix}rotate"]
367 convert_options += "-rotate #{rotate} "
369 if felem.attributes["#{attributes_prefix}enhance"]
370 convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
373 orig_base = File.basename(dests[0]['filename']) + File.basename(orig)
374 orig_image = "#{dest_dir}/#{orig_base}.jpg000000.jpg"
375 system("rm -f '#{orig_image}'")
377 if !File.exists?(orig_image)
378 transcode_options = ''
380 if felem.attributes["#{attributes_prefix}color-swap"]
381 transcode_options += '-k '
384 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"
387 if File.exists?("#{dest_dir}/#{orig_base}.avi")
388 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"
390 results = subproc_runaway_aware(cmd)
391 system("rm -f '#{dest_dir}/#{orig_base}.avi'")
392 if results =~ /skipping frames/ && results =~ /encoded 0 frames/
393 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
394 results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
396 elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image)
397 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/#{orig_base}.avi"
401 msg 0, _("could not make mencoder to encode %s to mjpeg") % orig
404 if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
405 if whitebalance.to_f != 0
406 neworig = "#{dest_dir}/#{orig_base}-whitebalance#{whitebalance}.jpg"
407 cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}"
409 if File.exists?(neworig)
414 if felem && gammacorrect = felem.attributes["#{attributes_prefix}gamma-correction"]
415 if gammacorrect.to_f != 0
416 neworig = "#{dest_dir}/#{orig_base}-gammacorrect#{gammacorrect}.jpg"
417 cmd = "booh-gamma-correction '#{orig_image}' '#{neworig}' #{gammacorrect}"
419 if File.exists?(neworig)
425 if !File.exists?(dest['filename'])
426 sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
430 system("rm -f '#{neworig}'")
436 def invornil(obj, methodname)
440 return obj.method(methodname).call
444 def find_subalbum_info_type(xmldir)
445 #- first look for subdirs info; if not, means there is no subdir
446 if xmldir.attributes['subdirs-caption']
453 def find_subalbum_caption_info(xmldir)
454 type = find_subalbum_info_type(xmldir)
455 return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
460 return File.size(path)
471 n < a ? a : n > b ? b : n
474 def pano_amount(elem)
475 if pano_amount = elem.attributes['pano-amount']
477 return clamp(pano_amount.to_f, 1, $N_per_row.to_i)
479 return clamp(pano_amount.to_f, 1, $default_N.to_i)
486 def substInFile(name)
487 newcontent = IO.readlines(name).collect { |l| yield l }
488 ios = File.open(name, "w")
489 ios.write(newcontent)
495 def File.reduce_path(path)
496 return path.gsub(/\w+\/\.\.\//, '')
501 def collect_with_index
503 each_with_index { |e,i|
511 def previous_element_byname(name)
513 while n = n.previous_element
521 def previous_element_byname_notattr(name, attr)
523 while n = n.previous_element
524 if n.name == name && !n.attributes[attr]
531 def next_element_byname(name)
533 while n = n.next_element
541 def next_element_byname_notattr(name, attr)
543 while n = n.next_element
544 if n.name == name && !n.attributes[attr]
551 def child_byname_notattr(name, attr)
552 elements.each(name) { |element|
553 if !element.attributes[attr]