tvdeyen-fleximage 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +14 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +36 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/autotest.rb +5 -0
- data/init.rb +1 -0
- data/lib/dsl_accessor.rb +52 -0
- data/lib/fleximage.rb +59 -0
- data/lib/fleximage/aviary_controller.rb +75 -0
- data/lib/fleximage/blank.rb +70 -0
- data/lib/fleximage/helper.rb +41 -0
- data/lib/fleximage/image_proxy.rb +69 -0
- data/lib/fleximage/legacy_view.rb +63 -0
- data/lib/fleximage/model.rb +713 -0
- data/lib/fleximage/operator/background.rb +62 -0
- data/lib/fleximage/operator/base.rb +189 -0
- data/lib/fleximage/operator/border.rb +50 -0
- data/lib/fleximage/operator/crop.rb +58 -0
- data/lib/fleximage/operator/image_overlay.rb +85 -0
- data/lib/fleximage/operator/resize.rb +92 -0
- data/lib/fleximage/operator/shadow.rb +87 -0
- data/lib/fleximage/operator/text.rb +104 -0
- data/lib/fleximage/operator/trim.rb +14 -0
- data/lib/fleximage/operator/unsharp_mask.rb +36 -0
- data/lib/fleximage/rails3_view.rb +31 -0
- data/lib/fleximage/rmagick_image_patch.rb +7 -0
- data/lib/fleximage/view.rb +57 -0
- data/lib/tasks/fleximage_tasks.rake +154 -0
- data/test/fixtures/100x1.jpg +0 -0
- data/test/fixtures/100x100.jpg +0 -0
- data/test/fixtures/1x1.jpg +0 -0
- data/test/fixtures/1x100.jpg +0 -0
- data/test/fixtures/cmyk.jpg +0 -0
- data/test/fixtures/not_a_photo.xml +1 -0
- data/test/fixtures/photo.jpg +0 -0
- data/test/mock_file.rb +21 -0
- data/test/rails_root/app/controllers/application.rb +10 -0
- data/test/rails_root/app/controllers/avatars_controller.rb +85 -0
- data/test/rails_root/app/controllers/photo_bares_controller.rb +85 -0
- data/test/rails_root/app/controllers/photo_dbs_controller.rb +85 -0
- data/test/rails_root/app/controllers/photo_files_controller.rb +85 -0
- data/test/rails_root/app/helpers/application_helper.rb +3 -0
- data/test/rails_root/app/helpers/avatars_helper.rb +2 -0
- data/test/rails_root/app/helpers/photo_bares_helper.rb +2 -0
- data/test/rails_root/app/helpers/photo_dbs_helper.rb +2 -0
- data/test/rails_root/app/helpers/photo_files_helper.rb +2 -0
- data/test/rails_root/app/locales/de.yml +7 -0
- data/test/rails_root/app/locales/en.yml +8 -0
- data/test/rails_root/app/models/abstract.rb +8 -0
- data/test/rails_root/app/models/avatar.rb +4 -0
- data/test/rails_root/app/models/photo_bare.rb +7 -0
- data/test/rails_root/app/models/photo_custom_error.rb +10 -0
- data/test/rails_root/app/models/photo_db.rb +3 -0
- data/test/rails_root/app/models/photo_file.rb +3 -0
- data/test/rails_root/app/models/photo_s3.rb +5 -0
- data/test/rails_root/app/views/avatars/edit.html.erb +17 -0
- data/test/rails_root/app/views/avatars/index.html.erb +20 -0
- data/test/rails_root/app/views/avatars/new.html.erb +16 -0
- data/test/rails_root/app/views/avatars/show.html.erb +8 -0
- data/test/rails_root/app/views/layouts/avatars.html.erb +17 -0
- data/test/rails_root/app/views/layouts/photo_bares.html.erb +17 -0
- data/test/rails_root/app/views/layouts/photo_dbs.html.erb +17 -0
- data/test/rails_root/app/views/layouts/photo_files.html.erb +17 -0
- data/test/rails_root/app/views/photo_bares/edit.html.erb +12 -0
- data/test/rails_root/app/views/photo_bares/index.html.erb +18 -0
- data/test/rails_root/app/views/photo_bares/new.html.erb +11 -0
- data/test/rails_root/app/views/photo_bares/show.html.erb +3 -0
- data/test/rails_root/app/views/photo_dbs/edit.html.erb +32 -0
- data/test/rails_root/app/views/photo_dbs/index.html.erb +26 -0
- data/test/rails_root/app/views/photo_dbs/new.html.erb +31 -0
- data/test/rails_root/app/views/photo_dbs/show.html.erb +23 -0
- data/test/rails_root/app/views/photo_files/edit.html.erb +27 -0
- data/test/rails_root/app/views/photo_files/index.html.erb +24 -0
- data/test/rails_root/app/views/photo_files/new.html.erb +26 -0
- data/test/rails_root/app/views/photo_files/show.html.erb +18 -0
- data/test/rails_root/config/boot.rb +109 -0
- data/test/rails_root/config/database.yml +7 -0
- data/test/rails_root/config/environment.rb +66 -0
- data/test/rails_root/config/environments/development.rb +18 -0
- data/test/rails_root/config/environments/production.rb +19 -0
- data/test/rails_root/config/environments/sqlite3.rb +0 -0
- data/test/rails_root/config/environments/test.rb +22 -0
- data/test/rails_root/config/initializers/inflections.rb +10 -0
- data/test/rails_root/config/initializers/load_translations.rb +4 -0
- data/test/rails_root/config/initializers/mime_types.rb +5 -0
- data/test/rails_root/config/routes.rb +43 -0
- data/test/rails_root/db/migrate/001_create_photo_files.rb +16 -0
- data/test/rails_root/db/migrate/002_create_photo_dbs.rb +16 -0
- data/test/rails_root/db/migrate/003_create_photo_bares.rb +12 -0
- data/test/rails_root/db/migrate/004_create_avatars.rb +13 -0
- data/test/rails_root/db/migrate/005_create_photo_s3s.rb +12 -0
- data/test/rails_root/public/.htaccess +40 -0
- data/test/rails_root/public/404.html +30 -0
- data/test/rails_root/public/422.html +30 -0
- data/test/rails_root/public/500.html +30 -0
- data/test/rails_root/public/dispatch.cgi +10 -0
- data/test/rails_root/public/dispatch.fcgi +24 -0
- data/test/rails_root/public/dispatch.rb +10 -0
- data/test/rails_root/public/favicon.ico +0 -0
- data/test/rails_root/public/images/rails.png +0 -0
- data/test/rails_root/public/index.html +277 -0
- data/test/rails_root/public/javascripts/application.js +2 -0
- data/test/rails_root/public/javascripts/controls.js +963 -0
- data/test/rails_root/public/javascripts/dragdrop.js +972 -0
- data/test/rails_root/public/javascripts/effects.js +1120 -0
- data/test/rails_root/public/javascripts/prototype.js +4225 -0
- data/test/rails_root/public/robots.txt +5 -0
- data/test/rails_root/public/stylesheets/scaffold.css +74 -0
- data/test/rails_root/vendor/plugins/fleximage/init.rb +2 -0
- data/test/s3_stubs.rb +7 -0
- data/test/test_helper.rb +82 -0
- data/test/unit/abstract_test.rb +20 -0
- data/test/unit/basic_model_test.rb +40 -0
- data/test/unit/blank_test.rb +23 -0
- data/test/unit/default_image_path_option_test.rb +16 -0
- data/test/unit/dsl_accessor_test.rb +120 -0
- data/test/unit/file_upload_from_local_test.rb +31 -0
- data/test/unit/file_upload_from_strings_test.rb +23 -0
- data/test/unit/file_upload_from_url_test.rb +35 -0
- data/test/unit/file_upload_to_db_test.rb +41 -0
- data/test/unit/has_store_test.rb +4 -0
- data/test/unit/i18n_messages_test.rb +49 -0
- data/test/unit/image_directory_option_test.rb +20 -0
- data/test/unit/image_proxy_test.rb +17 -0
- data/test/unit/image_storage_format_option_test.rb +31 -0
- data/test/unit/magic_columns_test.rb +34 -0
- data/test/unit/minimum_image_size_test.rb +56 -0
- data/test/unit/operator_base_test.rb +124 -0
- data/test/unit/operator_resize_test.rb +18 -0
- data/test/unit/preprocess_image_option_test.rb +21 -0
- data/test/unit/require_image_option_test.rb +30 -0
- data/test/unit/temp_image_test.rb +23 -0
- data/test/unit/use_creation_date_based_directories_option_test.rb +16 -0
- data/tvdeyen-fleximage.gemspec +180 -0
- 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
|