support panoramas
authorgc <gc>
Tue, 25 Oct 2005 20:24:28 +0000 (20:24 +0000)
committergc <gc>
Tue, 25 Oct 2005 20:24:28 +0000 (20:24 +0000)
bin/booh
bin/booh-backend
data/booh/themes/dark/metadata/parameters.rb
data/booh/themes/simple/metadata/parameters.rb
data/booh/themes/simple/skeleton_thumbnails.html
lib/booh/booh-lib.rb

index d09a0c6d8502ae1c9b864d556b2c85a7c8f54f13..9232874bd193f8e2229b03210e450f8e4f7bcf74 100755 (executable)
--- a/bin/booh
+++ b/bin/booh
@@ -537,6 +537,70 @@ from. There are approximately 25 frames per second in a video.
     }
 end
 
+def change_pano_amount(xmldir, attributes_prefix, value)
+    $modified = true
+    if value.nil?
+        xmldir.delete_attribute("#{attributes_prefix}pano-amount")
+    else
+        xmldir.add_attribute("#{attributes_prefix}pano-amount", value)
+    end
+end
+
+def ask_new_pano_amount(xmldir, attributes_prefix)
+    if xmldir
+        value = xmldir.attributes["#{attributes_prefix}pano-amount"]
+    else
+        value = ''
+    end
+
+    dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
+of this panorama image compared to other regular images. For example, if the panorama
+was taken out of four photos on one row, counting the necessary overlap, the width of
+this panorama image should probably be roughly three times the width of regular images.
+
+With this information, booh will be able to generate panorama thumbnails looking
+the right 'size'.
+"))
+    dialog.vbox.add(lbl)
+    dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
+                                                                         add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
+                                                                         add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
+                                                                         add(Gtk::Label.new(utf8(_("times the width of other images"))))))
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.show_all
+    if value
+        spin.value = value.to_f
+        rb_yes.active = true
+        spin.grab_focus
+    else
+        rb_no.active = true
+    end
+
+    dialog.run { |response|
+        if rb_no.active?
+            newval = nil
+        else
+            newval = spin.value.to_f
+        end
+        dialog.destroy
+        if response == Gtk::Dialog::RESPONSE_OK
+            $modified = true
+            msg 3, "changing panorama amount to #{newval}"
+            return { :old => value, :new => newval }
+        else
+            return nil
+        end
+    }
+end
+
 def change_whitebalance(xmlelem, attributes_prefix, value)
     $modified = true
     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
@@ -630,6 +694,7 @@ def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
         push_mousecursor_wait
         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
         gtk_thread_protect {
+            puts "destroyed: " + img.destroyed?.to_s
             img.set(destfile)
             $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
         }
@@ -744,6 +809,11 @@ def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, poss
     end
     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
     enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
+    if type == 'image' && possible_actions[:can_panorama]
+        menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
+        panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
+        panorama.signal_connect('activate') { closures[:pano].call }
+    end
     if optionals.include?('delete')
         menu.append(               Gtk::SeparatorMenuItem.new)
         menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
@@ -894,6 +964,33 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
         end
     }
 
+    change_pano_amount_and_cleanup_real = Proc.new { |values|
+        perform_change_pano_amount_and_cleanup = Proc.new { |val|
+            change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
+        }
+        perform_change_pano_amount_and_cleanup.call(values[:new])
+        
+        save_undo(_("change panorama amount"),
+                  Proc.new {
+                      perform_change_pano_amount_and_cleanup.call(values[:old])
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      Proc.new {
+                          perform_change_pano_amount_and_cleanup.call(values[:new])
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+
+    change_pano_amount_and_cleanup = Proc.new {
+        if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
+            change_pano_amount_and_cleanup_real.call(values)
+        end
+    }
+
     whitebalance_and_cleanup = Proc.new {
         if values = ask_whitebalance(fullpath, thumbnail_img, img,
                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
@@ -1036,7 +1133,8 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
-                                 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real }
+                                 :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup_real,
+                                 :pano => change_pano_amount_and_cleanup }
 
     textview.signal_connect('key-press-event') { |w, event|
         propagate = true
@@ -1151,10 +1249,11 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
                 next_ = autotable.get_next_widget(vbox)
                 popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["*[@filename='#{filename}']"], '',
                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
-                                       :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true },
+                                       :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
                                        :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
-                                       :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) } })
+                                       :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
+                                       :pano => change_pano_amount_and_cleanup })
             end
             $ignore_next_release = false
             $gesture_press = nil
@@ -1541,32 +1640,32 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
                     elements += sizes
                 end
                 element_counter = 0
-                gtk_thread_protect { pb1_1.fraction = 0 }
+                gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 0 }
                 if mode != 'one dir scan'
                     newtext = utf8(full_src_dir_to_rel($1, $2))
                     newtext = '/' if newtext == ''
-                    gtk_thread_protect { pb1_2.text = newtext }
+                    gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = newtext }
                     directories_counter += 1
-                    gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
+                    gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.fraction = directories_counter / directories }
                 end
             elsif line =~ /^processing element$/
                 element_counter += 1
-                gtk_thread_protect { pb1_1.fraction = element_counter / elements }
+                gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
             elsif line =~ /^processing size$/
                 element_counter += 1
-                gtk_thread_protect { pb1_1.fraction = element_counter / elements }
+                gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = element_counter / elements }
             elsif line =~ /^finished processing sizes$/
-                gtk_thread_protect { pb1_1.fraction = 1 }
+                gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = 1 }
             elsif line =~ /^creating index.html$/
-                gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
-                gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
+                gtk_thread_protect { puts "destroyed: " + pb1_2.destroyed?.to_s; pb1_2.text = utf8(_("finished")) }
+                gtk_thread_protect { puts "destroyed: " + pb1_1.destroyed?.to_s; pb1_1.fraction = pb1_2.fraction = 1 }
                 directories_counter = 0
             elsif line =~ /^index.html: (.+)\|(.+)/
                 newtext = utf8(full_src_dir_to_rel($1, $2))
                 newtext = '/' if newtext == ''
-                gtk_thread_protect { pb2.text = newtext }
+                gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.text = newtext }
                 directories_counter += 1
-                gtk_thread_protect { pb2.fraction = directories_counter / directories }
+                gtk_thread_protect { puts "destroyed: " + pb2.destroyed?.to_s; pb2.fraction = directories_counter / directories }
             end
         end
     }
@@ -1598,7 +1697,7 @@ def call_backend(cmd, waitmsg, mode, params)
         msg 2, cmd
         if pid = fork
             id, exitstatus = Process.waitpid2(pid)
-            gtk_thread_protect { w8.destroy }
+            gtk_thread_protect { puts "destroyed: " + w8.destroyed?.to_s; w8.destroy }
             if exitstatus == 0
                 if params[:successmsg]
                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
@@ -2336,7 +2435,7 @@ def new_album
             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
                 if File.readable?(from_utf8(src_nb_calculated_for))
                     src_nb_thread = Thread.new {
-                        gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
+                        gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
                         total = { 'image' => 0, 'video' => 0, nil => 0 }
                         `find '#{from_utf8(src_nb_calculated_for)}' -type d -follow`.each { |dir|
                             if File.basename(dir) =~ /^\./
@@ -2350,7 +2449,7 @@ def new_album
                                 end
                             end
                         }
-                        gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
+                        gtk_thread_protect { puts "destroyed: " + src_nb.destroyed?.to_s; src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
                         src_nb_thread = nil
                     }
                 else
index a9b4b7645e8c7fe1950bbaf46c9420cdf31d77cd..35ec7b4f26b8b1b9c01267db57af22cf05d87878 100755 (executable)
@@ -406,24 +406,35 @@ end
 
 def reset_iterations(iterations)
     for iter in iterations.values
-        iter['value'] = 1
+        iter['value'] = 0
     end
 end
 
-def run_iterations(iterations)
+def run_iterations(iterations, amount)
     html = ''
+    should_rerun = false
     for level in iterations.keys.sort
-        if iterations[level]['value'] == 1 || level == iterations.keys.max
+        if iterations[level]['value'] == 0
             html += iterations[level]['opening']
+        elsif level == iterations.keys.max
+            if !iterations[level]['max'] || iterations[level]['max'] && iterations[level]['value'] + amount <= iterations[level]['max'].to_i
+                html += iterations[level]['opening']
+            else
+                should_rerun = true
+            end
         end
-        iterations[level]['value'] += 1
+        iterations[level]['value'] += amount
         if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
-            iterations[level]['value'] = 1
-            iterations[level-1]['value'] = 1
+            iterations[level]['value'] = 0
+            iterations[level-1]['value'] = 0
             html += iterations[level-1]['closing']
         end
     end
-    return html
+    if should_rerun
+        return html + run_iterations(iterations, amount)
+    else
+        return html
+    end
 end
 
 def close_iterations(iterations)
@@ -696,6 +707,8 @@ def walk_source_dir
             thumbnail_videos ||= {}
             thumbnail_videos[sizeobj['name']] = []
         end
+        #- a special dummy size to keep 'references' to thumbnails in case of panorama, because the GUI will use the regular thumbnails
+        thumbnail_images['dont-delete-file-for-gui'] = []
         if $limit_sizes =~ /original/
             fullscreen_images['original'] = []
         end
@@ -711,14 +724,19 @@ def walk_source_dir
                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
             else
                 todo = []
+                elem = xmldir.elements["image[@filename='#{utf8(img)}']"]
                 for sizeobj in $images_size
                     size_fullscreen = sizeobj['fullscreen']
                     size_thumbnails = sizeobj['thumbnails']
                     fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
-                    thumbnail_dest_img  = base_dest_img + "-#{size_thumbnails}.jpg"
                     fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
-                    thumbnail_images[sizeobj['name']]  << File.basename(thumbnail_dest_img)
                     todo << { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen }
+                    if pano = pano_amount(elem)
+                        thumbnail_images['dont-delete-file-for-gui'] << File.basename(base_dest_img + "-#{size_thumbnails}.jpg")
+                        size_thumbnails = size_thumbnails.sub(/(\d+)/) { ($1.to_i * pano).to_i }
+                    end
+                    thumbnail_dest_img = base_dest_img + "-#{size_thumbnails}.jpg"
+                    thumbnail_images[sizeobj['name']] << File.basename(thumbnail_dest_img)
                     todo << { 'filename' => thumbnail_dest_img,  'size' => size_thumbnails }
                 end
                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, todo)
@@ -795,7 +813,18 @@ def walk_source_dir
                 for file in entries
                     type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
                     if type
-                        html_thumbnails += run_iterations(iterations)
+                        if type == 'image' && elem = xmldir.elements["image[@filename='#{utf8(file)}']"]
+                            if  pano = pano_amount(elem)
+                                html_thumbnails += run_iterations(iterations, pano)
+                                html_thumbnails.gsub!(/~~colspan~~/) { 'colspan="' + pano.ceil.to_s + '"' }
+                            else
+                                html_thumbnails += run_iterations(iterations, 1)
+                                html_thumbnails.gsub!(/~~colspan~~/, '')
+                            end
+                        else 
+                            html_thumbnails += run_iterations(iterations, 1)
+                            html_thumbnails.gsub!(/~~colspan~~/, '')
+                        end
                         if type == 'image'
                             index = images.index(file)
                             html_thumbnails.gsub!(/~~image_iteration~~/,
@@ -976,7 +1005,7 @@ def walk_source_dir
                 thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
                 gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
-                html_index += run_iterations(iterations)
+                html_index += run_iterations(iterations, 1)
                 html_index.gsub!(/~~image_iteration~~/, "<a href='thumbnails.html'>" + img_element(thumbnail) + '</a>')
                 html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
             end
@@ -988,7 +1017,7 @@ def walk_source_dir
                 end
                 subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
                 thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
-                html_index += run_iterations(iterations)
+                html_index += run_iterations(iterations, 1)
                 captionfile, caption = find_subalbum_caption_info(child)
                 gen_thumbnails_subdir(captionfile, child, false,
                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
index 4e67d3b9b35582b9103aed38e8bf88e8d3be8b9e..80bde98318dba971bff76848fe1b5eb690fdb9b5 100644 (file)
@@ -25,45 +25,45 @@ bindtextdomain("booh")
 #- it's necessary to fit according to the typical space taken by
 #- widgets defined in the skeleton of the theme
 #-
-#- ***IMPORTANT***: CHOOSE 4/3 ASPECT RATIO SIZES!
+#- ***IMPORTANT***: CHOOSE 4/3 ASPECT RATIO SIZES (for thumbnails)!
 $images_size = [
     {
         'name' => 'small',
-        'description' => _("Fullscreen 552x414, thumbnails 192x144, should fit 800x600 screens"),
-        'fullscreen' => '552x414',
+        'description' => _("Sizes that should fit browsers in fullscreen for 800x600 screens"),
+        'fullscreen' => '750x414',
         'thumbnails' => '192x144',
         'optional' => true,
     },
     {
         'name' => 'medium',
-        'description' => _("Fullscreen 704x528, thumbnails 240x180, should fit 1024x768 screens"),
-        'fullscreen' => '704x528',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1024x768 screens"),
+        'fullscreen' => '960x528',
         'thumbnails' => '240x180',
         'default' => true,
     },
     {
         'name' => 'large',
-        'description' => _("Fullscreen 880x660, thumbnails 300x225, should fit 1280x1024 screens"),
-        'fullscreen' => '880x660',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1280x1024 screens"),
+        'fullscreen' => '1200x660',
         'thumbnails' => '300x225',
     },
     {
         'name' => 'x-large',
-        'description' => _("Fullscreen 962x721, thumbnails 328x245, should fit 1400x1050 screens"),
-        'fullscreen' => '962x721',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1400x1050 screens"),
+        'fullscreen' => '1312x721',
         'thumbnails' => '328x245',
         'optional' => true,
     },
     {
         'name' => 'xx-large',
-        'description' => _("Fullscreen 1100x825, thumbnails 375x281, should fit 1600x1200 screens"),
-        'fullscreen' => '1100x825',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1600x1200 screens"),
+        'fullscreen' => '1500x825',
         'thumbnails' => '375x281',
         'optional' => true,
     }
 ]
 
-$allowed_N_values = [ 3, 4, 6, 8 ]
+$allowed_N_values = [ 3, 4, 5, 6, 8, 12 ]
 $default_N = 4
 
 $albums_thumbnail_size = '300x225'
index 543ee2ed44401f8fa0fcfd61140cd63d6f4a433c..80bde98318dba971bff76848fe1b5eb690fdb9b5 100644 (file)
@@ -25,39 +25,39 @@ bindtextdomain("booh")
 #- it's necessary to fit according to the typical space taken by
 #- widgets defined in the skeleton of the theme
 #-
-#- ***IMPORTANT***: CHOOSE 4/3 ASPECT RATIO SIZES!
+#- ***IMPORTANT***: CHOOSE 4/3 ASPECT RATIO SIZES (for thumbnails)!
 $images_size = [
     {
         'name' => 'small',
-        'description' => _("Fullscreen 552x414, thumbnails 192x144, should fit 800x600 screens"),
-        'fullscreen' => '552x414',
+        'description' => _("Sizes that should fit browsers in fullscreen for 800x600 screens"),
+        'fullscreen' => '750x414',
         'thumbnails' => '192x144',
         'optional' => true,
     },
     {
         'name' => 'medium',
-        'description' => _("Fullscreen 704x528, thumbnails 240x180, should fit 1024x768 screens"),
-        'fullscreen' => '704x528',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1024x768 screens"),
+        'fullscreen' => '960x528',
         'thumbnails' => '240x180',
         'default' => true,
     },
     {
         'name' => 'large',
-        'description' => _("Fullscreen 880x660, thumbnails 300x225, should fit 1280x1024 screens"),
-        'fullscreen' => '880x660',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1280x1024 screens"),
+        'fullscreen' => '1200x660',
         'thumbnails' => '300x225',
     },
     {
         'name' => 'x-large',
-        'description' => _("Fullscreen 962x721, thumbnails 328x245, should fit 1400x1050 screens"),
-        'fullscreen' => '962x721',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1400x1050 screens"),
+        'fullscreen' => '1312x721',
         'thumbnails' => '328x245',
         'optional' => true,
     },
     {
         'name' => 'xx-large',
-        'description' => _("Fullscreen 1100x825, thumbnails 375x281, should fit 1600x1200 screens"),
-        'fullscreen' => '1100x825',
+        'description' => _("Sizes that should fit browsers in fullscreen for 1600x1200 screens"),
+        'fullscreen' => '1500x825',
         'thumbnails' => '375x281',
         'optional' => true,
     }
index 2213e90f9270eef85d45d74bbde4e55c51cded47..8ad867c52980f8dcd5d2ed61b91cc216f90b677f 100644 (file)
@@ -56,7 +56,7 @@ input {
 ~~iterate1_open~~
 <tr>
     ~~iterate2_open_maxN~~
-    <td align="center" valign="top">
+    <td align="center" valign="top" ~~colspan~~>
        ~~image_iteration~~
        <p>~~ifvideo?~~<img src="video.png"/>~~fi~~ ~~caption_iteration~~</p>
     </td>
index 6f87d900ab4bdfdd86a38a3d870e69e7b0c83c0b..b6608463db13d8f5684f1b7de0d0509361468d1f 100644 (file)
@@ -404,6 +404,22 @@ module Booh
         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")