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