tvdeyen-fleximage 1.0.5

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