robinboening-fleximage 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +14 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +257 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/autotest.rb +5 -0
- data/fleximage.gemspec +179 -0
- data/init.rb +1 -0
- data/lib/dsl_accessor.rb +52 -0
- data/lib/fleximage.rb +56 -0
- data/lib/fleximage/aviary_controller.rb +75 -0
- data/lib/fleximage/blank.rb +70 -0
- data/lib/fleximage/helper.rb +41 -0
- data/lib/fleximage/image_proxy.rb +69 -0
- data/lib/fleximage/legacy_view.rb +63 -0
- data/lib/fleximage/model.rb +711 -0
- data/lib/fleximage/operator/background.rb +62 -0
- data/lib/fleximage/operator/base.rb +189 -0
- data/lib/fleximage/operator/border.rb +50 -0
- data/lib/fleximage/operator/crop.rb +58 -0
- data/lib/fleximage/operator/image_overlay.rb +85 -0
- data/lib/fleximage/operator/resize.rb +92 -0
- data/lib/fleximage/operator/shadow.rb +87 -0
- data/lib/fleximage/operator/text.rb +104 -0
- data/lib/fleximage/operator/trim.rb +14 -0
- data/lib/fleximage/operator/unsharp_mask.rb +36 -0
- data/lib/fleximage/rmagick_image_patch.rb +5 -0
- data/lib/fleximage/view.rb +57 -0
- data/tasks/fleximage_tasks.rake +154 -0
- data/test/fixtures/100x1.jpg +0 -0
- data/test/fixtures/100x100.jpg +0 -0
- data/test/fixtures/1x1.jpg +0 -0
- data/test/fixtures/1x100.jpg +0 -0
- data/test/fixtures/cmyk.jpg +0 -0
- data/test/fixtures/not_a_photo.xml +1 -0
- data/test/fixtures/photo.jpg +0 -0
- data/test/mock_file.rb +21 -0
- data/test/rails_root/app/controllers/application.rb +10 -0
- data/test/rails_root/app/controllers/avatars_controller.rb +85 -0
- data/test/rails_root/app/controllers/photo_bares_controller.rb +85 -0
- data/test/rails_root/app/controllers/photo_dbs_controller.rb +85 -0
- data/test/rails_root/app/controllers/photo_files_controller.rb +85 -0
- data/test/rails_root/app/helpers/application_helper.rb +3 -0
- data/test/rails_root/app/helpers/avatars_helper.rb +2 -0
- data/test/rails_root/app/helpers/photo_bares_helper.rb +2 -0
- data/test/rails_root/app/helpers/photo_dbs_helper.rb +2 -0
- data/test/rails_root/app/helpers/photo_files_helper.rb +2 -0
- data/test/rails_root/app/locales/de.yml +7 -0
- data/test/rails_root/app/locales/en.yml +8 -0
- data/test/rails_root/app/models/abstract.rb +8 -0
- data/test/rails_root/app/models/avatar.rb +4 -0
- data/test/rails_root/app/models/photo_bare.rb +7 -0
- data/test/rails_root/app/models/photo_custom_error.rb +10 -0
- data/test/rails_root/app/models/photo_db.rb +3 -0
- data/test/rails_root/app/models/photo_file.rb +3 -0
- data/test/rails_root/app/models/photo_s3.rb +5 -0
- data/test/rails_root/app/views/avatars/edit.html.erb +17 -0
- data/test/rails_root/app/views/avatars/index.html.erb +20 -0
- data/test/rails_root/app/views/avatars/new.html.erb +16 -0
- data/test/rails_root/app/views/avatars/show.html.erb +8 -0
- data/test/rails_root/app/views/layouts/avatars.html.erb +17 -0
- data/test/rails_root/app/views/layouts/photo_bares.html.erb +17 -0
- data/test/rails_root/app/views/layouts/photo_dbs.html.erb +17 -0
- data/test/rails_root/app/views/layouts/photo_files.html.erb +17 -0
- data/test/rails_root/app/views/photo_bares/edit.html.erb +12 -0
- data/test/rails_root/app/views/photo_bares/index.html.erb +18 -0
- data/test/rails_root/app/views/photo_bares/new.html.erb +11 -0
- data/test/rails_root/app/views/photo_bares/show.html.erb +3 -0
- data/test/rails_root/app/views/photo_dbs/edit.html.erb +32 -0
- data/test/rails_root/app/views/photo_dbs/index.html.erb +26 -0
- data/test/rails_root/app/views/photo_dbs/new.html.erb +31 -0
- data/test/rails_root/app/views/photo_dbs/show.html.erb +23 -0
- data/test/rails_root/app/views/photo_files/edit.html.erb +27 -0
- data/test/rails_root/app/views/photo_files/index.html.erb +24 -0
- data/test/rails_root/app/views/photo_files/new.html.erb +26 -0
- data/test/rails_root/app/views/photo_files/show.html.erb +18 -0
- data/test/rails_root/config/boot.rb +109 -0
- data/test/rails_root/config/database.yml +7 -0
- data/test/rails_root/config/environment.rb +66 -0
- data/test/rails_root/config/environments/development.rb +18 -0
- data/test/rails_root/config/environments/production.rb +19 -0
- data/test/rails_root/config/environments/sqlite3.rb +0 -0
- data/test/rails_root/config/environments/test.rb +22 -0
- data/test/rails_root/config/initializers/inflections.rb +10 -0
- data/test/rails_root/config/initializers/load_translations.rb +4 -0
- data/test/rails_root/config/initializers/mime_types.rb +5 -0
- data/test/rails_root/config/routes.rb +43 -0
- data/test/rails_root/db/migrate/001_create_photo_files.rb +16 -0
- data/test/rails_root/db/migrate/002_create_photo_dbs.rb +16 -0
- data/test/rails_root/db/migrate/003_create_photo_bares.rb +12 -0
- data/test/rails_root/db/migrate/004_create_avatars.rb +13 -0
- data/test/rails_root/db/migrate/005_create_photo_s3s.rb +12 -0
- data/test/rails_root/public/.htaccess +40 -0
- data/test/rails_root/public/404.html +30 -0
- data/test/rails_root/public/422.html +30 -0
- data/test/rails_root/public/500.html +30 -0
- data/test/rails_root/public/dispatch.cgi +10 -0
- data/test/rails_root/public/dispatch.fcgi +24 -0
- data/test/rails_root/public/dispatch.rb +10 -0
- data/test/rails_root/public/favicon.ico +0 -0
- data/test/rails_root/public/images/rails.png +0 -0
- data/test/rails_root/public/index.html +277 -0
- data/test/rails_root/public/javascripts/application.js +2 -0
- data/test/rails_root/public/javascripts/controls.js +963 -0
- data/test/rails_root/public/javascripts/dragdrop.js +972 -0
- data/test/rails_root/public/javascripts/effects.js +1120 -0
- data/test/rails_root/public/javascripts/prototype.js +4225 -0
- data/test/rails_root/public/robots.txt +5 -0
- data/test/rails_root/public/stylesheets/scaffold.css +74 -0
- data/test/rails_root/vendor/plugins/fleximage/init.rb +2 -0
- data/test/s3_stubs.rb +7 -0
- data/test/test_helper.rb +82 -0
- data/test/unit/abstract_test.rb +20 -0
- data/test/unit/basic_model_test.rb +40 -0
- data/test/unit/blank_test.rb +23 -0
- data/test/unit/default_image_path_option_test.rb +16 -0
- data/test/unit/dsl_accessor_test.rb +120 -0
- data/test/unit/file_upload_from_local_test.rb +31 -0
- data/test/unit/file_upload_from_strings_test.rb +23 -0
- data/test/unit/file_upload_from_url_test.rb +35 -0
- data/test/unit/file_upload_to_db_test.rb +41 -0
- data/test/unit/has_store_test.rb +4 -0
- data/test/unit/i18n_messages_test.rb +49 -0
- data/test/unit/image_directory_option_test.rb +20 -0
- data/test/unit/image_proxy_test.rb +17 -0
- data/test/unit/image_storage_format_option_test.rb +31 -0
- data/test/unit/magic_columns_test.rb +34 -0
- data/test/unit/minimum_image_size_test.rb +56 -0
- data/test/unit/operator_base_test.rb +124 -0
- data/test/unit/operator_resize_test.rb +18 -0
- data/test/unit/preprocess_image_option_test.rb +21 -0
- data/test/unit/require_image_option_test.rb +30 -0
- data/test/unit/temp_image_test.rb +23 -0
- data/test/unit/use_creation_date_based_directories_option_test.rb +16 -0
- metadata +258 -0
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w(lib fleximage)))
|
data/lib/dsl_accessor.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support' unless defined?(ActiveSupport)
|
2
|
+
|
3
|
+
class Class
|
4
|
+
def dsl_accessor(name, options = {})
|
5
|
+
raise TypeError, "DSL Error: options should be a hash. but got `#{options.class}'" unless options.is_a?(Hash)
|
6
|
+
writer = options[:writer] || options[:setter]
|
7
|
+
writer =
|
8
|
+
case writer
|
9
|
+
when NilClass then Proc.new{|value| value}
|
10
|
+
when Symbol then Proc.new{|value| __send__(writer, value)}
|
11
|
+
when Proc then writer
|
12
|
+
else raise TypeError, "DSL Error: writer should be a symbol or proc. but got `#{options[:writer].class}'"
|
13
|
+
end
|
14
|
+
write_inheritable_attribute(:"#{name}_writer", writer)
|
15
|
+
|
16
|
+
default =
|
17
|
+
case options[:default]
|
18
|
+
when NilClass then nil
|
19
|
+
when [] then Proc.new{[]}
|
20
|
+
when {} then Proc.new{{}}
|
21
|
+
when Symbol then Proc.new{__send__(options[:default])}
|
22
|
+
when Proc then options[:default]
|
23
|
+
else Proc.new{options[:default]}
|
24
|
+
end
|
25
|
+
write_inheritable_attribute(:"#{name}_default", default)
|
26
|
+
|
27
|
+
self.class.class_eval do
|
28
|
+
define_method("#{name}=") do |value|
|
29
|
+
writer = read_inheritable_attribute(:"#{name}_writer")
|
30
|
+
value = writer.call(value) if writer
|
31
|
+
write_inheritable_attribute(:"#{name}", value)
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method(name) do |*values|
|
35
|
+
if values.empty?
|
36
|
+
# getter method
|
37
|
+
key = :"#{name}"
|
38
|
+
if !inheritable_attributes.has_key?(key)
|
39
|
+
default = read_inheritable_attribute(:"#{name}_default")
|
40
|
+
value = default ? default.call(self) : nil
|
41
|
+
__send__("#{name}=", value)
|
42
|
+
end
|
43
|
+
read_inheritable_attribute(key)
|
44
|
+
else
|
45
|
+
# setter method
|
46
|
+
__send__("#{name}=", *values)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
data/lib/fleximage.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'base64'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'aws/s3'
|
5
|
+
|
6
|
+
# Load RMagick
|
7
|
+
begin
|
8
|
+
require 'RMagick'
|
9
|
+
rescue MissingSourceFile => e
|
10
|
+
puts %{ERROR :: FlexImage requires the RMagick gem. http://rmagick.rubyforge.org/install-faq.html}
|
11
|
+
raise e
|
12
|
+
end
|
13
|
+
|
14
|
+
# Apply a few RMagick patches
|
15
|
+
require 'fleximage/rmagick_image_patch'
|
16
|
+
|
17
|
+
# Load dsl_accessor from lib
|
18
|
+
require 'dsl_accessor'
|
19
|
+
|
20
|
+
# Load Operators
|
21
|
+
require 'fleximage/operator/base'
|
22
|
+
Dir.entries("#{File.dirname(__FILE__)}/fleximage/operator").each do |filename|
|
23
|
+
require "fleximage/operator/#{filename.gsub('.rb', '')}" if filename =~ /\.rb$/
|
24
|
+
end
|
25
|
+
|
26
|
+
# Setup Model
|
27
|
+
require 'fleximage/model'
|
28
|
+
ActiveRecord::Base.class_eval { include Fleximage::Model }
|
29
|
+
|
30
|
+
# Image Proxy
|
31
|
+
require 'fleximage/image_proxy'
|
32
|
+
|
33
|
+
# Setup View
|
34
|
+
ActionController::Base.exempt_from_layout :flexi
|
35
|
+
if defined?(ActionView::Template)
|
36
|
+
# Rails >= 2.1
|
37
|
+
require 'fleximage/view'
|
38
|
+
ActionView::Template.register_template_handler :flexi, Fleximage::View
|
39
|
+
else
|
40
|
+
# Rails < 2.1
|
41
|
+
require 'fleximage/legacy_view'
|
42
|
+
ActionView::Base.register_template_handler :flexi, Fleximage::LegacyView
|
43
|
+
end
|
44
|
+
|
45
|
+
# Setup Helper
|
46
|
+
require 'fleximage/helper'
|
47
|
+
ActionView::Base.class_eval { include Fleximage::Helper }
|
48
|
+
|
49
|
+
# Setup Aviary Controller
|
50
|
+
require 'fleximage/aviary_controller'
|
51
|
+
ActionController::Base.class_eval{ include Fleximage::AviaryController }
|
52
|
+
|
53
|
+
# Register mime types
|
54
|
+
Mime::Type.register "image/jpeg", :jpg, ["image/pjpeg"], ["jpeg"]
|
55
|
+
Mime::Type.register "image/gif", :gif
|
56
|
+
Mime::Type.register "image/png", :png
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Fleximage
|
2
|
+
|
3
|
+
module AviaryController
|
4
|
+
def self.api_key(value = nil)
|
5
|
+
value ? @api_key = value : @api_key
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.api_key=(value = nil)
|
9
|
+
api_key value
|
10
|
+
end
|
11
|
+
|
12
|
+
# Include acts_as_fleximage class method
|
13
|
+
def self.included(base) #:nodoc:
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
# Invoke this method to enable this controller to allow editing of images via Aviary
|
20
|
+
def editable_in_aviary(model_class, options = {})
|
21
|
+
unless options.has_key?(:secret)
|
22
|
+
raise ArgumentError, ":secret key in options is required.\nExample: editable_in_aviary(Photo, :secret => \"My-deep-dark-secret\")"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Don't verify authenticity for aviary callback
|
26
|
+
protect_from_forgery :except => :aviary_image_update
|
27
|
+
|
28
|
+
# Include the necesary instance methods
|
29
|
+
include Fleximage::AviaryController::InstanceMethods
|
30
|
+
|
31
|
+
# Add before_filter to secure aviary actions
|
32
|
+
before_filter :aviary_image_security, :only => [:aviary_image, :aviary_image_update]
|
33
|
+
|
34
|
+
# Allow the view access to the image hash generation method
|
35
|
+
helper_method :aviary_image_hash
|
36
|
+
|
37
|
+
# Save the Fleximage model class
|
38
|
+
model_class = model_class.constantize if model_class.is_a?(String)
|
39
|
+
dsl_accessor :aviary_model_class, :default => model_class
|
40
|
+
dsl_accessor :aviary_secret, :default => options[:secret]
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
module InstanceMethods
|
46
|
+
|
47
|
+
# Deliver the master image to aviary
|
48
|
+
def aviary_image
|
49
|
+
render :text => @model.load_image.to_blob,
|
50
|
+
:content_type => Mime::Type.lookup_by_extension(self.class.aviary_model_class.image_storage_format.to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Aviary posts the edited image back to the controller here
|
54
|
+
def aviary_image_update
|
55
|
+
@model.image_file_url = params[:imageurl]
|
56
|
+
@model.save
|
57
|
+
render :text => 'Image Updated From Aviary'
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
def aviary_image_hash(model)
|
62
|
+
Digest::SHA1.hexdigest("fleximage-aviary-#{model.id}-#{model.created_at}-#{self.class.aviary_secret}")
|
63
|
+
end
|
64
|
+
|
65
|
+
def aviary_image_security
|
66
|
+
@model = self.class.aviary_model_class.find(params[:id])
|
67
|
+
unless aviary_image_hash(@model) == params[:key]
|
68
|
+
render :text => '<h1>403 Not Authorized</h1>', :status => '403'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Fleximage
|
2
|
+
|
3
|
+
# The +Blank+ class allows easy creation of dynamic images for views which depends models that
|
4
|
+
# do not store images. For example, perhaps you want a rendering of a text label, or a comment,
|
5
|
+
# or some other type of data that is not inherently image based.
|
6
|
+
#
|
7
|
+
# Your model doesn't need to know anything about Fleximage. You can instantiate and operate on
|
8
|
+
# a new Fleximage::Blank object right in your view.
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
#
|
12
|
+
# Fleximage::Blank.new(size, options = {}).operate { |image| ... }
|
13
|
+
#
|
14
|
+
# Use the following keys in the +options+ hash:
|
15
|
+
#
|
16
|
+
# * color: the color the image will be. Can be a named color or a Magick::Pixel object.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
#
|
20
|
+
# # app/views/comments/show.png.flexi
|
21
|
+
# Fleximage::Blank.new('400x150')).operate do |image|
|
22
|
+
# # Start with a chat bubble image as the background
|
23
|
+
# image.image_overlay('public/images/comment_bubble.png')
|
24
|
+
#
|
25
|
+
# # Assuming that the user model acts_as_fleximage, this will draw the users image.
|
26
|
+
# image.image_overlay(@comment.user.file_path,
|
27
|
+
# :size => '50x50',
|
28
|
+
# :alignment => :top_left,
|
29
|
+
# :offset => '10x10'
|
30
|
+
# )
|
31
|
+
#
|
32
|
+
# # Add the author name text
|
33
|
+
# image.text(@comment.author,
|
34
|
+
# :alignment => :top_left,
|
35
|
+
# :offset => '10x10',
|
36
|
+
# :color => 'black',
|
37
|
+
# :font_size => 24,
|
38
|
+
# :shadow => {
|
39
|
+
# :blur => 1,
|
40
|
+
# :opacity => 0.5,
|
41
|
+
# }
|
42
|
+
# )
|
43
|
+
#
|
44
|
+
# # Add the comment body text
|
45
|
+
# image.text(@comment.body,
|
46
|
+
# :alignment => :top_left,
|
47
|
+
# :offset => '10x90',
|
48
|
+
# :color => color(128, 128, 128),
|
49
|
+
# :font_size => 14
|
50
|
+
# )
|
51
|
+
# end
|
52
|
+
class Blank
|
53
|
+
include Fleximage::Model
|
54
|
+
acts_as_fleximage
|
55
|
+
|
56
|
+
def initialize(size, options = {})
|
57
|
+
width, height = Fleximage::Operator::Base.size_to_xy(size)
|
58
|
+
|
59
|
+
@uploaded_image = Magick::Image.new(width, height) do
|
60
|
+
self.colorspace = Magick::RGBColorspace
|
61
|
+
self.depth = 8
|
62
|
+
self.density = '72'
|
63
|
+
self.format = 'PNG'
|
64
|
+
self.background_color = options[:color] || 'none'
|
65
|
+
end
|
66
|
+
|
67
|
+
@output_image = @uploaded_image
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Fleximage
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
# Creates an image tag that links directly to image data. Recommended for displays of a
|
5
|
+
# temporary upload that is not saved to a record in the databse yet.
|
6
|
+
def embedded_image_tag(model, options = {})
|
7
|
+
model.load_image
|
8
|
+
format = options[:format] || :jpg
|
9
|
+
mime = Mime::Type.lookup_by_extension(format.to_s).to_s
|
10
|
+
image = model.output_image(:format => format)
|
11
|
+
data = Base64.encode64(image)
|
12
|
+
|
13
|
+
options = { :alt => model.class.to_s }.merge(options)
|
14
|
+
|
15
|
+
result = image_tag("data:#{mime};base64,#{data}", options)
|
16
|
+
result.gsub(%r{src=".*/images/data:}, 'src="data:')
|
17
|
+
|
18
|
+
rescue Fleximage::Model::MasterImageNotFound => e
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a link that opens an image for editing in Aviary.
|
23
|
+
#
|
24
|
+
# Options:
|
25
|
+
#
|
26
|
+
# * image_url: url to the master image used by Aviary for editing. Defauls to <tt>url_for(:action => 'aviary_image', :id => model, :only_path => false)</tt>
|
27
|
+
# * post_url: url where Aviary will post the updated image. Defauls to <tt>url_for(:action => 'aviary_image_update', :id => model, :only_path => false)</tt>
|
28
|
+
#
|
29
|
+
# All other options are passed directly to the @link_to@ helper.
|
30
|
+
def link_to_edit_in_aviary(text, model, options = {})
|
31
|
+
key = aviary_image_hash(model)
|
32
|
+
image_url = options.delete(:image_url) || url_for(:action => 'aviary_image', :id => model, :only_path => false, :key => key)
|
33
|
+
post_url = options.delete(:image_update_url) || url_for(:action => 'aviary_image_update', :id => model, :only_path => false, :key => key)
|
34
|
+
api_key = Fleximage::AviaryController.api_key
|
35
|
+
url = "http://aviary.com/flash/aviary/index.aspx?tid=1&phoenix&apil=#{api_key}&loadurl=#{CGI.escape image_url}&posturl=#{CGI.escape post_url}"
|
36
|
+
|
37
|
+
link_to text, url, { :target => 'aviary' }.merge(options)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Fleximage
|
2
|
+
|
3
|
+
# An instance of this class is yielded when Model#operate is called. It enables image operators
|
4
|
+
# to be called to transform the image. You should never need to directly deal with this class.
|
5
|
+
# You simply call image operators on this object when inside an Model#operate block
|
6
|
+
#
|
7
|
+
# @photo.operate do |image|
|
8
|
+
# image.resize '640x480'
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# In this example, +image+ is an instance of ImageProxy
|
12
|
+
class ImageProxy
|
13
|
+
|
14
|
+
class OperatorNotFound < NameError #:nodoc:
|
15
|
+
end
|
16
|
+
|
17
|
+
# The image to be manipulated by operators.
|
18
|
+
attr_accessor :image
|
19
|
+
|
20
|
+
# Create a new image operator proxy.
|
21
|
+
def initialize(image, model_obj)
|
22
|
+
@image = image
|
23
|
+
@model = model_obj
|
24
|
+
end
|
25
|
+
|
26
|
+
# Shortcut for accessing current image width
|
27
|
+
def width
|
28
|
+
@image.columns
|
29
|
+
end
|
30
|
+
|
31
|
+
# Shortcut for accessing current image height
|
32
|
+
def height
|
33
|
+
@image.rows
|
34
|
+
end
|
35
|
+
|
36
|
+
# A call to an unknown method will look for an Operator by that method's name.
|
37
|
+
# If it finds one, it will execute that operator.
|
38
|
+
def method_missing(method_name, *args)
|
39
|
+
# Find the operator class
|
40
|
+
class_name = method_name.to_s.camelcase
|
41
|
+
operator_class = "Fleximage::Operator::#{class_name}".constantize
|
42
|
+
|
43
|
+
# Define a method for this operator so future calls to this operation are faster
|
44
|
+
self.class.module_eval <<-EOF
|
45
|
+
def #{method_name}(*args)
|
46
|
+
@image = execute_operator(#{operator_class}, *args)
|
47
|
+
end
|
48
|
+
EOF
|
49
|
+
|
50
|
+
# Call the method that was just defined to perform its functionality.
|
51
|
+
send(method_name, *args)
|
52
|
+
|
53
|
+
rescue NameError => e
|
54
|
+
if e.to_s =~ /uninitialized constant Fleximage::Operator::#{class_name}/
|
55
|
+
raise OperatorNotFound, "No operator Fleximage::Operator::#{class_name} found for the method \"#{method_name}\""
|
56
|
+
else
|
57
|
+
raise e
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
# Instantiate and execute the requested image Operator.
|
63
|
+
def execute_operator(operator_class, *args)
|
64
|
+
operator_class.new(self, @image, @model).execute(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Fleximage
|
2
|
+
|
3
|
+
# Renders a .flexi template
|
4
|
+
class LegacyView #:nodoc:
|
5
|
+
class TemplateDidNotReturnImage < RuntimeError #:nodoc:
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(view)
|
9
|
+
@view = view
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(template, local_assigns = {})
|
13
|
+
# process the view
|
14
|
+
result = @view.instance_eval do
|
15
|
+
|
16
|
+
# Shorthand color creation
|
17
|
+
def color(*args)
|
18
|
+
if args.size == 1 && args.first.is_a?(String)
|
19
|
+
args.first
|
20
|
+
else
|
21
|
+
Magick::Pixel.new(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# inject assigns into instance variables
|
26
|
+
assigns.each do |key, value|
|
27
|
+
instance_variable_set "@#{key}", value
|
28
|
+
end
|
29
|
+
|
30
|
+
# inject local assigns into reader methods
|
31
|
+
local_assigns.each do |key, value|
|
32
|
+
class << self; self; end.send(:define_method, key) { value }
|
33
|
+
end
|
34
|
+
|
35
|
+
#execute the template
|
36
|
+
eval(template)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Raise an error if object returned from template is not an image record
|
40
|
+
unless result.class.include?(Fleximage::Model::InstanceMethods)
|
41
|
+
raise TemplateDidNotReturnImage, ".flexi template was expected to return a model instance that acts_as_fleximage, but got an instance of <#{result.class}> instead."
|
42
|
+
end
|
43
|
+
|
44
|
+
# Figure out the proper format
|
45
|
+
requested_format = (@view.params[:format] || :jpg).to_sym
|
46
|
+
raise 'Image must be requested with an image type format. jpg, gif and png only are supported.' unless [:jpg, :gif, :png].include?(requested_format)
|
47
|
+
|
48
|
+
# Set proper content type
|
49
|
+
@view.controller.headers["Content-Type"] = Mime::Type.lookup_by_extension(requested_format.to_s).to_s
|
50
|
+
|
51
|
+
# get rendered result
|
52
|
+
rendered_image = result.output_image(:format => requested_format)
|
53
|
+
|
54
|
+
# Return image data
|
55
|
+
return rendered_image
|
56
|
+
ensure
|
57
|
+
|
58
|
+
# ensure garbage collection happens after every flex image render
|
59
|
+
rendered_image.dispose!
|
60
|
+
GC.start
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,711 @@
|
|
1
|
+
module Fleximage
|
2
|
+
|
3
|
+
# Container for Fleximage model method inclusion modules
|
4
|
+
module Model
|
5
|
+
|
6
|
+
class MasterImageNotFound < RuntimeError #:nodoc:
|
7
|
+
end
|
8
|
+
|
9
|
+
# Include acts_as_fleximage class method
|
10
|
+
def self.included(base) #:nodoc:
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Provides class methods for Fleximage for use in model classes. The only class method is
|
15
|
+
# acts_as_fleximage which integrates Fleximage functionality into a model class.
|
16
|
+
#
|
17
|
+
# The following class level accessors also get inserted.
|
18
|
+
#
|
19
|
+
# * +image_directory+: (String, no default) Where the master images are stored, directory path relative to your
|
20
|
+
# app root.
|
21
|
+
# * <tt>s3_bucket</tt>: Name of the bucket on Amazon S3 where your master images are stored. To use this you must
|
22
|
+
# call <tt>establish_connection!</tt> on the aws/s3 gem form your app's initilization to authenticate with your
|
23
|
+
# S3 account.
|
24
|
+
# * +use_creation_date_based_directories+: (Boolean, default +true+) If true, master images will be stored in
|
25
|
+
# directories based on creation date. For example: <tt>"#{image_directory}/2007/11/24/123.png"</tt> for an
|
26
|
+
# image with an id of 123 and a creation date of November 24, 2007. Turing this off would cause the path
|
27
|
+
# to be "#{image_directory}/123.png" instead. This helps keep the OS from having directories that are too
|
28
|
+
# full.
|
29
|
+
# * +image_storage_format+: (:png or :jpg, default :png) The format of your master images. Using :png will give
|
30
|
+
# you the best quality, since the master images as stored as lossless version of the original upload. :jpg
|
31
|
+
# will apply lossy compression, but the master image file sizes will be much smaller. If storage space is a
|
32
|
+
# concern, us :jpg.
|
33
|
+
# * +require_image+: (Boolean, default +true+) The model will raise a validation error if no image is uploaded
|
34
|
+
# with the record. Setting to false allows record to be saved with no images.
|
35
|
+
# * +missing_image_message+: (String, default "is required") Validation message to display when no image was uploaded for
|
36
|
+
# a record.
|
37
|
+
# * +invalid_image_message+: (String default "was not a readable image") Validation message when an image is uploaded, but is not an
|
38
|
+
# image format that can be read by RMagick.
|
39
|
+
# * +output_image_jpg_quality+: (Integer, default 85) When rendering JPGs, this represents the amount of
|
40
|
+
# compression. Valid values are 0-100, where 0 is very small and very ugly, and 100 is near lossless but
|
41
|
+
# very large in filesize.
|
42
|
+
# * +default_image_path+: (String, nil default) If no image is present for this record, the image at this path will be
|
43
|
+
# used instead. Useful for a placeholder graphic for new content that may not have an image just yet.
|
44
|
+
# * +default_image+: A hash which defines an empty starting image. This hash look like: <tt>:size => '123x456',
|
45
|
+
# :color => :transparent</tt>, where <tt>:size</tt> defines the dimensions of the default image, and <tt>:color</tt>
|
46
|
+
# defines the fill. <tt>:color</tt> can be a named color as a string ('red'), :transparent, or a Magick::Pixel object.
|
47
|
+
# * +preprocess_image+: (Block, no default) Call this class method just like you would call +operate+ in a view.
|
48
|
+
# The image transoformation in the provided block will be run on every uploaded image before its saved as the
|
49
|
+
# master image.
|
50
|
+
#
|
51
|
+
# Example:
|
52
|
+
#
|
53
|
+
# class Photo < ActiveRecord::Base
|
54
|
+
# acts_as_fleximage do
|
55
|
+
# image_directory 'public/images/uploaded'
|
56
|
+
# use_creation_date_based_directories true
|
57
|
+
# image_storage_format :png
|
58
|
+
# require_image true
|
59
|
+
# missing_image_message 'is required'
|
60
|
+
# invalid_image_message 'was not a readable image'
|
61
|
+
# default_image_path 'public/images/no_photo_yet.png'
|
62
|
+
# default_image nil
|
63
|
+
# output_image_jpg_quality 85
|
64
|
+
#
|
65
|
+
# preprocess_image do |image|
|
66
|
+
# image.resize '1024x768'
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # normal model methods...
|
71
|
+
# end
|
72
|
+
module ClassMethods
|
73
|
+
|
74
|
+
# Use this method to include Fleximage functionality in your model. It takes an
|
75
|
+
# options hash with a single required key, :+image_directory+. This key should
|
76
|
+
# point to the directory you want your images stored on your server. Or
|
77
|
+
# configure with a nice looking block.
|
78
|
+
def acts_as_fleximage(options = {})
|
79
|
+
|
80
|
+
# Include the necesary instance methods
|
81
|
+
include Fleximage::Model::InstanceMethods
|
82
|
+
|
83
|
+
# Call this class method just like you would call +operate+ in a view.
|
84
|
+
# The image transoformation in the provided block will be run on every uploaded image before its saved as the
|
85
|
+
# master image.
|
86
|
+
def self.preprocess_image(&block)
|
87
|
+
preprocess_image_operation(block)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Internal method to ask this class if it stores image in the DB.
|
91
|
+
def self.db_store?
|
92
|
+
return false if s3_store?
|
93
|
+
if respond_to?(:columns)
|
94
|
+
columns.find do |col|
|
95
|
+
col.name == 'image_file_data'
|
96
|
+
end
|
97
|
+
else
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.s3_store?
|
103
|
+
!!s3_bucket
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.file_store?
|
107
|
+
!db_store? && !s3_store?
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.has_store?
|
111
|
+
respond_to?(:columns) && (db_store? || s3_store? || image_directory)
|
112
|
+
end
|
113
|
+
|
114
|
+
# validation callback
|
115
|
+
validate :validate_image if respond_to?(:validate)
|
116
|
+
|
117
|
+
# The filename of the temp image. Used for storing of good images when validation fails
|
118
|
+
# and the form needs to be redisplayed.
|
119
|
+
attr_reader :image_file_temp
|
120
|
+
|
121
|
+
# Setter for jpg compression quality at the instance level
|
122
|
+
attr_accessor :jpg_compression_quality
|
123
|
+
|
124
|
+
# Where images get stored
|
125
|
+
dsl_accessor :image_directory
|
126
|
+
|
127
|
+
# Amazon S3 bucket where the master images are stored
|
128
|
+
dsl_accessor :s3_bucket
|
129
|
+
|
130
|
+
# Put uploads from different days into different subdirectories
|
131
|
+
dsl_accessor :use_creation_date_based_directories, :default => true
|
132
|
+
|
133
|
+
# The format are master images are stored in
|
134
|
+
dsl_accessor :image_storage_format, :default => Proc.new { :png }
|
135
|
+
|
136
|
+
# Require a valid image. Defaults to true. Set to false if its ok to have no image for
|
137
|
+
dsl_accessor :require_image, :default => true
|
138
|
+
|
139
|
+
|
140
|
+
def self.translate_error_message(name, fallback, options = {})
|
141
|
+
translation = I18n.translate "activerecord.errors.models.#{self.model_name.underscore}.#{name}", options
|
142
|
+
if translation.match /translation missing:/
|
143
|
+
I18n.translate "activerecord.errors.messages.#{name}", options.merge({ :default => fallback })
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Missing image message
|
148
|
+
#dsl_accessor :missing_image_message, :default => 'is required'
|
149
|
+
def self.missing_image_message(str = nil)
|
150
|
+
if str.nil?
|
151
|
+
if @missing_image_message
|
152
|
+
@missing_image_message
|
153
|
+
else
|
154
|
+
translate_error_message("missing_image", "is required")
|
155
|
+
end
|
156
|
+
|
157
|
+
else
|
158
|
+
@missing_image_message = str
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# Invalid image message
|
164
|
+
#dsl_accessor :invalid_image_message, :default => 'was not a readable image'
|
165
|
+
def self.invalid_image_message(str = nil)
|
166
|
+
if str.nil?
|
167
|
+
if @invalid_image_message
|
168
|
+
@invalid_image_message
|
169
|
+
else
|
170
|
+
translate_error_message("invalid_image", "was not a readable image")
|
171
|
+
end
|
172
|
+
else
|
173
|
+
@invalid_image_message = str
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Image too small message
|
178
|
+
# Should include {{minimum}}
|
179
|
+
def self.image_too_small_message(str = nil)
|
180
|
+
fb = "is too small (Minimum: {{minimum}})"
|
181
|
+
if str.nil?
|
182
|
+
minimum_size = Fleximage::Operator::Base.size_to_xy(validates_image_size).join('x')
|
183
|
+
if @image_too_small_message
|
184
|
+
@image_too_small_message.gsub("{{minimum}}", minimum_size)
|
185
|
+
else
|
186
|
+
translate_error_message("image_too_small", fb.gsub("{{minimum}}", minimum_size), :minimum => minimum_size)
|
187
|
+
end
|
188
|
+
else
|
189
|
+
@image_too_small_message = str
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Sets the quality of rendered JPGs
|
194
|
+
dsl_accessor :output_image_jpg_quality, :default => 85
|
195
|
+
|
196
|
+
# Set a default image to use when no image has been assigned to this record
|
197
|
+
dsl_accessor :default_image_path
|
198
|
+
|
199
|
+
# Set a default image based on a a size and fill
|
200
|
+
dsl_accessor :default_image
|
201
|
+
|
202
|
+
# A block that processes an image before it gets saved as the master image of a record.
|
203
|
+
# Can be helpful to resize potentially huge images to something more manageable. Set via
|
204
|
+
# the "preprocess_image { |image| ... }" class method.
|
205
|
+
dsl_accessor :preprocess_image_operation
|
206
|
+
|
207
|
+
# Set a minimum size ([x, y] e.g. 200, '800x600', [800, 600])
|
208
|
+
# Set '0x600' to just enforce y size or
|
209
|
+
# '800x0' to just validate x size.
|
210
|
+
dsl_accessor :validates_image_size
|
211
|
+
|
212
|
+
# Image related save and destroy callbacks
|
213
|
+
if respond_to?(:before_save)
|
214
|
+
after_destroy :delete_image_file
|
215
|
+
before_save :pre_save
|
216
|
+
after_save :post_save
|
217
|
+
end
|
218
|
+
|
219
|
+
# execute configuration block
|
220
|
+
yield if block_given?
|
221
|
+
|
222
|
+
# Create S3 bucket if it's not present
|
223
|
+
if s3_bucket
|
224
|
+
begin
|
225
|
+
AWS::S3::Bucket.find(s3_bucket)
|
226
|
+
rescue AWS::S3::NoSuchBucket
|
227
|
+
AWS::S3::Bucket.create(s3_bucket)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# set the image directory from passed options
|
232
|
+
image_directory options[:image_directory] if options[:image_directory]
|
233
|
+
|
234
|
+
# Require the declaration of a master image storage directory
|
235
|
+
if respond_to?(:validate) && !image_directory && !db_store? && !s3_store? && !default_image && !default_image_path
|
236
|
+
raise "No place to put images! Declare this via the :image_directory => 'path/to/directory' option\n"+
|
237
|
+
"Or add a database column named image_file_data for DB storage\n"+
|
238
|
+
"Or set :virtual to true if this class has no image store at all\n"+
|
239
|
+
"Or set a default image to show with :default_image or :default_image_path"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def image_file_exists(file)
|
244
|
+
# File must be a valid object
|
245
|
+
return false if file.nil?
|
246
|
+
|
247
|
+
# Get the size of the file. file.size works for form-uploaded images, file.stat.size works
|
248
|
+
# for file object created by File.open('foo.jpg', 'rb'). It must have a size > 0.
|
249
|
+
return false if (file.respond_to?(:size) ? file.size : file.stat.size) <= 0
|
250
|
+
|
251
|
+
# object must respond to the read method to fetch its contents.
|
252
|
+
return false if !file.respond_to?(:read)
|
253
|
+
|
254
|
+
# file validation passed, return true
|
255
|
+
true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Provides methods that every model instance that acts_as_fleximage needs.
|
260
|
+
module InstanceMethods
|
261
|
+
|
262
|
+
# Returns the path to the master image file for this record.
|
263
|
+
#
|
264
|
+
# @some_image.directory_path #=> /var/www/myapp/uploaded_images
|
265
|
+
#
|
266
|
+
# If this model has a created_at field, it will use a directory
|
267
|
+
# structure based on the creation date, to prevent hitting the OS imposed
|
268
|
+
# limit on the number files in a directory.
|
269
|
+
#
|
270
|
+
# @some_image.directory_path #=> /var/www/myapp/uploaded_images/2008/3/30
|
271
|
+
def directory_path
|
272
|
+
directory = self.class.image_directory
|
273
|
+
raise 'No image directory was defined, cannot generate path' unless directory
|
274
|
+
|
275
|
+
# base directory
|
276
|
+
directory = "#{RAILS_ROOT}/#{directory}" unless /^\// =~ directory
|
277
|
+
|
278
|
+
# specific creation date based directory suffix.
|
279
|
+
creation = self[:created_at] || self[:created_on]
|
280
|
+
if self.class.use_creation_date_based_directories && creation
|
281
|
+
"#{directory}/#{creation.year}/#{creation.month}/#{creation.day}"
|
282
|
+
else
|
283
|
+
directory
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Returns the path to the master image file for this record.
|
288
|
+
#
|
289
|
+
# @some_image.file_path #=> /var/www/myapp/uploaded_images/123.png
|
290
|
+
def file_path
|
291
|
+
"#{directory_path}/#{id}.#{extension}"
|
292
|
+
end
|
293
|
+
|
294
|
+
# Returns original format of the image if the image_format column exists
|
295
|
+
# otherwise returns the globally set format.
|
296
|
+
def extension
|
297
|
+
if self.respond_to?( :image_format)
|
298
|
+
case image_format
|
299
|
+
when "JPEG"
|
300
|
+
"jpg"
|
301
|
+
else
|
302
|
+
image_format ? image_format.downcase : self.class.image_storage_format
|
303
|
+
end
|
304
|
+
else
|
305
|
+
self.class.image_storage_format
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def url_format
|
310
|
+
extension.to_sym
|
311
|
+
end
|
312
|
+
|
313
|
+
# Sets the image file for this record to an uploaded file. This can
|
314
|
+
# be called directly, or passively like from an ActiveRecord mass
|
315
|
+
# assignment.
|
316
|
+
#
|
317
|
+
# Rails will automatically call this method for you, in most of the
|
318
|
+
# situations you would expect it to.
|
319
|
+
#
|
320
|
+
# # via mass assignment, the most common form you'll probably use
|
321
|
+
# Photo.new(params[:photo])
|
322
|
+
# Photo.create(params[:photo])
|
323
|
+
#
|
324
|
+
# # via explicit assignment hash
|
325
|
+
# Photo.new(:image_file => params[:photo][:image_file])
|
326
|
+
# Photo.create(:image_file => params[:photo][:image_file])
|
327
|
+
#
|
328
|
+
# # Direct Assignment, usually not needed
|
329
|
+
# photo = Photo.new
|
330
|
+
# photo.image_file = params[:photo][:image_file]
|
331
|
+
#
|
332
|
+
# # via an association proxy
|
333
|
+
# p = Product.find(1)
|
334
|
+
# p.images.create(params[:photo])
|
335
|
+
def image_file=(file)
|
336
|
+
if self.class.image_file_exists(file)
|
337
|
+
|
338
|
+
# Create RMagick Image object from uploaded file
|
339
|
+
if file.path
|
340
|
+
@uploaded_image = Magick::Image.read(file.path).first
|
341
|
+
else
|
342
|
+
@uploaded_image = Magick::Image.from_blob(file.read).first
|
343
|
+
end
|
344
|
+
|
345
|
+
# Sanitize image data
|
346
|
+
@uploaded_image.colorspace = Magick::RGBColorspace
|
347
|
+
@uploaded_image.density = '72'
|
348
|
+
|
349
|
+
# Save meta data to database
|
350
|
+
set_magic_attributes(file)
|
351
|
+
|
352
|
+
# Success, make sure everything is valid
|
353
|
+
@invalid_image = false
|
354
|
+
save_temp_image(file) unless @dont_save_temp
|
355
|
+
end
|
356
|
+
rescue Magick::ImageMagickError => e
|
357
|
+
error_strings = [
|
358
|
+
'Improper image header',
|
359
|
+
'no decode delegate for this image format',
|
360
|
+
'UnableToOpenBlob',
|
361
|
+
'Must specify image size'
|
362
|
+
]
|
363
|
+
if e.to_s =~ /#{error_strings.join('|')}/
|
364
|
+
@invalid_image = true
|
365
|
+
else
|
366
|
+
raise e
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def image_file
|
371
|
+
has_image?
|
372
|
+
end
|
373
|
+
|
374
|
+
# Assign the image via a URL, which will make the plugin go
|
375
|
+
# and fetch the image at the provided URL. The image will be stored
|
376
|
+
# locally as a master image for that record from then on. This is
|
377
|
+
# intended to be used along side the image upload to allow people the
|
378
|
+
# choice to upload from their local machine, or pull from the internet.
|
379
|
+
#
|
380
|
+
# @photo.image_file_url = 'http://foo.com/bar.jpg'
|
381
|
+
def image_file_url=(file_url)
|
382
|
+
@image_file_url = file_url
|
383
|
+
if file_url =~ %r{^(https?|ftp)://}
|
384
|
+
file = open(file_url)
|
385
|
+
|
386
|
+
# Force a URL based file to have an original_filename
|
387
|
+
eval <<-CODE
|
388
|
+
def file.original_filename
|
389
|
+
"#{file_url}"
|
390
|
+
end
|
391
|
+
CODE
|
392
|
+
|
393
|
+
self.image_file = file
|
394
|
+
|
395
|
+
elsif file_url.empty?
|
396
|
+
# Nothing to process, move along
|
397
|
+
|
398
|
+
else
|
399
|
+
# invalid URL, raise invalid image validation error
|
400
|
+
@invalid_image = true
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# Set the image for this record by reading in file data as a string.
|
405
|
+
#
|
406
|
+
# data = File.read('my_image_file.jpg')
|
407
|
+
# photo = Photo.find(123)
|
408
|
+
# photo.image_file_string = data
|
409
|
+
# photo.save
|
410
|
+
def image_file_string=(data)
|
411
|
+
self.image_file = StringIO.new(data)
|
412
|
+
end
|
413
|
+
|
414
|
+
# Set the image for this record by reading in a file as a base64 encoded string.
|
415
|
+
#
|
416
|
+
# data = Base64.encode64(File.read('my_image_file.jpg'))
|
417
|
+
# photo = Photo.find(123)
|
418
|
+
# photo.image_file_base64 = data
|
419
|
+
# photo.save
|
420
|
+
def image_file_base64=(data)
|
421
|
+
self.image_file_string = Base64.decode64(data)
|
422
|
+
end
|
423
|
+
|
424
|
+
# Sets the uploaded image to the name of a file in RAILS_ROOT/tmp that was just
|
425
|
+
# uploaded. Use as a hidden field in your forms to keep an uploaded image when
|
426
|
+
# validation fails and the form needs to be redisplayed
|
427
|
+
def image_file_temp=(file_name)
|
428
|
+
if !@uploaded_image && file_name && file_name.present? && file_name !~ %r{\.\./}
|
429
|
+
@image_file_temp = file_name
|
430
|
+
file_path = "#{RAILS_ROOT}/tmp/fleximage/#{file_name}"
|
431
|
+
|
432
|
+
@dont_save_temp = true
|
433
|
+
if File.exists?(file_path)
|
434
|
+
File.open(file_path, 'rb') do |f|
|
435
|
+
self.image_file = f
|
436
|
+
end
|
437
|
+
end
|
438
|
+
@dont_save_temp = false
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Return the @image_file_url that was previously assigned. This is not saved
|
443
|
+
# in the database, and only exists to make forms happy.
|
444
|
+
def image_file_url
|
445
|
+
@image_file_url
|
446
|
+
end
|
447
|
+
|
448
|
+
# Return true if this record has an image.
|
449
|
+
def has_image?
|
450
|
+
@uploaded_image || @output_image || has_saved_image?
|
451
|
+
end
|
452
|
+
|
453
|
+
def has_saved_image?
|
454
|
+
if self.class.db_store?
|
455
|
+
!!image_file_data
|
456
|
+
elsif self.class.s3_store?
|
457
|
+
AWS::S3::S3Object.exists?("#{id}.#{self.class.image_storage_format}", self.class.s3_bucket)
|
458
|
+
elsif self.class.file_store?
|
459
|
+
File.exists?(file_path)
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Call from a .flexi view template. This enables the rendering of operators
|
464
|
+
# so that you can transform your image. This is the method that is the foundation
|
465
|
+
# of .flexi views. Every view should consist of image manipulation code inside a
|
466
|
+
# block passed to this method.
|
467
|
+
#
|
468
|
+
# # app/views/photos/thumb.jpg.flexi
|
469
|
+
# @photo.operate do |image|
|
470
|
+
# image.resize '320x240'
|
471
|
+
# end
|
472
|
+
def operate(&block)
|
473
|
+
returning self do
|
474
|
+
proxy = ImageProxy.new(load_image, self)
|
475
|
+
block.call(proxy)
|
476
|
+
@output_image = proxy.image
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Self destructive operate. This will modify the master image for this record with
|
481
|
+
# the updated and processed result of the operation AND SAVES THE RECORD
|
482
|
+
def operate!(&block)
|
483
|
+
operate(&block)
|
484
|
+
self.image_file_string = output_image
|
485
|
+
save
|
486
|
+
end
|
487
|
+
|
488
|
+
# Load the image from disk/DB, or return the cached and potentially
|
489
|
+
# processed output image.
|
490
|
+
def load_image #:nodoc:
|
491
|
+
@output_image ||= @uploaded_image
|
492
|
+
|
493
|
+
# Return the current image if we have loaded it already
|
494
|
+
return @output_image if @output_image
|
495
|
+
|
496
|
+
# Load the image from disk
|
497
|
+
if self.class.db_store?
|
498
|
+
# Load the image from the database column
|
499
|
+
if image_file_data && image_file_data.present?
|
500
|
+
@output_image = Magick::Image.from_blob(image_file_data).first
|
501
|
+
end
|
502
|
+
|
503
|
+
elsif self.class.s3_store?
|
504
|
+
# Load image from S3
|
505
|
+
filename = "#{id}.#{self.class.image_storage_format}"
|
506
|
+
bucket = self.class.s3_bucket
|
507
|
+
|
508
|
+
if AWS::S3::S3Object.exists?(filename, bucket)
|
509
|
+
@output_image = Magick::Image.from_blob(AWS::S3::S3Object.value(filename, bucket)).first
|
510
|
+
end
|
511
|
+
|
512
|
+
else
|
513
|
+
# Load the image from the disk
|
514
|
+
@output_image = Magick::Image.read(file_path).first
|
515
|
+
|
516
|
+
end
|
517
|
+
|
518
|
+
if @output_image
|
519
|
+
@output_image
|
520
|
+
else
|
521
|
+
master_image_not_found
|
522
|
+
end
|
523
|
+
|
524
|
+
rescue Magick::ImageMagickError => e
|
525
|
+
if e.to_s =~ /unable to open (file|image)/
|
526
|
+
master_image_not_found
|
527
|
+
else
|
528
|
+
raise e
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Convert the current output image to a jpg, and return it in binary form. options support a
|
533
|
+
# :format key that can be :jpg, :gif or :png
|
534
|
+
def output_image(options = {}) #:nodoc:
|
535
|
+
format = (options[:format] || :jpg).to_s.upcase
|
536
|
+
@output_image.format = format
|
537
|
+
@output_image.strip!
|
538
|
+
if format == 'JPG'
|
539
|
+
quality = @jpg_compression_quality || self.class.output_image_jpg_quality
|
540
|
+
@output_image.to_blob { self.quality = quality }
|
541
|
+
else
|
542
|
+
@output_image.to_blob
|
543
|
+
end
|
544
|
+
ensure
|
545
|
+
GC.start
|
546
|
+
end
|
547
|
+
|
548
|
+
# Delete the image file for this record. This is automatically ran after this record gets
|
549
|
+
# destroyed, but you can call it manually if you want to remove the image from the record.
|
550
|
+
def delete_image_file
|
551
|
+
return unless self.class.has_store?
|
552
|
+
|
553
|
+
if self.class.db_store?
|
554
|
+
update_attribute :image_file_data, nil unless frozen?
|
555
|
+
elsif self.class.s3_store?
|
556
|
+
AWS::S3::S3Object.delete "#{id}.#{self.class.image_storage_format}", self.class.s3_bucket
|
557
|
+
else
|
558
|
+
File.delete(file_path) if File.exists?(file_path)
|
559
|
+
end
|
560
|
+
|
561
|
+
clear_magic_attributes
|
562
|
+
|
563
|
+
self
|
564
|
+
end
|
565
|
+
|
566
|
+
# Execute image presence and validity validations.
|
567
|
+
def validate_image #:nodoc:
|
568
|
+
field_name = (@image_file_url && @image_file_url.present?) ? :image_file_url : :image_file
|
569
|
+
|
570
|
+
# Could not read the file as an image
|
571
|
+
if @invalid_image
|
572
|
+
errors.add field_name, self.class.invalid_image_message
|
573
|
+
|
574
|
+
# no image uploaded and one is required
|
575
|
+
elsif self.class.require_image && !has_image?
|
576
|
+
errors.add field_name, self.class.missing_image_message
|
577
|
+
|
578
|
+
# Image does not meet minimum size
|
579
|
+
elsif self.class.validates_image_size && !@uploaded_image.nil?
|
580
|
+
x, y = Fleximage::Operator::Base.size_to_xy(self.class.validates_image_size)
|
581
|
+
|
582
|
+
if @uploaded_image.columns < x || @uploaded_image.rows < y
|
583
|
+
errors.add field_name, self.class.image_too_small_message
|
584
|
+
end
|
585
|
+
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
private
|
590
|
+
# Perform pre save tasks. Preprocess the image, and write it to DB.
|
591
|
+
def pre_save
|
592
|
+
if @uploaded_image
|
593
|
+
# perform preprocessing
|
594
|
+
perform_preprocess_operation
|
595
|
+
|
596
|
+
# Convert to storage format
|
597
|
+
@uploaded_image.format = self.class.image_storage_format.to_s.upcase unless respond_to?(:image_format)
|
598
|
+
|
599
|
+
# Write image data to the DB field
|
600
|
+
if self.class.db_store?
|
601
|
+
self.image_file_data = @uploaded_image.to_blob
|
602
|
+
end
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
# Write image to file system/S3 and cleanup garbage.
|
607
|
+
def post_save
|
608
|
+
if @uploaded_image
|
609
|
+
if self.class.file_store?
|
610
|
+
# Make sure target directory exists
|
611
|
+
FileUtils.mkdir_p(directory_path)
|
612
|
+
|
613
|
+
# Write master image file
|
614
|
+
@uploaded_image.write(file_path)
|
615
|
+
|
616
|
+
elsif self.class.s3_store?
|
617
|
+
blob = StringIO.new(@uploaded_image.to_blob)
|
618
|
+
AWS::S3::S3Object.store("#{id}.#{self.class.image_storage_format}", blob, self.class.s3_bucket)
|
619
|
+
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# Cleanup temp files
|
624
|
+
delete_temp_image
|
625
|
+
|
626
|
+
# Start GC to close up memory leaks
|
627
|
+
if @uploaded_image
|
628
|
+
GC.start
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Preprocess this image before saving
|
633
|
+
def perform_preprocess_operation
|
634
|
+
if self.class.preprocess_image_operation
|
635
|
+
operate(&self.class.preprocess_image_operation)
|
636
|
+
set_magic_attributes #update width and height magic columns
|
637
|
+
@uploaded_image = @output_image
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
def clear_magic_attributes
|
642
|
+
unless frozen?
|
643
|
+
self.image_filename = nil if respond_to?(:image_filename=)
|
644
|
+
self.image_width = nil if respond_to?(:image_width=)
|
645
|
+
self.image_height = nil if respond_to?(:image_height=)
|
646
|
+
self.image_format = nil if respond_to?(:image_format=)
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
# If any magic column names exists fill them with image meta data.
|
651
|
+
def set_magic_attributes(file = nil)
|
652
|
+
if file && self.respond_to?(:image_filename=)
|
653
|
+
filename = file.original_filename if file.respond_to?(:original_filename)
|
654
|
+
filename = file.basename if file.respond_to?(:basename)
|
655
|
+
self.image_filename = filename
|
656
|
+
end
|
657
|
+
self.image_width = @uploaded_image.columns if self.respond_to?(:image_width=)
|
658
|
+
self.image_height = @uploaded_image.rows if self.respond_to?(:image_height=)
|
659
|
+
self.image_format = @uploaded_image.format if self.respond_to?(:image_format=)
|
660
|
+
end
|
661
|
+
|
662
|
+
# Save the image in the rails tmp directory
|
663
|
+
def save_temp_image(file)
|
664
|
+
file_name = file.respond_to?(:original_filename) ? file.original_filename : file.path
|
665
|
+
@image_file_temp = Time.now.to_f.to_s.sub('.', '_')
|
666
|
+
path = "#{RAILS_ROOT}/tmp/fleximage"
|
667
|
+
FileUtils.mkdir_p(path)
|
668
|
+
File.open("#{path}/#{@image_file_temp}", 'wb') do |f|
|
669
|
+
file.rewind
|
670
|
+
f.write file.read
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
# Delete the temp image after its no longer needed
|
675
|
+
def delete_temp_image
|
676
|
+
FileUtils.rm_rf "#{RAILS_ROOT}/tmp/fleximage/#{@image_file_temp}"
|
677
|
+
end
|
678
|
+
|
679
|
+
# Load the default image, or raise an expection
|
680
|
+
def master_image_not_found
|
681
|
+
# Load the default image from a path
|
682
|
+
if self.class.default_image_path
|
683
|
+
@output_image = Magick::Image.read("#{RAILS_ROOT}/#{self.class.default_image_path}").first
|
684
|
+
|
685
|
+
# Or create a default image
|
686
|
+
elsif self.class.default_image
|
687
|
+
x, y = Fleximage::Operator::Base.size_to_xy(self.class.default_image[:size])
|
688
|
+
color = self.class.default_image[:color]
|
689
|
+
|
690
|
+
@output_image = Magick::Image.new(x, y) do
|
691
|
+
self.background_color = color if color && color != :transparent
|
692
|
+
end
|
693
|
+
|
694
|
+
# No default, not master image, so raise exception
|
695
|
+
else
|
696
|
+
message = "Master image was not found for this record"
|
697
|
+
|
698
|
+
if !self.class.db_store?
|
699
|
+
message << "\nExpected image to be at:"
|
700
|
+
message << "\n #{file_path}"
|
701
|
+
end
|
702
|
+
|
703
|
+
raise MasterImageNotFound, message
|
704
|
+
end
|
705
|
+
ensure
|
706
|
+
GC.start
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
end
|
711
|
+
end
|