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'
34 $CURRENT_CHARSET = `locale charmap`.chomp
35 $convert = 'convert -interlace line +profile "*"'
36 $convert_enhance = '-contrast -enhance -normalize'
39 return Iconv::iconv("UTF-8", $CURRENT_CHARSET, string).to_s
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
51 return Iconv::iconv($CURRENT_CHARSET, "UTF-8", string).to_s
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
61 def msg(verbose_level, msg)
62 if verbose_level <= $verbose_level
64 warn _("\t***ERROR***: %s\n") % msg
65 elsif verbose_level == 1
66 warn _("\tWarning: %s\n") % msg
73 def msg_(verbose_level, msg)
74 if verbose_level <= $verbose_level
76 warn _("\t***ERROR***: %s") % msg
77 elsif verbose_level == 1
78 warn _("\tWarning: %s") % msg
90 def select_theme(name, limit_sizes)
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
97 eval File.open("#{themedir}/metadata/parameters.rb").readlines.join
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.")
108 $images_size = $images_size.find_all { |e| !e['optional'] }
111 $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
112 if $default_size == nil
113 $default_size = $images_size[0]
117 def entry2type(entry)
118 if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i && entry !~ /['"\[\]]/
120 elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i && entry !~ /['"\[\]]/
121 #- might consider using file magic later..
134 finished = Process.wait2
135 $pids.delete(finished[0])
136 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
140 while $pids.length > 0
145 #- parallelizable sys
155 if $pids.length == $mproc
163 #- grab the results of a command but don't sleep forever on a runaway process
164 def subproc_runaway_aware(command)
169 rescue Timeout::Error
170 msg 1, _("forgetting runaway process (transcode sucks again?)")
171 #- todo should slay transcode but dunno how to do that
176 def get_image_size(fullpath)
177 if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
178 return { :x => $1.to_i, :y => $2.to_i }
184 #- commify from http://pleac.sourceforge.net/ (pleac rulz)
186 n.to_s =~ /([^\.]*)(\..*)?/
187 int, dec = $1.reverse, $2 ? $2 : ""
188 while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
193 def guess_rotate(filename)
194 orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
195 if orientation =~ /right - top/
197 elsif orientation =~ /left - bottom/
202 size = get_image_size(filename)
203 #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
204 if size && size[:x] < size[:y]
211 def rotate_pixbuf(pixbuf, angle)
212 return pixbuf.rotate(angle == 90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
213 angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
214 angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
215 Gdk::Pixbuf::ROTATE_NONE)
218 def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
219 if xmldirorelem.name == 'dir'
220 xmldirorelem = xmldirorelem.elements["[@filename='#{utf8(File.basename(orig))}']"]
222 gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
225 def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
226 #- type can be `subdirs' or `thumbnails'
227 gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
230 def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
231 if !dests.detect { |dest| !File.exists?(dest['filename']) }
236 dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
238 if entry2type(orig) == 'image'
240 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
241 neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
242 cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
244 if File.exists?(neworig)
248 rotate = felem.attributes["#{attributes_prefix}rotate"]
250 felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
252 convert_options += "-rotate #{rotate} "
253 if felem.attributes["#{attributes_prefix}enhance"]
254 convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
258 if !File.exists?(dest['filename'])
260 #- don't resize if image is already smaller than destination size
261 if size = get_image_size(orig)
262 dest['size'] =~ /(\d+)x(\d+)/
263 if (rotate == "90" || rotate == "270" || size[:x] < size[:y]) ? size[:y] < $1.to_i : size[:x] < $1.to_i
264 cmd = "#{$convert} #{convert_options} '#{orig}' '#{dest['filename']}'"
267 cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
279 system("rm -f '#{neworig}'")
283 elsif entry2type(orig) == 'video'
285 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
286 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
288 felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
290 frame_offset = frame_offset.to_i
291 if rotate = felem.attributes["#{attributes_prefix}rotate"]
292 convert_options += "-rotate #{rotate} "
294 if felem.attributes["#{attributes_prefix}enhance"]
295 convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
298 orig_image = "#{dest_dir}/screenshot.jpg000000.jpg"
300 if !File.exists?(orig_image)
301 transcode_options = ''
303 if felem.attributes["#{attributes_prefix}color-swap"]
304 transcode_options += '-k '
307 cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
309 results = subproc_runaway_aware(cmd)
310 if results =~ /skipping frames/ && results =~ /encoded 0 frames/
311 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another problem. try another value.") %
312 results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
314 elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
315 msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
316 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"
319 if File.exists?("#{dest_dir}/foo.avi")
320 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"
322 results = subproc_runaway_aware(cmd)
323 system("rm -f '#{dest_dir}/foo.avi'")
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 probleme. try another value.") %
326 results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
328 elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
329 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/foo.avi"
333 msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
337 if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
338 if whitebalance.to_f != 0
339 neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
340 cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}"
342 if File.exists?(neworig)
348 if !File.exists?(dest['filename'])
349 sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
353 system("rm -f '#{neworig}'")
359 def invornil(obj, methodname)
363 return obj.method(methodname).call
367 def find_subalbum_info_type(xmldir)
368 #- first look for subdirs info; if not, means there is no subdir
369 if xmldir.attributes['subdirs-caption']
376 def find_subalbum_caption_info(xmldir)
377 type = find_subalbum_info_type(xmldir)
378 return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
383 return File.size(path)
393 def substInFile(name)
394 newcontent = IO.readlines(name).collect { |l| yield l }
395 ios = File.open(name, "w")
396 ios.write(newcontent)
402 def File.reduce_path(path)
403 return path.gsub(/\w+\/\.\.\//, '')
408 def previous_element_byname(name)
410 while n = n.previous_element
418 def next_element_byname(name)
420 while n = n.next_element
428 def child_byname_notattr(name, attr)
429 elements.each(name) { |element|
430 if !element.attributes[attr]