class Photo include Magick def initialize(session) @orig_f_name = session[:f_name] if session[:photo_params].blank? init_vars @vars['prefix'] = session.session_id[0..9] else @vars = session[:photo_params] end end # Get rid of cache and work files when Photo is destroyed def destroy begin Photo.clear_work_files(@vars['prefix'], @orig_f_name) rescue end end # Given a session hash, delete the work and cache files the session # uses def Photo.cleanup_files_for(session_data) if session_data[:f_name] && session_data[:photo_params] prefix = session_data[:photo_params]['prefix'] orig_f_name = session_data[:f_name] begin Photo.clear_work_files(prefix, orig_f_name) rescue end end end def Photo.clear_work_files(prefix, image_f_name) cache_name = "#{RAILS_ROOT}/#{PSitsnotEngine.config :cache_dir_name}" + "/#{prefix}_#{image_f_name.gsub(/\.\w+$/i, '')}" cache_type = ".#{PSitsnotEngine.config :cache_file_type}" File.delete(cache_name + cache_type) if File.exist?(cache_name + cache_type) if cache_type =~ /MPC/i File.delete(cache_name + ".cache") if File.exist?(cache_name + ".cache") end work_name = "#{RAILS_ROOT}/public/images/#{prefix}_work.jpg" File.delete(work_name) if File.exist?(work_name) File.delete("#{work_name}.bak") if File.exist?("#{work_name}.bak") File.delete("#{work_name}.uncrop") if File.exist?("#{work_name}.uncrop") end # Load an image into an editable Photo object # The source may be a pathname (String), an open File, a Tempfile, # or a StringIO object def load(img_source) begin cache_image_for_processing(img_source) rescue Exception => e raise e.to_s + "\nCould not creat an image file from #{@orig_f_name}" end begin get_photo_working_copy rescue Exception => e FileUtils.rm_f("#{photo_workfile_full_path}") raise e.to_s + "\nCould not get working copy #{photo_workfile_full_path}" end end # Save photo object by putting its variables into session def save(session) session[:photo_params] = @vars unless session[:f_name].blank? end def rotate(amount) if (amount > 0) @vars['geom'].rotate_90_clockwise else @vars['geom'].rotate_90_counterclockwise end refresh_working_copy end def brighter(make_brighter) amount = make_brighter ? 1.30 : 0.80 if PSitsnotEngine.config :invert_level_params perform_operation {|img| img.level(0, amount)} @vars['ops'] << ['level', [0.to_s, amount.to_s]] else perform_operation {|img| img.level(0, MaxRGB, amount)} @vars['ops'] << ['level', [0.to_s, 'MaxRGB', amount.to_s]] end end def adjust_contrast(more_contrast) perform_operation {|img| img.contrast(more_contrast)} @vars['ops'] << ['contrast', [more_contrast.to_s]] end def adjust_red(more_red) amount = more_red ? 1.1 : 0.9 perform_operation {|img| img.level_channel(Magick::RedChannel, 0.0, MaxRGB, amount)} @vars['ops'] << ['level_channel', ['Magick::RedChannel, 0.0, MaxRGB', amount.to_s]] end def adjust_green(more_green) amount = more_green ? 1.1 : 0.9 perform_operation {|img| img.level_channel(Magick::GreenChannel, 0.0, MaxRGB, amount)} @vars['ops'] << ['level_channel', ['Magick::GreenChannel, 0.0, MaxRGB', amount.to_s]] end def adjust_blue(more_blue) amount = more_blue ? 1.1 : 0.9 perform_operation {|img| img.level_channel(Magick::BlueChannel, 0.0, MaxRGB, amount)} @vars['ops'] << ['level_channel', ['Magick::BlueChannel, 0.0, MaxRGB', amount.to_s]] end def enhance_color_contrast perform_operation {|img| img.normalize} @vars['ops'] << ['normalize', [nil.to_s]] end def sharpen perform_operation {|img| img.sharpen} @vars['ops'] << ['sharpen', [0.to_s]] end def reduce_noise perform_operation {|img| img.reduce_noise(0)} @vars['ops'] << ['reduce_noise', [0.to_s]] end def undo if File.exist?("#{photo_workfile_full_path}.bak") FileUtils.cp("#{photo_workfile_full_path}.bak", "#{photo_workfile_full_path}") FileUtils.rm("#{photo_workfile_full_path}.bak") @vars['ops'].delete(@vars['ops'].last) end end # Overlay transparent gray on the areas to be cropped out def preview_crop(left, top, right, bottom) left, top, right, bottom = limit_crop(left, top, right, bottom) FileUtils.cp("#{photo_workfile_full_path}.uncrop", "#{photo_workfile_full_path}") unless @vars['preview'].blank? img = Image.read("#{photo_workfile_full_path}").first img.write("#{photo_workfile_full_path}.uncrop") @vars['preview'] = true img_width = img.columns img_height = img.rows x = left.to_f / 100 * img_width y = top.to_f / 100 * img_height width = img_width - (right.to_f / 100 * img_width) - x height = img_height - (bottom.to_f / 100 * img_height) - y rect = Magick::Draw.new rect.stroke('transparent') rect.fill('black') rect.fill_opacity(0.40) rect.rectangle(0,0, x, img_height) rect.rectangle(x, 0, x + width, y) rect.rectangle(x + width, 0, img_width, img_height) rect.rectangle(x, y + height, x + width, img_height) rect.draw(img) img.write("#{photo_workfile_full_path}") img = nil rect = nil GC.start end def crop(left, top, right, bottom) left, top, right, bottom = limit_crop(left, top, right, bottom) @vars['geom'].crop(left, top, right, bottom) @vars['preview'] = nil img = original_image apply_crop_before_rotation(img) apply_rotation(img) img = apply_all_ops(img) img.write("#{photo_workfile_full_path}") { self.quality = 50 } img = nil GC.start end def set_max_size(maxH, maxV) limit = PSitsnotEngine.config :max_output_size maxH = maxH.to_i maxV = maxV.to_i @vars['max_h'] = maxH < limit ? maxH : limit @vars['max_v'] = maxV < limit ? maxV : limit refresh_working_copy end # Start over from original cached image def revert limit = PSitsnotEngine.config :max_output_size @vars['preview'] = nil @vars['ops'] = [] @vars['max_h'] = limit @vars['max_v'] = limit get_photo_working_copy end # Start from original cached image, apply all operations up to now, make # new work copy def refresh_working_copy @vars['preview'] = nil img = original_image apply_crop_before_rotation(img) img = apply_rotation(img) img = apply_all_ops(img) img.write("#{photo_workfile_full_path}") { self.quality = 50 } img = nil GC.start end def photo_workfile_name "#{@vars['prefix']}_work.jpg" end def photo_workfile_partial_path "images/#{photo_workfile_name}" end def photo_workfile_full_path "#{RAILS_ROOT}/public/#{photo_workfile_partial_path}" end private def perform_operation(&operation) FileUtils.cp("#{photo_workfile_full_path}.uncrop", "#{photo_workfile_full_path}") unless @vars['preview'].blank? @vars['preview'] = nil FileUtils.cp("#{photo_workfile_full_path}", "#{photo_workfile_full_path}.bak") img = Image.read("#{photo_workfile_full_path}").first img = operation.call(img) img.write("#{photo_workfile_full_path}") { self.quality = 50 } img = nil GC.start end def apply_rotation(img) rotation = @vars['geom'].rotation.to_f img.rotate!(rotation) if rotation > 0.0 img end def apply_all_ops(img) @vars['ops'].each do |op| eval("img = img.#{op[0]}(#{op[1].join(',')}); GC.start") GC.start end img end def apply_crop(img) x, y, width, height = @vars['geom'].x_y_width_height if (width != @vars['geom'].total_width) || (height != @vars['geom'].total_height) img.crop!(x, y, width, height) end scale_fact = @vars['geom'].scale_factor_for(@vars['max_h'], @vars['max_v']) img.strip! img.resize!(scale_fact * width, scale_fact * height) end def apply_crop_before_rotation(img) x, y, width, height = @vars['geom'].x_y_width_height_before_rotation if (width != @vars['geom'].total_width) || (height != @vars['geom'].total_height) img.crop!(x, y, width, height) end scale_fact = @vars['geom'].scale_factor_for(@vars['max_h'], @vars['max_v']) img.strip! img.resize!(scale_fact * width, scale_fact * height) end def get_photo_working_copy img = original_image @vars['geom'] = PhotoGeometry.new(img.columns, img.rows) img.strip!.change_geometry("#{@vars['max_h']}x#{@vars['max_v']}") { |cols, rows, img| img.resize!(cols, rows) } img.write("#{photo_workfile_full_path}") { self.quality = 50 } img = nil GC.start end def init_vars limit = PSitsnotEngine.config :max_output_size @vars = {} @vars['prefix'] = '' @vars['preview'] = nil @vars['max_h'] = limit @vars['max_v'] = limit @vars['ops'] = [] @vars['geom'] = PhotoGeometry.new(0, 0) end def cache_image_for_processing(source) img = nil if source.class == StringIO source.rewind img = Image.from_blob(source.read).first elsif source.class == String img = Image.read(source).first elsif source.class == File source.rewind img = Image.read(source).first elsif source.class == Tempfile img = Image.read(source.path).first else raise "Unknown type of image source" end cache_geom = "#{PSitsnotEngine.config :max_cache_size}x#{PSitsnotEngine.config :max_cache_size}" img.strip!.change_geometry(cache_geom) { |cols, rows, img| img.resize!(cols, rows) } img.write(cache_file_name) img = nil GC.start end def original_image img = Image.read(cache_file_name).first end def cache_file_name "#{photo_dl_dir}/#{@vars['prefix']}_#{photo_basename}.#{cache_file_suffix}" end def cache_file_suffix PSitsnotEngine.config :cache_file_type end def photo_dl_dir path = "#{RAILS_ROOT}/#{PSitsnotEngine.config :cache_dir_name}" FileUtils.mkdir(path) unless File.exist?(path) path end # name of original photo without extension def photo_basename @orig_f_name.gsub(/\.\w+$/i, '') end # Don't allow cropping away 100% in either dimension! def limit_crop(left, right, top, bottom) left = left.to_f right = right.to_f top = top.to_f bottom = bottom.to_f if (right + left) >= 100.0 excess = (right + left - 98.0) / 2.0 excess = excess.ceil right -= excess left -= excess end if (top + bottom) >= 100 excess = (top + bottom - 98) / 2 excess = excess.ceil top -= excess bottom -= excess end return left, right, top, bottom end end