#!/usr/local/bin/ruby %w(rubygems camping mongrel mongrel/camping image_science JSON digest).each { |lib| require lib } Camping.goes :Croppr module Croppr MIMES = {:png => "image/png", :jpg => "image/jpeg", :gif => "image/gif", :css => "text/css", :js => "text/javascript"} DIR = Dir.pwd def self.create unless Croppr::Models::Image.table_exists? ActiveRecord::Schema.define do create_table :croppr_images, :force => true do |t| t.column :extension, :string, :limit => 100 t.column :current_crop, :string, :limit => 255 t.column :created_at, :date t.column :key, :string end end Croppr::Models::Image.reset_column_information end end end module Croppr::Models class Image < Base validates_presence_of :extension, :key def file=(incoming_file) self.key = Digest::MD5.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join ) Image.transaction do raise "Bad File Type" unless self.extension = Croppr::MIMES.index(incoming_file[:type]).to_s ImageScience.with_image(incoming_file.tempfile.path) do |img| thumb! img img.save(File.join(Croppr::DIR, "images/#{self.key}.#{self.extension}")) end self.save! end end def pars self.current_crop ? JSON.parse(self.current_crop) : false end def full File.join(Croppr::DIR, "images/#{self.key}.#{self.extension}") end def thumb File.read(File.join(Croppr::DIR, "images/#{self.key}_thumb.png")) end def cropped pars ? File.read(File.join(Croppr::DIR, "images/#{self.key}_cropped.#{self.extension}")) : image end #Accepts an ImageScience image object def thumb!(img) img.cropped_thumbnail(80) do |thumb| thumb.save File.join(Croppr::DIR, "images/#{self.key}_thumb.png") end end def crop! size self.current_crop = size ImageScience.with_image(self.full) do |img| img.resize((pars['s'] * img.width).to_i, (pars['s'] * img.height).to_i) do |resized| resized.with_crop(pars['x'], pars['y'], pars['x'] + pars['w'], pars['y'] + pars['h']) do |cropped| cropped.save(File.join(Croppr::DIR, "images/#{self.key}_cropped.#{self.extension}")) thumb! cropped end end end self.save! end end end module Croppr::Controllers class Index < R '/' def get @images = Image.find(:all) render :index end end class Images < R '/images/(\d+)', '/images/(\d+)/(\w+)' def get(id, type = "full") image = Image.find(id) @headers['Content-Type'] = MIMES[image.extension.intern] case type when "full": File.read(image.full) when "cropped": image.cropped else @headers['Content-Type'] = "image/png" image.thumb end end end class New def get; render :new end def post image = Image.new image.file = input["image"] redirect Index end end class Delete < R '/delete/(\d+)' def get(id) image = Image.find(id) Image.transaction do Dir.chdir(File.join(DIR, "images")) do Dir.foreach('.') do |die| File.delete(die) if die =~ /^#{image.key}/ end end image.destroy end redirect Index end end class Ext < R '/ext/(.+)' def get(path) @headers['Content-Type'] = MIMES[path[/\.(\w+)$/, 1].intern] || "text/plain" @headers['X-Sendfile'] = File.join(DIR, "ext/#{path}") end end class Crop < R '/crop/(\d+)', '/crop/(\d+)/(.+)' def get(id,crop = nil) if crop Image.find(id).crop! crop redirect Index else @image = Image.find(id) render :crop end end end end module Croppr::Views def index #I have to do this outside of a layout because of needing to be XHTML Strict Markaby::Builder.set(:output_xml_instruction, false) xhtml_strict do head do script "", :src => R(Ext, "mootools_src.js"), :type => "text/javascript" script "", :src => R(Ext, "croppr.js"), :type => "text/javascript" link :href => R(Ext, "main.css"), :rel => "stylesheet", :type => "text/css" end body do img :src => R(Ext, "croppr.png"), :alt => "Croppr", :style => "float:right" div.page do @images.each do |i| div :class => "controls" do div :class => "image" do a :class => "hot", :href => R(Crop, i.id) do img :src=> R(Images, i.id, "thumb") end span :class => "info", :style => "display:none" do span i.pars ? "#{i.pars['w']}x#{i.pars['h']}" : "none" a "X", :onclick => "return confirm('You sure about this?')", :href => R(Delete, i.id) end end form :id => "form_#{i.id}" do input :type => "hidden", :id => "scale_#{i.id}", :value => i.pars ? i.pars['s'] : 0 input :type => "hidden", :id => "initial_#{i.id}", :value => i.pars ? "#{(i.pars['x']+i.pars['w']/2)/i.pars['s']},#{(i.pars['y']+i.pars['h'] /2)/i.pars['s']}" : "false" select :id => "crop_#{i.id}", :class => "crop_size" do ["Crop at:", "80x80", "120x120", "640x480", "other"].each { |o| option o} end end end end br :style => "clear:both" div.controls do a "New", :href => R(New) end script do %[ window.addEvent('load', function() { document.getElementsByClassName("crop_size").each(function(e,i) { e.addEvent('change', function() { sel = this[this.selectedIndex].text; id = this.id.split("_")[1]; if(sel == "Crop at:") return; else if(sel == "other") custom(id); else createCrop(id,sel); }) }); document.getElementsByClassName("image").each(function(e) { e.addEvent('mouseover', function() { mom = this; while(this.getTag() != "div") mom = this.getParent(); mom.getChildren()[1].style.display = "block"; }); e.addEvent('mouseout', function() { mom = this; while(this.getTag() != "div") mom = this.getParent(); mom.getChildren()[1].style.display = "none"; }); }); }); function custom(id) { cust = document.createElement("input"); cust.type = 'text';cust.id = 'cust_crop_'+id;cust.style.width = '80px'; $('form_'+id).replaceChild(cust,$('crop_'+id)); cust.form.onsubmit = function() { createCrop(this,$('cust_crop_'+this).value); return false; }.bind(id) cust.focus(); } function createCrop(id, size, s) { if(size.match(/^(\\d+)x(\\d+)$/)) cool = new Croppr('/croppr/images/'+id, {crop: size.split("x"), url: function(v){window.location = '/croppr/crop/'+id+'/'+v}, scale: $('scale_'+id).value, initial: $('initial_'+id).value.split(','), background:'#000'}); else alert("Size must be in the following format: 50x50"); }] end end end end end def new form :action => R(New), :method => 'post', :enctype => 'multipart/form-data' do label 'Image', :for => 'image'; br input :name => 'image', :type => 'file' input :type => 'submit', :value => 'Crop' end end def crop img :src => R(Images, @image.id, "cropped"); br a "Back", :href => R(Index) end end if __FILE__ == $0 Croppr::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'croppr.db' Croppr::Models::Base.logger = Logger.new('croppr.log') Croppr::Models::Base.threaded_connections=false Croppr.create server = Mongrel::Camping::start("0.0.0.0",3000,"/croppr",Croppr) puts "** Croppr example is running at http://localhost:3000/croppr" server.run.join end