prevent from reloading a non existant page if a user has several albums on the same...
[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|3gp|mp4)$/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     def waitjob
134         finished = Process.wait2
135         $pids.delete(finished[0])
136         $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
137     end
138
139     def waitjobs
140         while $pids.length > 0
141             waitjob
142         end
143     end
144
145     #- parallelizable sys
146     def psys(cmd)
147         if $mproc
148             if pid = fork
149                 $pids << pid
150             else
151                 msg 2, cmd + ' &'
152                 system(cmd)
153                 exit 0
154             end
155             if $pids.length == $mproc
156                 waitjob
157             end
158         else
159             sys(cmd)
160         end
161     end
162
163     #- grab the results of a command but don't sleep forever on a runaway process
164     def subproc_runaway_aware(command)
165         begin
166             timeout(5) {
167                 return `#{command}`
168             }
169         rescue Timeout::Error    
170             msg 1, _("forgetting runaway process (transcode sucks again?)")
171             #- todo should slay transcode but dunno how to do that
172             return nil
173         end
174     end
175
176     def get_image_size(fullpath)
177         if `identify '#{fullpath}'` =~ / JPEG (\d+)x(\d+) /
178             return { :x => $1.to_i, :y => $2.to_i }
179         else
180             return nil
181         end
182     end
183
184     #- commify from http://pleac.sourceforge.net/ (pleac rulz)
185     def commify(n)
186         n.to_s =~ /([^\.]*)(\..*)?/
187         int, dec = $1.reverse, $2 ? $2 : ""
188         while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2' + _(",") + '\3')
189         end
190         int.reverse + dec
191     end
192
193     def guess_rotate(filename)
194         orientation = `exif '#{filename}'`.detect { |line| line =~ /^Orientation/ }
195         if orientation =~ /right - top/
196             angle = 90
197         elsif orientation =~ /left - bottom/
198             angle = -90
199         else
200             return 0
201         end
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]
205             return 0
206         else
207             return angle
208         end
209     end
210
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)
216     end
217
218     def gen_thumbnails_element(orig, xmldirorelem, allow_background, dests)
219         if xmldirorelem.name == 'dir'
220             xmldirorelem = xmldirorelem.elements["[@filename='#{utf8(File.basename(orig))}']"]
221         end
222         gen_thumbnails(orig, allow_background, dests, xmldirorelem, '')
223     end
224
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 + '-')
228     end
229
230     def gen_thumbnails(orig, allow_background, dests, felem, attributes_prefix)
231         if !dests.detect { |dest| !File.exists?(dest['filename']) } 
232             return true
233         end
234
235         convert_options = ''
236         dest_dir = make_dest_filename(File.dirname(dests[0]['filename']))
237
238         if entry2type(orig) == 'image'
239             if felem
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}"
243                     sys(cmd)
244                     if File.exists?(neworig)
245                         orig = neworig
246                     end
247                 end
248                 rotate = felem.attributes["#{attributes_prefix}rotate"]
249                 if !rotate
250                     felem.add_attribute("#{attributes_prefix}rotate", rotate = guess_rotate(orig).to_i)
251                 end
252                 convert_options += "-rotate #{rotate} "
253                 if felem.attributes["#{attributes_prefix}enhance"]
254                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
255                 end
256             end
257             for dest in dests
258                 if !File.exists?(dest['filename'])
259                     cmd = nil
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']}'"
265                         end
266                     end
267                     cmd ||= "#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig}' '#{dest['filename']}'"
268                     if allow_background
269                         psys(cmd)
270                     else
271                         sys(cmd)
272                     end
273                 end
274             end
275             if neworig
276                 if allow_background
277                     waitjobs
278                 end
279                 system("rm -f '#{neworig}'")
280             end
281             return true
282
283         elsif entry2type(orig) == 'video'
284             if felem
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"]
287                 if !frame_offset
288                     felem.add_attribute("#{attributes_prefix}frame-offset", frame_offset = "0")
289                 end
290                 frame_offset = frame_offset.to_i
291                 if rotate = felem.attributes["#{attributes_prefix}rotate"]
292                     convert_options += "-rotate #{rotate} "
293                 end
294                 if felem.attributes["#{attributes_prefix}enhance"]
295                     convert_options += ($config['convert-enhance'] || $convert_enhance) + " "
296                 end
297             end
298             orig_image = "#{dest_dir}/screenshot.jpg000000.jpg"
299             for dest in dests
300                 if !File.exists?(orig_image)
301                     transcode_options = ''
302                     if felem
303                         if felem.attributes["#{attributes_prefix}color-swap"]
304                             transcode_options += '-k '
305                         end
306                     end
307                     cmd = "transcode -a 0 -c #{frame_offset}-#{frame_offset+1} -i '#{orig}' -y jpg -o '#{dest_dir}/screenshot.jpg' #{transcode_options} 2>&1"
308                     msg 2, cmd
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]
313                         return false
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"
317                         msg 2, cmd
318                         system cmd
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"
321                             msg 2, cmd
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]
327                                 return false
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"
330                                 return false
331                             end
332                         else
333                             msg 0, _("could not make mencoder to encode %s to mpeg4") % orig
334                             return false
335                         end
336                     end
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}"
341                             sys(cmd)
342                             if File.exists?(neworig)
343                                 orig_image = neworig
344                             end
345                         end
346                     end
347                 end
348                 if !File.exists?(dest['filename'])
349                     sys("#{$convert} #{convert_options}-size #{dest['size']} -resize #{dest['size']} '#{orig_image}' '#{dest['filename']}'")
350                 end
351             end
352             if neworig
353                 system("rm -f '#{neworig}'")
354             end
355             return true
356         end
357     end
358
359     def invornil(obj, methodname)
360         if obj == nil
361             return nil
362         else
363             return obj.method(methodname).call
364         end
365     end
366
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']
370             return 'subdirs'
371         else
372             return 'thumbnails'
373         end
374     end
375
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"] ]
379     end
380
381     def file_size(path)
382         begin
383             return File.size(path)
384         rescue
385             return -1
386         end
387     end
388
389     def max(a, b)
390         a > b ? a : b
391     end
392
393     def substInFile(name)
394         newcontent = IO.readlines(name).collect { |l| yield l }
395         ios = File.open(name, "w")
396         ios.write(newcontent)
397         ios.close
398     end
399 end
400
401 class File
402     def File.reduce_path(path)
403         return path.gsub(/\w+\/\.\.\//, '')
404     end
405 end
406
407 class REXML::Element
408     def previous_element_byname(name)
409         n = self
410         while n = n.previous_element
411             if n.name == name
412                 return n
413             end
414         end
415         return nil
416     end
417
418     def next_element_byname(name)
419         n = self
420         while n = n.next_element
421             if n.name == name
422                 return n
423             end
424         end
425         return nil
426     end
427
428     def child_byname_notattr(name, attr)
429         elements.each(name) { |element|
430             if !element.attributes[attr]
431                 return element
432             end
433         }
434         return nil
435     end
436 end
437
438