dougmcbride-fleximage 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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