fleximage 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +27 -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 +235 -0
- data/init.rb +1 -0
- data/lib/dsl_accessor.rb +52 -0
- data/lib/fleximage.rb +59 -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 +689 -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/string_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/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 +15 -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/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/test_helper.rb +81 -0
- data/test/unit/abstract_test.rb +20 -0
- data/test/unit/basic_model_test.rb +30 -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/i18n_messages_test.rb +49 -0
- data/test/unit/image_directory_option_test.rb +18 -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 +30 -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 +17 -0
- data/test/unit/use_creation_date_based_directories_option_test.rb +16 -0
- metadata +279 -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,59 @@
|
|
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
|
+
# Patch String class for ruby < 1.9
|
15
|
+
require 'fleximage/string_patch'
|
16
|
+
|
17
|
+
# Apply a few RMagick patches
|
18
|
+
require 'fleximage/rmagick_image_patch'
|
19
|
+
|
20
|
+
# Load dsl_accessor from lib
|
21
|
+
require 'dsl_accessor'
|
22
|
+
|
23
|
+
# Load Operators
|
24
|
+
require 'fleximage/operator/base'
|
25
|
+
Dir.entries("#{File.dirname(__FILE__)}/fleximage/operator").each do |filename|
|
26
|
+
require "fleximage/operator/#{filename.gsub('.rb', '')}" if filename =~ /\.rb$/
|
27
|
+
end
|
28
|
+
|
29
|
+
# Setup Model
|
30
|
+
require 'fleximage/model'
|
31
|
+
ActiveRecord::Base.class_eval { include Fleximage::Model }
|
32
|
+
|
33
|
+
# Image Proxy
|
34
|
+
require 'fleximage/image_proxy'
|
35
|
+
|
36
|
+
# Setup View
|
37
|
+
ActionController::Base.exempt_from_layout :flexi
|
38
|
+
if defined?(ActionView::Template)
|
39
|
+
# Rails >= 2.1
|
40
|
+
require 'fleximage/view'
|
41
|
+
ActionView::Template.register_template_handler :flexi, Fleximage::View
|
42
|
+
else
|
43
|
+
# Rails < 2.1
|
44
|
+
require 'fleximage/legacy_view'
|
45
|
+
ActionView::Base.register_template_handler :flexi, Fleximage::LegacyView
|
46
|
+
end
|
47
|
+
|
48
|
+
# Setup Helper
|
49
|
+
require 'fleximage/helper'
|
50
|
+
ActionView::Base.class_eval { include Fleximage::Helper }
|
51
|
+
|
52
|
+
# Setup Aviary Controller
|
53
|
+
ActionController::Base.class_eval{ include Fleximage::AviaryController }
|
54
|
+
|
55
|
+
# Register mime types
|
56
|
+
Mime::Type.register_alias "image/pjpeg", :jpg # IE6 sends jpg data as "image/pjpeg". Silly IE6.
|
57
|
+
Mime::Type.register "image/jpeg", :jpg
|
58
|
+
Mime::Type.register "image/gif", :gif
|
59
|
+
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,689 @@
|
|
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? || 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
|
+
raise 'No image directory was defined, cannot generate path' unless self.class.image_directory
|
273
|
+
|
274
|
+
# base directory
|
275
|
+
directory = "#{RAILS_ROOT}/#{self.class.image_directory}"
|
276
|
+
|
277
|
+
# specific creation date based directory suffix.
|
278
|
+
creation = self[:created_at] || self[:created_on]
|
279
|
+
if self.class.use_creation_date_based_directories && creation
|
280
|
+
"#{directory}/#{creation.year}/#{creation.month}/#{creation.day}"
|
281
|
+
else
|
282
|
+
directory
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Returns the path to the master image file for this record.
|
287
|
+
#
|
288
|
+
# @some_image.file_path #=> /var/www/myapp/uploaded_images/123.png
|
289
|
+
def file_path
|
290
|
+
"#{directory_path}/#{id}.#{self.class.image_storage_format}"
|
291
|
+
end
|
292
|
+
|
293
|
+
# Sets the image file for this record to an uploaded file. This can
|
294
|
+
# be called directly, or passively like from an ActiveRecord mass
|
295
|
+
# assignment.
|
296
|
+
#
|
297
|
+
# Rails will automatically call this method for you, in most of the
|
298
|
+
# situations you would expect it to.
|
299
|
+
#
|
300
|
+
# # via mass assignment, the most common form you'll probably use
|
301
|
+
# Photo.new(params[:photo])
|
302
|
+
# Photo.create(params[:photo])
|
303
|
+
#
|
304
|
+
# # via explicit assignment hash
|
305
|
+
# Photo.new(:image_file => params[:photo][:image_file])
|
306
|
+
# Photo.create(:image_file => params[:photo][:image_file])
|
307
|
+
#
|
308
|
+
# # Direct Assignment, usually not needed
|
309
|
+
# photo = Photo.new
|
310
|
+
# photo.image_file = params[:photo][:image_file]
|
311
|
+
#
|
312
|
+
# # via an association proxy
|
313
|
+
# p = Product.find(1)
|
314
|
+
# p.images.create(params[:photo])
|
315
|
+
def image_file=(file)
|
316
|
+
if self.class.image_file_exists(file)
|
317
|
+
|
318
|
+
# Create RMagick Image object from uploaded file
|
319
|
+
if file.path
|
320
|
+
@uploaded_image = Magick::Image.read(file.path).first
|
321
|
+
else
|
322
|
+
@uploaded_image = Magick::Image.from_blob(file.read).first
|
323
|
+
end
|
324
|
+
|
325
|
+
# Sanitize image data
|
326
|
+
@uploaded_image.colorspace = Magick::RGBColorspace
|
327
|
+
@uploaded_image.density = '72'
|
328
|
+
|
329
|
+
# Save meta data to database
|
330
|
+
set_magic_attributes(file)
|
331
|
+
|
332
|
+
# Success, make sure everything is valid
|
333
|
+
@invalid_image = false
|
334
|
+
save_temp_image(file) unless @dont_save_temp
|
335
|
+
end
|
336
|
+
rescue Magick::ImageMagickError => e
|
337
|
+
error_strings = [
|
338
|
+
'Improper image header',
|
339
|
+
'no decode delegate for this image format',
|
340
|
+
'UnableToOpenBlob',
|
341
|
+
'Must specify image size'
|
342
|
+
]
|
343
|
+
if e.to_s =~ /#{error_strings.join('|')}/
|
344
|
+
@invalid_image = true
|
345
|
+
else
|
346
|
+
raise e
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def image_file
|
351
|
+
has_image?
|
352
|
+
end
|
353
|
+
|
354
|
+
# Assign the image via a URL, which will make the plugin go
|
355
|
+
# and fetch the image at the provided URL. The image will be stored
|
356
|
+
# locally as a master image for that record from then on. This is
|
357
|
+
# intended to be used along side the image upload to allow people the
|
358
|
+
# choice to upload from their local machine, or pull from the internet.
|
359
|
+
#
|
360
|
+
# @photo.image_file_url = 'http://foo.com/bar.jpg'
|
361
|
+
def image_file_url=(file_url)
|
362
|
+
@image_file_url = file_url
|
363
|
+
if file_url =~ %r{^(https?|ftp)://}
|
364
|
+
file = open(file_url)
|
365
|
+
|
366
|
+
# Force a URL based file to have an original_filename
|
367
|
+
eval <<-CODE
|
368
|
+
def file.original_filename
|
369
|
+
"#{file_url}"
|
370
|
+
end
|
371
|
+
CODE
|
372
|
+
|
373
|
+
self.image_file = file
|
374
|
+
|
375
|
+
elsif file_url.empty?
|
376
|
+
# Nothing to process, move along
|
377
|
+
|
378
|
+
else
|
379
|
+
# invalid URL, raise invalid image validation error
|
380
|
+
@invalid_image = true
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# Set the image for this record by reading in file data as a string.
|
385
|
+
#
|
386
|
+
# data = File.read('my_image_file.jpg')
|
387
|
+
# photo = Photo.find(123)
|
388
|
+
# photo.image_file_string = data
|
389
|
+
# photo.save
|
390
|
+
def image_file_string=(data)
|
391
|
+
self.image_file = StringIO.new(data)
|
392
|
+
end
|
393
|
+
|
394
|
+
# Set the image for this record by reading in a file as a base64 encoded string.
|
395
|
+
#
|
396
|
+
# data = Base64.encode64(File.read('my_image_file.jpg'))
|
397
|
+
# photo = Photo.find(123)
|
398
|
+
# photo.image_file_base64 = data
|
399
|
+
# photo.save
|
400
|
+
def image_file_base64=(data)
|
401
|
+
self.image_file_string = Base64.decode64(data)
|
402
|
+
end
|
403
|
+
|
404
|
+
# Sets the uploaded image to the name of a file in RAILS_ROOT/tmp that was just
|
405
|
+
# uploaded. Use as a hidden field in your forms to keep an uploaded image when
|
406
|
+
# validation fails and the form needs to be redisplayed
|
407
|
+
def image_file_temp=(file_name)
|
408
|
+
if !@uploaded_image && file_name && file_name.present?
|
409
|
+
@image_file_temp = file_name
|
410
|
+
file_path = "#{RAILS_ROOT}/tmp/fleximage/#{file_name}"
|
411
|
+
|
412
|
+
@dont_save_temp = true
|
413
|
+
if File.exists?(file_path)
|
414
|
+
File.open(file_path, 'rb') do |f|
|
415
|
+
self.image_file = f
|
416
|
+
end
|
417
|
+
end
|
418
|
+
@dont_save_temp = false
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Return the @image_file_url that was previously assigned. This is not saved
|
423
|
+
# in the database, and only exists to make forms happy.
|
424
|
+
def image_file_url
|
425
|
+
@image_file_url
|
426
|
+
end
|
427
|
+
|
428
|
+
# Return true if this record has an image.
|
429
|
+
def has_image?
|
430
|
+
@uploaded_image || @output_image || has_saved_image?
|
431
|
+
end
|
432
|
+
|
433
|
+
def has_saved_image?
|
434
|
+
if self.class.db_store?
|
435
|
+
!!image_file_data
|
436
|
+
elsif self.class.s3_store?
|
437
|
+
AWS::S3::S3Object.exists?("#{id}.#{self.class.image_storage_format}", self.class.s3_bucket)
|
438
|
+
elsif self.class.file_store?
|
439
|
+
File.exists?(file_path)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Call from a .flexi view template. This enables the rendering of operators
|
444
|
+
# so that you can transform your image. This is the method that is the foundation
|
445
|
+
# of .flexi views. Every view should consist of image manipulation code inside a
|
446
|
+
# block passed to this method.
|
447
|
+
#
|
448
|
+
# # app/views/photos/thumb.jpg.flexi
|
449
|
+
# @photo.operate do |image|
|
450
|
+
# image.resize '320x240'
|
451
|
+
# end
|
452
|
+
def operate(&block)
|
453
|
+
returning self do
|
454
|
+
proxy = ImageProxy.new(load_image, self)
|
455
|
+
block.call(proxy)
|
456
|
+
@output_image = proxy.image
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Self destructive operate. This will modify the master image for this record with
|
461
|
+
# the updated and processed result of the operation AND SAVES THE RECORD
|
462
|
+
def operate!(&block)
|
463
|
+
operate(&block)
|
464
|
+
self.image_file_string = output_image
|
465
|
+
save
|
466
|
+
end
|
467
|
+
|
468
|
+
# Load the image from disk/DB, or return the cached and potentially
|
469
|
+
# processed output image.
|
470
|
+
def load_image #:nodoc:
|
471
|
+
@output_image ||= @uploaded_image
|
472
|
+
|
473
|
+
# Return the current image if we have loaded it already
|
474
|
+
return @output_image if @output_image
|
475
|
+
|
476
|
+
# Load the image from disk
|
477
|
+
if self.class.db_store?
|
478
|
+
# Load the image from the database column
|
479
|
+
if image_file_data && image_file_data.present?
|
480
|
+
@output_image = Magick::Image.from_blob(image_file_data).first
|
481
|
+
end
|
482
|
+
|
483
|
+
elsif self.class.s3_store?
|
484
|
+
# Load image from S3
|
485
|
+
filename = "#{id}.#{self.class.image_storage_format}"
|
486
|
+
bucket = self.class.s3_bucket
|
487
|
+
|
488
|
+
if AWS::S3::S3Object.exists?(filename, bucket)
|
489
|
+
@output_image = Magick::Image.from_blob(AWS::S3::S3Object.value(filename, bucket)).first
|
490
|
+
end
|
491
|
+
|
492
|
+
else
|
493
|
+
# Load the image from the disk
|
494
|
+
@output_image = Magick::Image.read(file_path).first
|
495
|
+
|
496
|
+
end
|
497
|
+
|
498
|
+
if @output_image
|
499
|
+
@output_image
|
500
|
+
else
|
501
|
+
master_image_not_found
|
502
|
+
end
|
503
|
+
|
504
|
+
rescue Magick::ImageMagickError => e
|
505
|
+
if e.to_s =~ /unable to open (file|image)/
|
506
|
+
master_image_not_found
|
507
|
+
else
|
508
|
+
raise e
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Convert the current output image to a jpg, and return it in binary form. options support a
|
513
|
+
# :format key that can be :jpg, :gif or :png
|
514
|
+
def output_image(options = {}) #:nodoc:
|
515
|
+
format = (options[:format] || :jpg).to_s.upcase
|
516
|
+
@output_image.format = format
|
517
|
+
@output_image.strip!
|
518
|
+
if format = 'JPG'
|
519
|
+
quality = @jpg_compression_quality || self.class.output_image_jpg_quality
|
520
|
+
@output_image.to_blob { self.quality = quality }
|
521
|
+
else
|
522
|
+
@output_image.to_blob
|
523
|
+
end
|
524
|
+
ensure
|
525
|
+
GC.start
|
526
|
+
end
|
527
|
+
|
528
|
+
# Delete the image file for this record. This is automatically ran after this record gets
|
529
|
+
# destroyed, but you can call it manually if you want to remove the image from the record.
|
530
|
+
def delete_image_file
|
531
|
+
return unless self.class.has_store?
|
532
|
+
|
533
|
+
if self.class.db_store?
|
534
|
+
update_attribute :image_file_data, nil unless frozen?
|
535
|
+
elsif self.class.s3_store?
|
536
|
+
AWS::S3::S3Object.delete "#{id}.#{self.class.image_storage_format}", self.class.s3_bucket
|
537
|
+
else
|
538
|
+
File.delete(file_path) if File.exists?(file_path)
|
539
|
+
end
|
540
|
+
|
541
|
+
clear_magic_attributes
|
542
|
+
|
543
|
+
self
|
544
|
+
end
|
545
|
+
|
546
|
+
# Execute image presence and validity validations.
|
547
|
+
def validate_image #:nodoc:
|
548
|
+
field_name = (@image_file_url && @image_file_url.present?) ? :image_file_url : :image_file
|
549
|
+
|
550
|
+
# Could not read the file as an image
|
551
|
+
if @invalid_image
|
552
|
+
errors.add field_name, self.class.invalid_image_message
|
553
|
+
|
554
|
+
# no image uploaded and one is required
|
555
|
+
elsif self.class.require_image && !has_image?
|
556
|
+
errors.add field_name, self.class.missing_image_message
|
557
|
+
|
558
|
+
# Image does not meet minimum size
|
559
|
+
elsif self.class.validates_image_size && !@uploaded_image.nil?
|
560
|
+
x, y = Fleximage::Operator::Base.size_to_xy(self.class.validates_image_size)
|
561
|
+
|
562
|
+
if @uploaded_image.columns < x || @uploaded_image.rows < y
|
563
|
+
errors.add field_name, self.class.image_too_small_message
|
564
|
+
end
|
565
|
+
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
private
|
570
|
+
# Perform pre save tasks. Preprocess the image, and write it to DB.
|
571
|
+
def pre_save
|
572
|
+
if @uploaded_image
|
573
|
+
# perform preprocessing
|
574
|
+
perform_preprocess_operation
|
575
|
+
|
576
|
+
# Convert to storage format
|
577
|
+
@uploaded_image.format = self.class.image_storage_format.to_s.upcase
|
578
|
+
|
579
|
+
# Write image data to the DB field
|
580
|
+
if self.class.db_store?
|
581
|
+
self.image_file_data = @uploaded_image.to_blob
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# Write image to file system/S3 and cleanup garbage.
|
587
|
+
def post_save
|
588
|
+
if @uploaded_image
|
589
|
+
if self.class.file_store?
|
590
|
+
# Make sure target directory exists
|
591
|
+
FileUtils.mkdir_p(directory_path)
|
592
|
+
|
593
|
+
# Write master image file
|
594
|
+
@uploaded_image.write(file_path)
|
595
|
+
|
596
|
+
elsif self.class.s3_store?
|
597
|
+
blob = StringIO.new(@uploaded_image.to_blob)
|
598
|
+
AWS::S3::S3Object.store("#{id}.#{self.class.image_storage_format}", blob, self.class.s3_bucket)
|
599
|
+
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# Cleanup temp files
|
604
|
+
delete_temp_image
|
605
|
+
|
606
|
+
# Start GC to close up memory leaks
|
607
|
+
if @uploaded_image
|
608
|
+
GC.start
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
# Preprocess this image before saving
|
613
|
+
def perform_preprocess_operation
|
614
|
+
if self.class.preprocess_image_operation
|
615
|
+
operate(&self.class.preprocess_image_operation)
|
616
|
+
set_magic_attributes #update width and height magic columns
|
617
|
+
@uploaded_image = @output_image
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
def clear_magic_attributes
|
622
|
+
unless frozen?
|
623
|
+
self.image_filename = nil if respond_to?(:image_filename=)
|
624
|
+
self.image_width = nil if respond_to?(:image_width=)
|
625
|
+
self.image_height = nil if respond_to?(:image_height=)
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
# If any magic column names exists fill them with image meta data.
|
630
|
+
def set_magic_attributes(file = nil)
|
631
|
+
if file && self.respond_to?(:image_filename=)
|
632
|
+
filename = file.original_filename if file.respond_to?(:original_filename)
|
633
|
+
filename = file.basename if file.respond_to?(:basename)
|
634
|
+
self.image_filename = filename
|
635
|
+
end
|
636
|
+
self.image_width = @uploaded_image.columns if self.respond_to?(:image_width=)
|
637
|
+
self.image_height = @uploaded_image.rows if self.respond_to?(:image_height=)
|
638
|
+
end
|
639
|
+
|
640
|
+
# Save the image in the rails tmp directory
|
641
|
+
def save_temp_image(file)
|
642
|
+
file_name = file.respond_to?(:original_filename) ? file.original_filename : file.path
|
643
|
+
@image_file_temp = Time.now.to_f.to_s.sub('.', '_')
|
644
|
+
path = "#{RAILS_ROOT}/tmp/fleximage"
|
645
|
+
FileUtils.mkdir_p(path)
|
646
|
+
File.open("#{path}/#{@image_file_temp}", 'w') do |f|
|
647
|
+
file.rewind
|
648
|
+
f.write file.read
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
# Delete the temp image after its no longer needed
|
653
|
+
def delete_temp_image
|
654
|
+
FileUtils.rm_rf "#{RAILS_ROOT}/tmp/fleximage/#{@image_file_temp}"
|
655
|
+
end
|
656
|
+
|
657
|
+
# Load the default image, or raise an expection
|
658
|
+
def master_image_not_found
|
659
|
+
# Load the default image from a path
|
660
|
+
if self.class.default_image_path
|
661
|
+
@output_image = Magick::Image.read("#{RAILS_ROOT}/#{self.class.default_image_path}").first
|
662
|
+
|
663
|
+
# Or create a default image
|
664
|
+
elsif self.class.default_image
|
665
|
+
x, y = Fleximage::Operator::Base.size_to_xy(self.class.default_image[:size])
|
666
|
+
color = self.class.default_image[:color]
|
667
|
+
|
668
|
+
@output_image = Magick::Image.new(x, y) do
|
669
|
+
self.background_color = color if color && color != :transparent
|
670
|
+
end
|
671
|
+
|
672
|
+
# No default, not master image, so raise exception
|
673
|
+
else
|
674
|
+
message = "Master image was not found for this record"
|
675
|
+
|
676
|
+
if !self.class.db_store?
|
677
|
+
message << "\nExpected image to be at:"
|
678
|
+
message << "\n #{file_path}"
|
679
|
+
end
|
680
|
+
|
681
|
+
raise MasterImageNotFound, message
|
682
|
+
end
|
683
|
+
ensure
|
684
|
+
GC.start
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
end
|
689
|
+
end
|