dougmcbride-fleximage 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. data/.gitignore +27 -0
  2. data/CHANGELOG.rdoc +14 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +257 -0
  5. data/Rakefile +49 -0
  6. data/VERSION +1 -0
  7. data/autotest.rb +5 -0
  8. data/fleximage.gemspec +236 -0
  9. data/init.rb +1 -0
  10. data/lib/dsl_accessor.rb +52 -0
  11. data/lib/fleximage.rb +59 -0
  12. data/lib/fleximage/aviary_controller.rb +75 -0
  13. data/lib/fleximage/blank.rb +70 -0
  14. data/lib/fleximage/helper.rb +41 -0
  15. data/lib/fleximage/image_proxy.rb +69 -0
  16. data/lib/fleximage/legacy_view.rb +63 -0
  17. data/lib/fleximage/model.rb +726 -0
  18. data/lib/fleximage/operator/background.rb +62 -0
  19. data/lib/fleximage/operator/base.rb +189 -0
  20. data/lib/fleximage/operator/border.rb +50 -0
  21. data/lib/fleximage/operator/crop.rb +58 -0
  22. data/lib/fleximage/operator/image_overlay.rb +85 -0
  23. data/lib/fleximage/operator/resize.rb +92 -0
  24. data/lib/fleximage/operator/shadow.rb +87 -0
  25. data/lib/fleximage/operator/text.rb +104 -0
  26. data/lib/fleximage/operator/trim.rb +14 -0
  27. data/lib/fleximage/operator/unsharp_mask.rb +36 -0
  28. data/lib/fleximage/rmagick_image_patch.rb +5 -0
  29. data/lib/fleximage/string_patch.rb +5 -0
  30. data/lib/fleximage/view.rb +58 -0
  31. data/tasks/fleximage_tasks.rake +154 -0
  32. data/test/fixtures/100x1.jpg +0 -0
  33. data/test/fixtures/100x100.jpg +0 -0
  34. data/test/fixtures/1x1.jpg +0 -0
  35. data/test/fixtures/1x100.jpg +0 -0
  36. data/test/fixtures/cmyk.jpg +0 -0
  37. data/test/fixtures/not_a_photo.xml +1 -0
  38. data/test/fixtures/photo.jpg +0 -0
  39. data/test/mock_file.rb +21 -0
  40. data/test/rails_root/app/controllers/application.rb +10 -0
  41. data/test/rails_root/app/controllers/avatars_controller.rb +85 -0
  42. data/test/rails_root/app/controllers/photo_bares_controller.rb +85 -0
  43. data/test/rails_root/app/controllers/photo_dbs_controller.rb +85 -0
  44. data/test/rails_root/app/controllers/photo_files_controller.rb +85 -0
  45. data/test/rails_root/app/helpers/application_helper.rb +3 -0
  46. data/test/rails_root/app/helpers/avatars_helper.rb +2 -0
  47. data/test/rails_root/app/helpers/photo_bares_helper.rb +2 -0
  48. data/test/rails_root/app/helpers/photo_dbs_helper.rb +2 -0
  49. data/test/rails_root/app/helpers/photo_files_helper.rb +2 -0
  50. data/test/rails_root/app/locales/de.yml +7 -0
  51. data/test/rails_root/app/locales/en.yml +8 -0
  52. data/test/rails_root/app/models/abstract.rb +8 -0
  53. data/test/rails_root/app/models/avatar.rb +4 -0
  54. data/test/rails_root/app/models/photo_bare.rb +7 -0
  55. data/test/rails_root/app/models/photo_custom_error.rb +10 -0
  56. data/test/rails_root/app/models/photo_db.rb +3 -0
  57. data/test/rails_root/app/models/photo_file.rb +3 -0
  58. data/test/rails_root/app/views/avatars/edit.html.erb +17 -0
  59. data/test/rails_root/app/views/avatars/index.html.erb +20 -0
  60. data/test/rails_root/app/views/avatars/new.html.erb +16 -0
  61. data/test/rails_root/app/views/avatars/show.html.erb +8 -0
  62. data/test/rails_root/app/views/layouts/avatars.html.erb +17 -0
  63. data/test/rails_root/app/views/layouts/photo_bares.html.erb +17 -0
  64. data/test/rails_root/app/views/layouts/photo_dbs.html.erb +17 -0
  65. data/test/rails_root/app/views/layouts/photo_files.html.erb +17 -0
  66. data/test/rails_root/app/views/photo_bares/edit.html.erb +12 -0
  67. data/test/rails_root/app/views/photo_bares/index.html.erb +18 -0
  68. data/test/rails_root/app/views/photo_bares/new.html.erb +11 -0
  69. data/test/rails_root/app/views/photo_bares/show.html.erb +3 -0
  70. data/test/rails_root/app/views/photo_dbs/edit.html.erb +32 -0
  71. data/test/rails_root/app/views/photo_dbs/index.html.erb +26 -0
  72. data/test/rails_root/app/views/photo_dbs/new.html.erb +31 -0
  73. data/test/rails_root/app/views/photo_dbs/show.html.erb +23 -0
  74. data/test/rails_root/app/views/photo_files/edit.html.erb +27 -0
  75. data/test/rails_root/app/views/photo_files/index.html.erb +24 -0
  76. data/test/rails_root/app/views/photo_files/new.html.erb +26 -0
  77. data/test/rails_root/app/views/photo_files/show.html.erb +18 -0
  78. data/test/rails_root/config/boot.rb +109 -0
  79. data/test/rails_root/config/database.yml +7 -0
  80. data/test/rails_root/config/environment.rb +66 -0
  81. data/test/rails_root/config/environments/development.rb +18 -0
  82. data/test/rails_root/config/environments/production.rb +19 -0
  83. data/test/rails_root/config/environments/sqlite3.rb +0 -0
  84. data/test/rails_root/config/environments/test.rb +22 -0
  85. data/test/rails_root/config/initializers/inflections.rb +10 -0
  86. data/test/rails_root/config/initializers/load_translations.rb +4 -0
  87. data/test/rails_root/config/initializers/mime_types.rb +5 -0
  88. data/test/rails_root/config/routes.rb +43 -0
  89. data/test/rails_root/db/migrate/001_create_photo_files.rb +16 -0
  90. data/test/rails_root/db/migrate/002_create_photo_dbs.rb +16 -0
  91. data/test/rails_root/db/migrate/003_create_photo_bares.rb +12 -0
  92. data/test/rails_root/db/migrate/004_create_avatars.rb +13 -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/test_helper.rb +81 -0
  112. data/test/unit/abstract_test.rb +20 -0
  113. data/test/unit/basic_model_test.rb +36 -0
  114. data/test/unit/blank_test.rb +23 -0
  115. data/test/unit/default_image_path_option_test.rb +16 -0
  116. data/test/unit/dsl_accessor_test.rb +120 -0
  117. data/test/unit/file_upload_from_local_test.rb +31 -0
  118. data/test/unit/file_upload_from_strings_test.rb +23 -0
  119. data/test/unit/file_upload_from_url_test.rb +35 -0
  120. data/test/unit/file_upload_to_db_test.rb +41 -0
  121. data/test/unit/i18n_messages_test.rb +49 -0
  122. data/test/unit/image_directory_option_test.rb +20 -0
  123. data/test/unit/image_proxy_test.rb +17 -0
  124. data/test/unit/image_storage_format_option_test.rb +31 -0
  125. data/test/unit/magic_columns_test.rb +34 -0
  126. data/test/unit/minimum_image_size_test.rb +56 -0
  127. data/test/unit/operator_base_test.rb +124 -0
  128. data/test/unit/operator_resize_test.rb +18 -0
  129. data/test/unit/preprocess_image_option_test.rb +21 -0
  130. data/test/unit/require_image_option_test.rb +30 -0
  131. data/test/unit/temp_image_test.rb +23 -0
  132. data/test/unit/use_creation_date_based_directories_option_test.rb +16 -0
  133. metadata +284 -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
+
@@ -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,726 @@
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
+ # Use a hash instead of the primary key db value to store the image
131
+ dsl_accessor :use_hashed_key
132
+
133
+ # Put uploads from different days into different subdirectories
134
+ dsl_accessor :use_creation_date_based_directories, :default => true
135
+
136
+ # The format are master images are stored in
137
+ dsl_accessor :image_storage_format, :default => Proc.new { :png }
138
+
139
+ # Require a valid image. Defaults to true. Set to false if its ok to have no image for
140
+ dsl_accessor :require_image, :default => true
141
+
142
+
143
+ def self.translate_error_message(name, fallback, options = {})
144
+ translation = I18n.translate "activerecord.errors.models.#{self.model_name.underscore}.#{name}", options
145
+ if translation.match /translation missing:/
146
+ I18n.translate "activerecord.errors.messages.#{name}", options.merge({ :default => fallback })
147
+ end
148
+ end
149
+
150
+ # Missing image message
151
+ #dsl_accessor :missing_image_message, :default => 'is required'
152
+ def self.missing_image_message(str = nil)
153
+ if str.nil?
154
+ if @missing_image_message
155
+ @missing_image_message
156
+ else
157
+ translate_error_message("missing_image", "is required")
158
+ end
159
+
160
+ else
161
+ @missing_image_message = str
162
+ end
163
+ end
164
+
165
+
166
+ # Invalid image message
167
+ #dsl_accessor :invalid_image_message, :default => 'was not a readable image'
168
+ def self.invalid_image_message(str = nil)
169
+ if str.nil?
170
+ if @invalid_image_message
171
+ @invalid_image_message
172
+ else
173
+ translate_error_message("invalid_image", "was not a readable image")
174
+ end
175
+ else
176
+ @invalid_image_message = str
177
+ end
178
+ end
179
+
180
+ # Image too small message
181
+ # Should include {{minimum}}
182
+ def self.image_too_small_message(str = nil)
183
+ fb = "is too small (Minimum: {{minimum}})"
184
+ if str.nil?
185
+ minimum_size = Fleximage::Operator::Base.size_to_xy(validates_image_size).join('x')
186
+ if @image_too_small_message
187
+ @image_too_small_message.gsub("{{minimum}}", minimum_size)
188
+ else
189
+ translate_error_message("image_too_small", fb.gsub("{{minimum}}", minimum_size), :minimum => minimum_size)
190
+ end
191
+ else
192
+ @image_too_small_message = str
193
+ end
194
+ end
195
+
196
+ # Sets the quality of rendered JPGs
197
+ dsl_accessor :output_image_jpg_quality, :default => 85
198
+
199
+ # Set a default image to use when no image has been assigned to this record
200
+ dsl_accessor :default_image_path
201
+
202
+ # Set a default image based on a a size and fill
203
+ dsl_accessor :default_image
204
+
205
+ # A block that processes an image before it gets saved as the master image of a record.
206
+ # Can be helpful to resize potentially huge images to something more manageable. Set via
207
+ # the "preprocess_image { |image| ... }" class method.
208
+ dsl_accessor :preprocess_image_operation
209
+
210
+ # Set a minimum size ([x, y] e.g. 200, '800x600', [800, 600])
211
+ # Set '0x600' to just enforce y size or
212
+ # '800x0' to just validate x size.
213
+ dsl_accessor :validates_image_size
214
+
215
+ # Image related save and destroy callbacks
216
+ if respond_to?(:before_save)
217
+ after_destroy :delete_image_file
218
+ before_save :pre_save
219
+ after_save :post_save
220
+ before_create :pre_create
221
+ end
222
+
223
+ # execute configuration block
224
+ yield if block_given?
225
+
226
+ # Create S3 bucket if it's not present
227
+ if s3_bucket
228
+ begin
229
+ AWS::S3::Bucket.find(s3_bucket)
230
+ rescue AWS::S3::NoSuchBucket
231
+ AWS::S3::Bucket.create(s3_bucket)
232
+ end
233
+ end
234
+
235
+ # set the image directory from passed options
236
+ image_directory options[:image_directory] if options[:image_directory]
237
+
238
+ # Require the declaration of a master image storage directory
239
+ if respond_to?(:validate) && !image_directory && !db_store? && !s3_store? && !default_image && !default_image_path
240
+ raise "No place to put images! Declare this via the :image_directory => 'path/to/directory' option\n"+
241
+ "Or add a database column named image_file_data for DB storage\n"+
242
+ "Or set :virtual to true if this class has no image store at all\n"+
243
+ "Or set a default image to show with :default_image or :default_image_path"
244
+ end
245
+ end
246
+
247
+ def image_file_exists(file)
248
+ # File must be a valid object
249
+ return false if file.nil?
250
+
251
+ # Get the size of the file. file.size works for form-uploaded images, file.stat.size works
252
+ # for file object created by File.open('foo.jpg', 'rb'). It must have a size > 0.
253
+ return false if (file.respond_to?(:size) ? file.size : file.stat.size) <= 0
254
+
255
+ # object must respond to the read method to fetch its contents.
256
+ return false if !file.respond_to?(:read)
257
+
258
+ # file validation passed, return true
259
+ true
260
+ end
261
+ end
262
+
263
+ # Provides methods that every model instance that acts_as_fleximage needs.
264
+ module InstanceMethods
265
+
266
+ # Returns the path to the master image file for this record.
267
+ #
268
+ # @some_image.directory_path #=> /var/www/myapp/uploaded_images
269
+ #
270
+ # If this model has a created_at field, it will use a directory
271
+ # structure based on the creation date, to prevent hitting the OS imposed
272
+ # limit on the number files in a directory.
273
+ #
274
+ # @some_image.directory_path #=> /var/www/myapp/uploaded_images/2008/3/30
275
+ def directory_path
276
+ directory = self.class.image_directory
277
+ raise 'No image directory was defined, cannot generate path' unless directory
278
+
279
+ # base directory
280
+ directory = "#{RAILS_ROOT}/#{directory}" unless /^\// =~ directory
281
+
282
+ # specific creation date based directory suffix.
283
+ creation = self[:created_at] || self[:created_on]
284
+ if self.class.use_creation_date_based_directories && creation
285
+ "#{directory}/#{creation.year}/#{creation.month}/#{creation.day}"
286
+ else
287
+ directory
288
+ end
289
+ end
290
+
291
+ # Returns the path to the master image file for this record.
292
+ #
293
+ # @some_image.file_path #=> /var/www/myapp/uploaded_images/123.png
294
+ def file_path
295
+ "#{directory_path}/#{key_name}.#{extension}"
296
+ end
297
+
298
+ # Returns original format of the image if the image_format column exists
299
+ # otherwise returns the globally set format.
300
+ def extension
301
+ if self.respond_to?( :image_format)
302
+ case image_format
303
+ when "JPEG"
304
+ "jpg"
305
+ else
306
+ image_format ? image_format.downcase : self.class.image_storage_format
307
+ end
308
+ else
309
+ self.class.image_storage_format
310
+ end
311
+ end
312
+
313
+ def url_format
314
+ extension.to_sym
315
+ end
316
+
317
+ # Sets the image file for this record to an uploaded file. This can
318
+ # be called directly, or passively like from an ActiveRecord mass
319
+ # assignment.
320
+ #
321
+ # Rails will automatically call this method for you, in most of the
322
+ # situations you would expect it to.
323
+ #
324
+ # # via mass assignment, the most common form you'll probably use
325
+ # Photo.new(params[:photo])
326
+ # Photo.create(params[:photo])
327
+ #
328
+ # # via explicit assignment hash
329
+ # Photo.new(:image_file => params[:photo][:image_file])
330
+ # Photo.create(:image_file => params[:photo][:image_file])
331
+ #
332
+ # # Direct Assignment, usually not needed
333
+ # photo = Photo.new
334
+ # photo.image_file = params[:photo][:image_file]
335
+ #
336
+ # # via an association proxy
337
+ # p = Product.find(1)
338
+ # p.images.create(params[:photo])
339
+ def image_file=(file)
340
+ if self.class.image_file_exists(file)
341
+
342
+ # Create RMagick Image object from uploaded file
343
+ if file.path
344
+ @uploaded_image = Magick::Image.read(file.path).first
345
+ else
346
+ @uploaded_image = Magick::Image.from_blob(file.read).first
347
+ end
348
+
349
+ # Sanitize image data
350
+ @uploaded_image.colorspace = Magick::RGBColorspace
351
+ @uploaded_image.density = '72'
352
+
353
+ # Save meta data to database
354
+ set_magic_attributes(file)
355
+
356
+ # Success, make sure everything is valid
357
+ @invalid_image = false
358
+ save_temp_image(file) unless @dont_save_temp
359
+ end
360
+ rescue Magick::ImageMagickError => e
361
+ error_strings = [
362
+ 'Improper image header',
363
+ 'no decode delegate for this image format',
364
+ 'UnableToOpenBlob',
365
+ 'Must specify image size'
366
+ ]
367
+ if e.to_s =~ /#{error_strings.join('|')}/
368
+ @invalid_image = true
369
+ else
370
+ raise e
371
+ end
372
+ end
373
+
374
+ def image_file
375
+ has_image?
376
+ end
377
+
378
+ # Assign the image via a URL, which will make the plugin go
379
+ # and fetch the image at the provided URL. The image will be stored
380
+ # locally as a master image for that record from then on. This is
381
+ # intended to be used along side the image upload to allow people the
382
+ # choice to upload from their local machine, or pull from the internet.
383
+ #
384
+ # @photo.image_file_url = 'http://foo.com/bar.jpg'
385
+ def image_file_url=(file_url)
386
+ @image_file_url = file_url
387
+ if file_url =~ %r{^(https?|ftp)://}
388
+ file = open(file_url)
389
+
390
+ # Force a URL based file to have an original_filename
391
+ eval <<-CODE
392
+ def file.original_filename
393
+ "#{file_url}"
394
+ end
395
+ CODE
396
+
397
+ self.image_file = file
398
+
399
+ elsif file_url.empty?
400
+ # Nothing to process, move along
401
+
402
+ else
403
+ # invalid URL, raise invalid image validation error
404
+ @invalid_image = true
405
+ end
406
+ end
407
+
408
+ # Set the image for this record by reading in file data as a string.
409
+ #
410
+ # data = File.read('my_image_file.jpg')
411
+ # photo = Photo.find(123)
412
+ # photo.image_file_string = data
413
+ # photo.save
414
+ def image_file_string=(data)
415
+ self.image_file = StringIO.new(data)
416
+ end
417
+
418
+ # Set the image for this record by reading in a file as a base64 encoded string.
419
+ #
420
+ # data = Base64.encode64(File.read('my_image_file.jpg'))
421
+ # photo = Photo.find(123)
422
+ # photo.image_file_base64 = data
423
+ # photo.save
424
+ def image_file_base64=(data)
425
+ self.image_file_string = Base64.decode64(data)
426
+ end
427
+
428
+ # Sets the uploaded image to the name of a file in RAILS_ROOT/tmp that was just
429
+ # uploaded. Use as a hidden field in your forms to keep an uploaded image when
430
+ # validation fails and the form needs to be redisplayed
431
+ def image_file_temp=(file_name)
432
+ if !@uploaded_image && file_name && file_name.present? && file_name !~ %r{\.\./}
433
+ @image_file_temp = file_name
434
+ file_path = "#{RAILS_ROOT}/tmp/fleximage/#{file_name}"
435
+
436
+ @dont_save_temp = true
437
+ if File.exists?(file_path)
438
+ File.open(file_path, 'rb') do |f|
439
+ self.image_file = f
440
+ end
441
+ end
442
+ @dont_save_temp = false
443
+ end
444
+ end
445
+
446
+ # Return the @image_file_url that was previously assigned. This is not saved
447
+ # in the database, and only exists to make forms happy.
448
+ def image_file_url
449
+ @image_file_url
450
+ end
451
+
452
+ # Return true if this record has an image.
453
+ def has_image?
454
+ @uploaded_image || @output_image || has_saved_image?
455
+ end
456
+
457
+ def key_name
458
+ self.class.use_hashed_key ? hashed_key : id
459
+ end
460
+
461
+ def s3_image_name
462
+ "#{key_name}.#{self.class.image_storage_format}"
463
+ end
464
+
465
+ def has_saved_image?
466
+ if self.class.db_store?
467
+ !!image_file_data
468
+ elsif self.class.s3_store?
469
+ AWS::S3::S3Object.exists?(s3_image_name, self.class.s3_bucket)
470
+ elsif self.class.file_store?
471
+ File.exists?(file_path)
472
+ end
473
+ end
474
+
475
+ # Call from a .flexi view template. This enables the rendering of operators
476
+ # so that you can transform your image. This is the method that is the foundation
477
+ # of .flexi views. Every view should consist of image manipulation code inside a
478
+ # block passed to this method.
479
+ #
480
+ # # app/views/photos/thumb.jpg.flexi
481
+ # @photo.operate do |image|
482
+ # image.resize '320x240'
483
+ # end
484
+ def operate(&block)
485
+ returning self do
486
+ proxy = ImageProxy.new(load_image, self)
487
+ block.call(proxy)
488
+ @output_image = proxy.image
489
+ end
490
+ end
491
+
492
+ # Self destructive operate. This will modify the master image for this record with
493
+ # the updated and processed result of the operation AND SAVES THE RECORD
494
+ def operate!(&block)
495
+ operate(&block)
496
+ self.image_file_string = output_image
497
+ save
498
+ end
499
+
500
+ # Load the image from disk/DB, or return the cached and potentially
501
+ # processed output image.
502
+ def load_image #:nodoc:
503
+ @output_image ||= @uploaded_image
504
+
505
+ # Return the current image if we have loaded it already
506
+ return @output_image if @output_image
507
+
508
+ # Load the image from disk
509
+ if self.class.db_store?
510
+ # Load the image from the database column
511
+ if image_file_data && image_file_data.present?
512
+ @output_image = Magick::Image.from_blob(image_file_data).first
513
+ end
514
+
515
+ elsif self.class.s3_store?
516
+ # Load image from S3
517
+ bucket = self.class.s3_bucket
518
+
519
+ if AWS::S3::S3Object.exists?(s3_image_name, bucket)
520
+ @output_image = Magick::Image.from_blob(AWS::S3::S3Object.value(s3_image_name, bucket)).first
521
+ end
522
+
523
+ else
524
+ # Load the image from the disk
525
+ @output_image = Magick::Image.read(file_path).first
526
+
527
+ end
528
+
529
+ if @output_image
530
+ @output_image
531
+ else
532
+ master_image_not_found
533
+ end
534
+
535
+ rescue Magick::ImageMagickError => e
536
+ if e.to_s =~ /unable to open (file|image)/
537
+ master_image_not_found
538
+ else
539
+ raise e
540
+ end
541
+ end
542
+
543
+ # Convert the current output image to a jpg, and return it in binary form. options support a
544
+ # :format key that can be :jpg, :gif or :png
545
+ def output_image(options = {}) #:nodoc:
546
+ format = (options[:format] || :jpg).to_s.upcase
547
+ @output_image.format = format
548
+ @output_image.strip!
549
+ if format == 'JPG'
550
+ quality = @jpg_compression_quality || self.class.output_image_jpg_quality
551
+ @output_image.to_blob { self.quality = quality }
552
+ else
553
+ @output_image.to_blob
554
+ end
555
+ ensure
556
+ GC.start
557
+ end
558
+
559
+ # Delete the image file for this record. This is automatically ran after this record gets
560
+ # destroyed, but you can call it manually if you want to remove the image from the record.
561
+ def delete_image_file
562
+ return unless self.class.has_store?
563
+
564
+ if self.class.db_store?
565
+ update_attribute :image_file_data, nil unless frozen?
566
+ elsif self.class.s3_store?
567
+ AWS::S3::S3Object.delete s3_image_name, self.class.s3_bucket
568
+ else
569
+ File.delete(file_path) if File.exists?(file_path)
570
+ end
571
+
572
+ clear_magic_attributes
573
+
574
+ self
575
+ end
576
+
577
+ # Execute image presence and validity validations.
578
+ def validate_image #:nodoc:
579
+ field_name = (@image_file_url && @image_file_url.present?) ? :image_file_url : :image_file
580
+
581
+ # Could not read the file as an image
582
+ if @invalid_image
583
+ errors.add field_name, self.class.invalid_image_message
584
+
585
+ # no image uploaded and one is required
586
+ elsif self.class.require_image && !has_image?
587
+ errors.add field_name, self.class.missing_image_message
588
+
589
+ # Image does not meet minimum size
590
+ elsif self.class.validates_image_size && !@uploaded_image.nil?
591
+ x, y = Fleximage::Operator::Base.size_to_xy(self.class.validates_image_size)
592
+
593
+ if @uploaded_image.columns < x || @uploaded_image.rows < y
594
+ errors.add field_name, self.class.image_too_small_message
595
+ end
596
+
597
+ end
598
+ end
599
+
600
+ private
601
+ def pre_create
602
+ self.hashed_key = ActiveSupport::SecureRandom.hex 16 if self.class.use_hashed_key
603
+ end
604
+
605
+ # Perform pre save tasks. Preprocess the image, and write it to DB.
606
+ def pre_save
607
+ if @uploaded_image
608
+ # perform preprocessing
609
+ perform_preprocess_operation
610
+
611
+ # Convert to storage format
612
+ @uploaded_image.format = self.class.image_storage_format.to_s.upcase unless respond_to?(:image_format)
613
+
614
+ # Write image data to the DB field
615
+ if self.class.db_store?
616
+ self.image_file_data = @uploaded_image.to_blob
617
+ end
618
+ end
619
+ end
620
+
621
+ # Write image to file system/S3 and cleanup garbage.
622
+ def post_save
623
+ if @uploaded_image
624
+ if self.class.file_store?
625
+ # Make sure target directory exists
626
+ FileUtils.mkdir_p(directory_path)
627
+
628
+ # Write master image file
629
+ @uploaded_image.write(file_path)
630
+
631
+ elsif self.class.s3_store?
632
+ blob = StringIO.new(@uploaded_image.to_blob)
633
+ AWS::S3::S3Object.store(s3_image_name, blob, self.class.s3_bucket)
634
+
635
+ end
636
+ end
637
+
638
+ # Cleanup temp files
639
+ delete_temp_image
640
+
641
+ # Start GC to close up memory leaks
642
+ if @uploaded_image
643
+ GC.start
644
+ end
645
+ end
646
+
647
+ # Preprocess this image before saving
648
+ def perform_preprocess_operation
649
+ if self.class.preprocess_image_operation
650
+ operate(&self.class.preprocess_image_operation)
651
+ set_magic_attributes #update width and height magic columns
652
+ @uploaded_image = @output_image
653
+ end
654
+ end
655
+
656
+ def clear_magic_attributes
657
+ unless frozen?
658
+ self.image_filename = nil if respond_to?(:image_filename=)
659
+ self.image_width = nil if respond_to?(:image_width=)
660
+ self.image_height = nil if respond_to?(:image_height=)
661
+ self.image_format = nil if respond_to?(:image_format=)
662
+ end
663
+ end
664
+
665
+ # If any magic column names exists fill them with image meta data.
666
+ def set_magic_attributes(file = nil)
667
+ if file && self.respond_to?(:image_filename=)
668
+ filename = file.original_filename if file.respond_to?(:original_filename)
669
+ filename = file.basename if file.respond_to?(:basename)
670
+ self.image_filename = filename
671
+ end
672
+ self.image_width = @uploaded_image.columns if self.respond_to?(:image_width=)
673
+ self.image_height = @uploaded_image.rows if self.respond_to?(:image_height=)
674
+ self.image_format = @uploaded_image.format if self.respond_to?(:image_format=)
675
+ end
676
+
677
+ # Save the image in the rails tmp directory
678
+ def save_temp_image(file)
679
+ file_name = file.respond_to?(:original_filename) ? file.original_filename : file.path
680
+ @image_file_temp = Time.now.to_f.to_s.sub('.', '_')
681
+ path = "#{RAILS_ROOT}/tmp/fleximage"
682
+ FileUtils.mkdir_p(path)
683
+ File.open("#{path}/#{@image_file_temp}", 'wb') do |f|
684
+ file.rewind
685
+ f.write file.read
686
+ end
687
+ end
688
+
689
+ # Delete the temp image after its no longer needed
690
+ def delete_temp_image
691
+ FileUtils.rm_rf "#{RAILS_ROOT}/tmp/fleximage/#{@image_file_temp}"
692
+ end
693
+
694
+ # Load the default image, or raise an expection
695
+ def master_image_not_found
696
+ # Load the default image from a path
697
+ if self.class.default_image_path
698
+ @output_image = Magick::Image.read("#{RAILS_ROOT}/#{self.class.default_image_path}").first
699
+
700
+ # Or create a default image
701
+ elsif self.class.default_image
702
+ x, y = Fleximage::Operator::Base.size_to_xy(self.class.default_image[:size])
703
+ color = self.class.default_image[:color]
704
+
705
+ @output_image = Magick::Image.new(x, y) do
706
+ self.background_color = color if color && color != :transparent
707
+ end
708
+
709
+ # No default, not master image, so raise exception
710
+ else
711
+ message = "Master image was not found for this record"
712
+
713
+ if !self.class.db_store?
714
+ message << "\nExpected image to be at:"
715
+ message << "\n #{file_path}"
716
+ end
717
+
718
+ raise MasterImageNotFound, message
719
+ end
720
+ ensure
721
+ GC.start
722
+ end
723
+ end
724
+
725
+ end
726
+ end