remember deleted elements to not merge them back when using "merge new/removed images...
[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("%2X", 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)
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         $default_size = $images_size.detect { |sizeobj| sizeobj['default'] }
112         if $default_size == nil
113             $default_size = $images_size[0]
114         end
115     end
116
117     def entry2type(entry)
118         if entry =~ /\.(jpg|jpeg|jpe|gif|bmp|png)$/i && entry !~ /['"\[\]]/
119             return 'image'
120         elsif entry =~ /\.(mov|avi|mpg|mpeg|mpe|wmv|asx)$/i && entry !~ /['"\[\]]/
121             #- might consider using file magic later..
122             return 'video'
123         else
124             return nil
125         end
126     end
127
128     def sys(cmd)
129         msg 2, cmd
130         system(cmd)
131     end
132
133     #- parallelizable sys
134     def psys(cmd)
135         if $mproc
136             if pid = fork
137                 $pids << pid
138             else
139                 msg 2, cmd + ' &'
140                 system(cmd)
141                 exit 0
142             end
143             if $pids.length == $mproc
144                 finished = Process.wait2
145                 $pids.delete(finished[0])
146                 $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
147             end
148         else
149             sys(cmd)
150         end
151     end
152
153     #- grab the results of a command but don't sleep forever on a runaway process
154     def subproc_runaway_aware(command)
155         begin
156             timeout(5) {
157                 return `#{command}`
158             }
159         rescue Timeout::Error    
160             msg 1, _("forgetting runaway process (transcode sucks again?)")
161             #- todo should slay transcode but dunno how to do that
162             return nil
163         end
164     end
165
166     def get_image_size(fullpath)
167         if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
168             return { :x => $1.to_i, :y => $2.to_i }
169         else
170             return nil
171         end
172     end
173
174     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
175     def commify(n)
176         n.to_s =~ /([^\.]*)(\..*)?/
177         int, dec = $1.reverse, $2 ? $2 : ""
178         while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
179         end
180         int.reverse + dec
181     end
182
183     def guess_rotate(filename)
184         orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
185         if orientation =~ /right - top/
186             angle = 90
187         elsif orientation =~ /left - bottom/
188             angle = -90
189         else
190             return 0
191         end
192         size = get_image_size(filename)
193         #- remove rotate if image is obviously already in portrait (situation can come from gthumb)
194         if size && size[:x] < size[:y]
195             return 0
196         else
197             return angle
198         end
199     end
200
201     def rotate_pixbuf(pixbuf, angle)
202         return pixbuf.rotate(angle ==  90 ? Gdk::Pixbuf::ROTATE_CLOCKWISE :
203                              angle == 180 ? Gdk::Pixbuf::ROTATE_UPSIDEDOWN :
204                              angle == 270 ? Gdk::Pixbuf::ROTATE_COUNTERCLOCKWISE :
205                                             Gdk::Pixbuf::ROTATE_NONE)
206     end
207
208     def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
209         if xmldirorelem.name == 'dir'
210             xmldirorelem = xmldirorelem.elements["[@filename='#{utf8(File.basename(orig))}']"]
211         end
212         gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
213     end
214
215     def gen_thumbnails_subdir(orig, xmldirorelem, allow_background, dests, type)
216         #- type can be `subdirs' or `thumbnails' 
217         gen_thumbnails(orig, allow_background, dests, xmldirorelem, type + '-')
218     end
219
220     def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
221         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
222             return true
223         end
224
225         convert_options = ''
226         dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
227
228         if entry2type(orig) == 'image'
229             if felem
230                 if whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
231                     neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
232                     cmd = "booh-fix-whitebalance '#{orig}' '#{neworig}' #{whitebalance}"
233                     sys(cmd)
234                     if File.exists?(neworig)
235                         orig = neworig
236                         allow_background = false
237                     end
238                 end
239                 rotate = felem.attributes["#{attributes_prefix}rotate"]
240                 if !rotate
241                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
242                 end
243                 convert_options += "-rotate #{rotate} "
244                 if felem.attributes["#{attributes_prefix}enhance"]
245                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
246                 end
247             end
248             for dest in dests
249                 if !File.exists?(dest['filename'])
250                     cmd = nil
251                     #- don't resize if image is already smaller than destination size
252                     if size = get_image_size(orig)
253                         dest['size'] =~ /(\d+)x(\d+)/
254                         if (rotate == "90" || rotate == "270" || size[:x] < size[:y]) ? size[:y] < $1.to_i : size[:x] < $1.to_i
255                             cmd = "#{$convert} #{convert_options} '#{orig}' '#{dest['filename']}'"
256                         end
257                     end
258                     cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
259                     if allow_background
260                         psys(cmd)
261                     else
262                         sys(cmd)
263                     end
264                 end
265             end
266             if neworig
267                 system("rm -f '#{neworig}'")
268             end
269             return true
270
271         elsif entry2type(orig) == 'video'
272             if felem
273                 #- frame-offset is an attribute that allows to specify which frame to use for the thumbnail
274                 frame_offset = felem.attributes["#{attributes_prefix}frame-offset"]
275                 if !frame_offset
276                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
277                 end
278                 frame_offset = frame_offset.to_i
279                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
280                     convert_options += "-rotate #{rotate} "
281                 end
282                 if felem.attributes["#{attributes_prefix}enhance"]
283                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
284                 end
285             end
286             orig_image = "#{dest_dir}/screenshot.jpg000000.jpg"
287             for dest in dests
288                 if !File.exists?(orig_image)
289                     transcode_options = ''
290                     if felem
291                         if felem.attributes["#{attributes_prefix}color-swap"]
292                             transcode_options += '-k '
293                         end
294                     end
295                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
296                     msg 2, cmd
297                     results = subproc_runaway_aware(cmd)
298                     if results =~ /skipping frames/ && results =~ /encoded 0 frames/
299                         msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another problem. try another value.") %
300                                results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
301                         return false
302                     elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
303                         msg 2, _("* could not extract first image of video %s with transcode, will try first converting with mencoder") % orig
304                         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"
305                         msg 2, cmd
306                         system cmd
307                         if File.exists?("#{dest_dir}/foo.avi")
308                             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"
309                             msg 2, cmd
310                             results = subproc_runaway_aware(cmd)
311                             system("rm -f '#{dest_dir}/foo.avi'")
312                             if results =~ /skipping frames/ && results =~ /encoded 0 frames/
313                                 msg 0, _("specified frame-offset too large? max frame was: %s. that may also be another probleme. try another value.") %
314                                        results.scan(/skipping frames \[000000-(\d+)\]/)[-1]
315                                 return false
316                             elsif results =~ /V: import format.*unknown/ || !File.exists?("#{dest_dir}/screenshot.jpg000000.jpg")
317                                 msg 0, _("could not extract first image of video %s encoded by mencoder") % "#{dest_dir}/foo.avi"
318                                 return false
319                             end
320                         else
321                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
322                             return false
323                         end
324                     end
325                     if felem && whitebalance = felem.attributes["#{attributes_prefix}white-balance"]
326                         if whitebalance.to_f != 0
327                             neworig = "#{dest_dir}/#{File.basename(orig)}-whitebalance#{whitebalance}.jpg"
328                             cmd = "booh-fix-whitebalance '#{orig_image}' '#{neworig}' #{whitebalance}"
329                             sys(cmd)
330                             if File.exists?(neworig)
331                                 orig_image = neworig
332                             end
333                         end
334                     end
335                 end
336                 if !File.exists?(dest['filename'])
337                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
338                 end
339             end
340             if neworig
341                 system("rm -f '#{neworig}'")
342             end
343             return true
344         end
345     end
346
347     def invornil(obj, methodname)
348         if obj == nil
349             return nil
350         else
351             return obj.method(methodname).call
352         end
353     end
354
355     def find_subalbum_info_type(xmldir)
356         #- first look for subdirs info; if not, means there is no subdir
357         if xmldir.attributes['subdirs-caption']
358             return 'subdirs'
359         else
360             return 'thumbnails'
361         end
362     end
363
364     def find_subalbum_caption_info(xmldir)
365         type = find_subalbum_info_type(xmldir)
366         return [ from_utf8(xmldir.attributes["#{type}-captionfile"]), xmldir.attributes["#{type}-caption"] ]
367     end
368
369     def file_size(path)
370         begin
371             return File.size(path)
372         rescue
373             return -1
374         end
375     end
376
377     def max(a, b)
378         a > b ? a : b
379     end
380
381     def substInFile(name)
382         newcontent = IO.readlines(name).collect { |l| yield l }
383         ios = File.open(name, "w")
384         ios.write(newcontent)
385         ios.close
386     end
387 end
388
389 class File
390     def File.reduce_path(path)
391         return path.gsub(/\w+\/\.\.\//, '')
392     end
393 end
394
395 class REXML::Element
396     def previous_element_byname(name)
397         n = self
398         while n = n.previous_element
399             if n.name == name
400                 return n
401             end
402         end
403         return nil
404     end
405
406     def next_element_byname(name)
407         n = self
408         while n = n.next_element
409             if n.name == name
410                 return n
411             end
412         end
413         return nil
414     end
415
416     def child_byname_notattr(name, attr)
417         elements.each(name) { |element|
418             if !element.attributes[attr]
419                 return element
420             end
421         }
422         return nil
423     end
424 end
425
426