robinboening-fleximage 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. data/CHANGELOG.rdoc +14 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +257 -0
  4. data/Rakefile +49 -0
  5. data/VERSION +1 -0
  6. data/autotest.rb +5 -0
  7. data/fleximage.gemspec +179 -0
  8. data/init.rb +1 -0
  9. data/lib/dsl_accessor.rb +52 -0
  10. data/lib/fleximage.rb +56 -0
  11. data/lib/fleximage/aviary_controller.rb +75 -0
  12. data/lib/fleximage/blank.rb +70 -0
  13. data/lib/fleximage/helper.rb +41 -0
  14. data/lib/fleximage/image_proxy.rb +69 -0
  15. data/lib/fleximage/legacy_view.rb +63 -0
  16. data/lib/fleximage/model.rb +711 -0
  17. data/lib/fleximage/operator/background.rb +62 -0
  18. data/lib/fleximage/operator/base.rb +189 -0
  19. data/lib/fleximage/operator/border.rb +50 -0
  20. data/lib/fleximage/operator/crop.rb +58 -0
  21. data/lib/fleximage/operator/image_overlay.rb +85 -0
  22. data/lib/fleximage/operator/resize.rb +92 -0
  23. data/lib/fleximage/operator/shadow.rb +87 -0
  24. data/lib/fleximage/operator/text.rb +104 -0
  25. data/lib/fleximage/operator/trim.rb +14 -0
  26. data/lib/fleximage/operator/unsharp_mask.rb +36 -0
  27. data/lib/fleximage/rmagick_image_patch.rb +5 -0
  28. data/lib/fleximage/view.rb +57 -0
  29. data/tasks/fleximage_tasks.rake +154 -0
  30. data/test/fixtures/100x1.jpg +0 -0
  31. data/test/fixtures/100x100.jpg +0 -0
  32. data/test/fixtures/1x1.jpg +0 -0
  33. data/test/fixtures/1x100.jpg +0 -0
  34. data/test/fixtures/cmyk.jpg +0 -0
  35. data/test/fixtures/not_a_photo.xml +1 -0
  36. data/test/fixtures/photo.jpg +0 -0
  37. data/test/mock_file.rb +21 -0
  38. data/test/rails_root/app/controllers/application.rb +10 -0
  39. data/test/rails_root/app/controllers/avatars_controller.rb +85 -0
  40. data/test/rails_root/app/controllers/photo_bares_controller.rb +85 -0
  41. data/test/rails_root/app/controllers/photo_dbs_controller.rb +85 -0
  42. data/test/rails_root/app/controllers/photo_files_controller.rb +85 -0
  43. data/test/rails_root/app/helpers/application_helper.rb +3 -0
  44. data/test/rails_root/app/helpers/avatars_helper.rb +2 -0
  45. data/test/rails_root/app/helpers/photo_bares_helper.rb +2 -0
  46. data/test/rails_root/app/helpers/photo_dbs_helper.rb +2 -0
  47. data/test/rails_root/app/helpers/photo_files_helper.rb +2 -0
  48. data/test/rails_root/app/locales/de.yml +7 -0
  49. data/test/rails_root/app/locales/en.yml +8 -0
  50. data/test/rails_root/app/models/abstract.rb +8 -0
  51. data/test/rails_root/app/models/avatar.rb +4 -0
  52. data/test/rails_root/app/models/photo_bare.rb +7 -0
  53. data/test/rails_root/app/models/photo_custom_error.rb +10 -0
  54. data/test/rails_root/app/models/photo_db.rb +3 -0
  55. data/test/rails_root/app/models/photo_file.rb +3 -0
  56. data/test/rails_root/app/models/photo_s3.rb +5 -0
  57. data/test/rails_root/app/views/avatars/edit.html.erb +17 -0
  58. data/test/rails_root/app/views/avatars/index.html.erb +20 -0
  59. data/test/rails_root/app/views/avatars/new.html.erb +16 -0
  60. data/test/rails_root/app/views/avatars/show.html.erb +8 -0
  61. data/test/rails_root/app/views/layouts/avatars.html.erb +17 -0
  62. data/test/rails_root/app/views/layouts/photo_bares.html.erb +17 -0
  63. data/test/rails_root/app/views/layouts/photo_dbs.html.erb +17 -0
  64. data/test/rails_root/app/views/layouts/photo_files.html.erb +17 -0
  65. data/test/rails_root/app/views/photo_bares/edit.html.erb +12 -0
  66. data/test/rails_root/app/views/photo_bares/index.html.erb +18 -0
  67. data/test/rails_root/app/views/photo_bares/new.html.erb +11 -0
  68. data/test/rails_root/app/views/photo_bares/show.html.erb +3 -0
  69. data/test/rails_root/app/views/photo_dbs/edit.html.erb +32 -0
  70. data/test/rails_root/app/views/photo_dbs/index.html.erb +26 -0
  71. data/test/rails_root/app/views/photo_dbs/new.html.erb +31 -0
  72. data/test/rails_root/app/views/photo_dbs/show.html.erb +23 -0
  73. data/test/rails_root/app/views/photo_files/edit.html.erb +27 -0
  74. data/test/rails_root/app/views/photo_files/index.html.erb +24 -0
  75. data/test/rails_root/app/views/photo_files/new.html.erb +26 -0
  76. data/test/rails_root/app/views/photo_files/show.html.erb +18 -0
  77. data/test/rails_root/config/boot.rb +109 -0
  78. data/test/rails_root/config/database.yml +7 -0
  79. data/test/rails_root/config/environment.rb +66 -0
  80. data/test/rails_root/config/environments/development.rb +18 -0
  81. data/test/rails_root/config/environments/production.rb +19 -0
  82. data/test/rails_root/config/environments/sqlite3.rb +0 -0
  83. data/test/rails_root/config/environments/test.rb +22 -0
  84. data/test/rails_root/config/initializers/inflections.rb +10 -0
  85. data/test/rails_root/config/initializers/load_translations.rb +4 -0
  86. data/test/rails_root/config/initializers/mime_types.rb +5 -0
  87. data/test/rails_root/config/routes.rb +43 -0
  88. data/test/rails_root/db/migrate/001_create_photo_files.rb +16 -0
  89. data/test/rails_root/db/migrate/002_create_photo_dbs.rb +16 -0
  90. data/test/rails_root/db/migrate/003_create_photo_bares.rb +12 -0
  91. data/test/rails_root/db/migrate/004_create_avatars.rb +13 -0
  92. data/test/rails_root/db/migrate/005_create_photo_s3s.rb +12 -0
  93. data/test/rails_root/public/.htaccess +40 -0
  94. data/test/rails_root/public/404.html +30 -0
  95. data/test/rails_root/public/422.html +30 -0
  96. data/test/rails_root/public/500.html +30 -0
  97. data/test/rails_root/public/dispatch.cgi +10 -0
  98. data/test/rails_root/public/dispatch.fcgi +24 -0
  99. data/test/rails_root/public/dispatch.rb +10 -0
  100. data/test/rails_root/public/favicon.ico +0 -0
  101. data/test/rails_root/public/images/rails.png +0 -0
  102. data/test/rails_root/public/index.html +277 -0
  103. data/test/rails_root/public/javascripts/application.js +2 -0
  104. data/test/rails_root/public/javascripts/controls.js +963 -0
  105. data/test/rails_root/public/javascripts/dragdrop.js +972 -0
  106. data/test/rails_root/public/javascripts/effects.js +1120 -0
  107. data/test/rails_root/public/javascripts/prototype.js +4225 -0
  108. data/test/rails_root/public/robots.txt +5 -0
  109. data/test/rails_root/public/stylesheets/scaffold.css +74 -0
  110. data/test/rails_root/vendor/plugins/fleximage/init.rb +2 -0
  111. data/test/s3_stubs.rb +7 -0
  112. data/test/test_helper.rb +82 -0
  113. data/test/unit/abstract_test.rb +20 -0
  114. data/test/unit/basic_model_test.rb +40 -0
  115. data/test/unit/blank_test.rb +23 -0
  116. data/test/unit/default_image_path_option_test.rb +16 -0
  117. data/test/unit/dsl_accessor_test.rb +120 -0
  118. data/test/unit/file_upload_from_local_test.rb +31 -0
  119. data/test/unit/file_upload_from_strings_test.rb +23 -0
  120. data/test/unit/file_upload_from_url_test.rb +35 -0
  121. data/test/unit/file_upload_to_db_test.rb +41 -0
  122. data/test/unit/has_store_test.rb +4 -0
  123. data/test/unit/i18n_messages_test.rb +49 -0
  124. data/test/unit/image_directory_option_test.rb +20 -0
  125. data/test/unit/image_proxy_test.rb +17 -0
  126. data/test/unit/image_storage_format_option_test.rb +31 -0
  127. data/test/unit/magic_columns_test.rb +34 -0
  128. data/test/unit/minimum_image_size_test.rb +56 -0
  129. data/test/unit/operator_base_test.rb +124 -0
  130. data/test/unit/operator_resize_test.rb +18 -0
  131. data/test/unit/preprocess_image_option_test.rb +21 -0
  132. data/test/unit/require_image_option_test.rb +30 -0
  133. data/test/unit/temp_image_test.rb +23 -0
  134. data/test/unit/use_creation_date_based_directories_option_test.rb +16 -0
  135. metadata +258 -0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w(lib fleximage)))
@@ -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