fleximage 1.0.0

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