# * BOOH * # # A.k.a `Best web-album Of the world, Or your money back, Humerus'. # # The acronyn sucks, however this is a tribute to Dragon Ball by # Akira Toriyama, where the last enemy beaten by heroes of Dragon # Ball is named "Boo". But there was already a free software project # called Boo, so this one will be it "Booh". Or whatever. # # # Copyright (c) 2004 Guillaume Cottenceau # # This software may be freely redistributed under the terms of the GNU # public license version 2. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. require 'iconv' require 'timeout' require 'rexml/document' require 'gettext' include GetText bindtextdomain("booh") require 'booh/config.rb' require 'booh/version.rb' module Booh $verbose_level = 2 $CURRENT_CHARSET = `locale charmap`.chomp $convert = 'convert -interlace line +profile "*"' $convert_enhance = '-contrast -enhance -normalize' def utf8(string) return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string).to_s end def utf8cut(string, maxlen) begin return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string[0..maxlen-1]).to_s rescue Iconv::InvalidCharacter return utf8cut(string, maxlen-1) end end def sizename(key) #- fake for gettext to find these; if themes need more sizes, english name for them should be added here sizenames = { 'small' => utf8(_("small")), 'medium' => utf8(_("medium")), 'large' => utf8(_("large")), 'x-large' => utf8(_("x-large")), 'xx-large' => utf8(_("xx-large")), 'original' => utf8(_("original")) } return sizenames[key] || key end def from_utf8(string) return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s end def from_utf8_safe(string) begin return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s rescue Iconv::IllegalSequence return nil end end def make_dest_filename_old(orig_filename) #- we remove non alphanumeric characters but need to do that #- cleverly to not end up with two similar dest filenames. we won't #- urlencode because urldecode might happen in the browser. return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("%2X", v) }.to_s end def make_dest_filename(orig_filename) #- we remove non alphanumeric characters but need to do that #- cleverly to not end up with two similar dest filenames. we won't #- urlencode because urldecode might happen in the browser. return orig_filename.unpack("C*").collect { |v| v.chr =~ /[a-zA-Z\-_0-9\.\/]/ ? v.chr : sprintf("~%02X", v) }.to_s end def msg(verbose_level, msg) if verbose_level <= $verbose_level if verbose_level == 0 warn _("\t***ERROR***: %s\n") % msg elsif verbose_level == 1 warn _("\tWarning: %s\n") % msg else puts msg end end end def msg_(verbose_level, msg) if verbose_level <= $verbose_level if verbose_level == 0 warn _("\t***ERROR***: %s") % msg elsif verbose_level == 1 warn _("\tWarning: %s") % msg else print msg end end end def die_(msg) puts msg exit 1 end def select_theme(name, limit_sizes, optimizefor32, nperrow) $theme = name msg 3, _("Selecting theme '%s'") % $theme themedir = "#{$FPATH}/themes/#{$theme}" if !File.directory?(themedir) die _("Theme was not found (tried %s directory).") % themedir end eval File.open("#{themedir}/metadata/parameters.rb").readlines.join if limit_sizes if limit_sizes != 'all' sizes = limit_sizes.split(/,/) $images_size = $images_size.find_all { |e| sizes.include?(e['name']) } if $images_size.length == 0 die _("Can't carry on, no valid size selected.") end end else $images_size = $images_size.find_all { |e| !e['optional'] } end if optimizefor32 $images_size.each { |e| e['fullscreen'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s } e['thumbnails'].gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s } } $albums_thumbnail_size.gsub!(/(\d+x)(\d+)/) { $1 + ($2.to_f*8/9).to_i.to_s } end if nperrow && nperrow != $default_N ratio = nperrow.to_f / $default_N.to_f $images_size.each { |e| e['thumbnails'].gsub!(/(\d+)x(\d+)/) { ($1.to_f/ratio).to_i.to_s + 'x' + ($2.to_f/ratio).to_i.to_s } } end $default_size = $images_size.detect { |sizeobj| sizeobj['default'] } if $default_size == nil $default_size = $images_size[0] end end def entry2type(entry) if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i && entry !~ /['"\[\]]/ return 'image' elsif !$ignore_videos && entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx|3gp|mp4)$/i && entry !~ /['"\[\]]/ #- might consider using file magic later.. return 'video' else return nil end end def sys(cmd) msg 2, cmd system(cmd) end def waitjob finished = Process.wait2 $pids.delete(finished[0]) $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil } end def waitjobs while $pids && $pids.length > 0 waitjob end end #- parallelizable sys def psys(cmd) if $mproc if pid = fork $pids << pid else msg 2, cmd + ' &' system(cmd) exit 0 end if $pids.length == $mproc waitjob end else sys(cmd) end end #- grab the results of a command but don't sleep forever on a runaway process def subproc_runaway_aware(command) begin timeout(10) { return `#{command}` } rescue Timeout::Error msg 1, _("forgetting runaway process (transcode sucks again?)") #- todo should slay transcode but dunno how to do that return nil end end def get_image_size(fullpath) if !$no_identify && `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) / return { :x => $1.to_i, :y => $2.to_i } else return nil end end #- commify from http://pleac.sourceforge.net/ (pleac rulz) def commify(n) n.to_s =~ /([^\.]*)(\..*)?/ int, dec = $1.reverse, $2 ? $2 : "" while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3') end int.reverse + dec end def guess_rotate(filename) if $no_identify return 0 end orientation = `identify -format "%[EXIF:orientation]" '#{filename}'`.chomp if orientation == '6' angle = 90 elsif orientation == '8' angle = -90 else return 0 end size = get_image_size(filename) #- remove rotate if image is obviously already in portrait (situation can come from gthumb) if size && size[:x] < size[:y] return 0 else return angle end end def rotate_pixbuf(pixbuf, angle) return pixbuf.rotate(angle == 90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE : angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN : angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE : Gdk::Pixbuf::ROTATE_NONE) end def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests) if xmldirorelem.name == 'dir' xmldirorelem = xmldirorelem.elements["*[@filename='#{utf8(File.basename(orig))}']"] end gen_thumbnails(orig, allow_background, dests, xmldirorelem, '') end def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type) #- type can be `subdirs' or `thumbnails' gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-') end def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix) if !dests.detect { |dest| !File.exists?(dest['filename']) } return true end convert_options = '' dest_dir = make_dest_filename(File.dirname(dests[0]['filename'])) if entry2type(orig) == 'image' if felem if whitebalance = felem.attributes["#{attributes_prefix}white-balance"] neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg" cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}" sys(cmd) if File.exists?(neworig) orig = neworig end end rotate = felem.attributes["#{attributes_prefix}rotate"] if !rotate felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i) end convert_options += "-rotate #{rotate} " if felem.attributes["#{attributes_prefix}enhance"] convert_options += ($config['convert-enhance'] || $convert_enhance) + " " end end for dest in dests if !File.exists?(dest['filename']) cmd = nil cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize '#{dest['size']}>' '#{orig}' '#{dest['filename']}'" if allow_background psys(cmd) else sys(cmd) end end end if neworig if allow_background waitjobs end system("rm -f '#{neworig}'") end return true elsif entry2type(orig) == 'video' if felem #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail frame_offset = felem.attributes["#{attributes_prefix}frame-offset"] if !frame_offset felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0") end frame_offset = frame_offset.to_i if rotate = felem.attributes["#{attributes_prefix}rotate"] convert_options += "-rotate #{rotate} " end if felem.attributes["#{attributes_prefix}enhance"] convert_options += ($config['convert-enhance'] || $convert_enhance) + " " end end orig_base = File.basename(dests[0]['filename']) + File.basename(orig) orig_image = "#{dest_dir}/#{orig_base}.jpg000000.jpg" system("rm -f '#{orig_image}'") for dest in dests if !File.exists?(orig_image) transcode_options = '' if felem if felem.attributes["#{attributes_prefix}color-swap"] transcode_options += '-k ' end end cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/#{orig_base}.jpg' #{transcode_options} 2>&1" msg 2, cmd results = subproc_runaway_aware(cmd) if results =~ /skipping frames/ && results =~ /encoded 0 frames/ msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another problem. try another value.") % results.scan(/skipping frames \[000000-(\d+)\]/)[-1] return false elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image) msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig 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" msg 2, cmd system cmd if File.exists?("#{dest_dir}/#{orig_base}.avi") 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" msg 2, cmd results = subproc_runaway_aware(cmd) system("rm -f '#{dest_dir}/#{orig_base}.avi'") if results =~ /skipping frames/ && results =~ /encoded 0 frames/ msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") % results.scan(/skipping frames \[000000-(\d+)\]/)[-1] return false elsif results =~ /V: import format.*unknown/ || !File.exists?(orig_image) msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/#{orig_base}.avi" return false end else msg 0, _("could not make mencoder to encode %s to mpeg4") % orig return false end end if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"] if whitebalance.to_f != 0 neworig = "#{dest_dir}/#{orig_base}-whitebalance#{whitebalance}.jpg" cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}" sys(cmd) if File.exists?(neworig) orig_image = neworig end end end end if !File.exists?(dest['filename']) sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'") end end if neworig system("rm -f '#{neworig}'") end return true end end def invornil(obj, methodname) if obj == nil return nil else return obj.method(methodname).call end end def find_subalbum_info_type(xmldir) #- first look for subdirs info; if not, means there is no subdir if xmldir.attributes['subdirs-caption'] return 'subdirs' else return 'thumbnails' end end def find_subalbum_caption_info(xmldir) type = find_subalbum_info_type(xmldir) return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ] end def file_size(path) begin return File.size(path) rescue return -1 end end def max(a, b) a > b ? a : b end def clamp(n, a, b) n < a ? a : n > b ? b : n end def pano_amount(elem) if pano_amount = elem.attributes['pano-amount'] if $N_per_row return clamp(pano_amount.to_f, 1, $N_per_row.to_i) else return clamp(pano_amount.to_f, 1, $default_N.to_i) end else return nil end end def substInFile(name) newcontent = IO.readlines(name).collect { |l| yield l } ios = File.open(name, "w") ios.write(newcontent) ios.close end end class File def File.reduce_path(path) return path.gsub(/\w+\/\.\.\//, '') end end class REXML::Element def previous_element_byname(name) n = self while n = n.previous_element if n.name == name return n end end return nil end def previous_element_byname_notattr(name, attr) n = self while n = n.previous_element if n.name == name && !n.attributes[attr] return n end end return nil end def next_element_byname(name) n = self while n = n.next_element if n.name == name return n end end return nil end def next_element_byname_notattr(name, attr) n = self while n = n.next_element if n.name == name && !n.attributes[attr] return n end end return nil end def child_byname_notattr(name, attr) elements.each(name) { |element| if !element.attributes[attr] return element end } return nil end end