dynamic_image 1.0.4 → 2.0.0.beta1
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.
- checksums.yaml +7 -0
- data/{LICENSE → MIT-LICENSE} +1 -1
- data/README.md +57 -80
- data/Rakefile +7 -30
- data/lib/dynamic_image/belongs_to.rb +22 -0
- data/lib/dynamic_image/controller.rb +94 -0
- data/lib/dynamic_image/digest_verifier.rb +62 -0
- data/lib/dynamic_image/errors.rb +10 -0
- data/lib/dynamic_image/helper.rb +139 -85
- data/lib/dynamic_image/image_sizing.rb +156 -0
- data/lib/dynamic_image/metadata.rb +84 -0
- data/lib/dynamic_image/model/dimensions.rb +95 -0
- data/lib/dynamic_image/model/validations.rb +94 -0
- data/lib/dynamic_image/model.rb +130 -0
- data/lib/dynamic_image/processed_image.rb +119 -0
- data/lib/dynamic_image/railtie.rb +18 -0
- data/lib/dynamic_image/routing.rb +24 -0
- data/lib/dynamic_image/version.rb +5 -0
- data/lib/dynamic_image.rb +14 -71
- data/lib/rails/generators/dynamic_image/resource/resource_generator.rb +74 -0
- metadata +130 -97
- data/VERSION +0 -1
- data/app/controllers/images_controller.rb +0 -79
- data/app/models/image.rb +0 -188
- data/config/routes.rb +0 -16
- data/dynamic_image.gemspec +0 -62
- data/dynamic_image.sublime-project +0 -9
- data/dynamic_image.sublime-workspace +0 -1599
- data/init.rb +0 -1
- data/install.rb +0 -1
- data/lib/binary_storage/active_record_extensions.rb +0 -144
- data/lib/binary_storage/blob.rb +0 -104
- data/lib/binary_storage.rb +0 -28
- data/lib/dynamic_image/active_record_extensions.rb +0 -60
- data/lib/dynamic_image/engine.rb +0 -6
- data/lib/dynamic_image/filterset.rb +0 -79
- data/lib/generators/dynamic_image/USAGE +0 -5
- data/lib/generators/dynamic_image/dynamic_image_generator.rb +0 -38
- data/lib/generators/dynamic_image/templates/migrations/create_images.rb +0 -21
- data/uninstall.rb +0 -1
@@ -0,0 +1,156 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DynamicImage
|
4
|
+
# = DynamicImage Image Sizing
|
5
|
+
#
|
6
|
+
# Calculates cropping and fitting for image sizes.
|
7
|
+
class ImageSizing
|
8
|
+
def initialize(record, options={})
|
9
|
+
@record = record
|
10
|
+
@uncropped = options[:uncropped] ? true : false
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calculates crop geometry. The given vector is scaled
|
14
|
+
# to match the image size, since DynamicImage performs
|
15
|
+
# cropping before resizing.
|
16
|
+
#
|
17
|
+
# ==== Example
|
18
|
+
#
|
19
|
+
# image = Image.find(params[:id]) # 320x200 image
|
20
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
21
|
+
#
|
22
|
+
# sizing.crop_geometry(Vector2d(100, 100))
|
23
|
+
# # => [Vector2d(200, 200), Vector2d(60, 0)]
|
24
|
+
#
|
25
|
+
# Returns a tuple with crop size and crop start vectors.
|
26
|
+
def crop_geometry(ratio_vector)
|
27
|
+
# Maximize the crop area to fit the image size
|
28
|
+
crop_size = ratio_vector.fit(size).round
|
29
|
+
|
30
|
+
# Ignore pixels outside the pre-cropped area for now
|
31
|
+
center = crop_gravity - crop_start
|
32
|
+
|
33
|
+
start = center - (crop_size / 2).floor
|
34
|
+
start = clamp(start, crop_size, size)
|
35
|
+
|
36
|
+
[crop_size, (start + crop_start)]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns crop geometry as an ImageMagick compatible string.
|
40
|
+
#
|
41
|
+
# ==== Example
|
42
|
+
#
|
43
|
+
# image = Image.find(params[:id]) # 320x200 image
|
44
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
45
|
+
#
|
46
|
+
# sizing.crop_geometry(Vector2d(100, 100)) # => "200x200+60+0"
|
47
|
+
def crop_geometry_string(ratio_vector)
|
48
|
+
crop_size, start = crop_geometry(ratio_vector)
|
49
|
+
crop_size.floor.to_s + "+#{start.x.to_i}+#{start.y.to_i}"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adjusts +fit_size+ to fit the image dimensions.
|
53
|
+
# Any dimension set to zero will be ignored.
|
54
|
+
#
|
55
|
+
# ==== Options
|
56
|
+
#
|
57
|
+
# * <tt>:crop</tt> - Don't keep aspect ratio. This will allow
|
58
|
+
# the image to be cropped to the requested size.
|
59
|
+
# * <tt>:upscale</tt> - Don't limit to the size of the image.
|
60
|
+
# Images smaller than the given size will be scaled up.
|
61
|
+
#
|
62
|
+
# ==== Examples
|
63
|
+
#
|
64
|
+
# image = Image.find(params[:id]) # 320x200 image
|
65
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
66
|
+
#
|
67
|
+
# sizing.fit(Vector2d(0, 100))
|
68
|
+
# # => Vector2d(160.0, 100.0)
|
69
|
+
#
|
70
|
+
# sizing.fit(Vector2d(500, 500))
|
71
|
+
# # => Vector2d(320.0, 200.0)
|
72
|
+
#
|
73
|
+
# sizing.fit(Vector2d(500, 500), crop: true)
|
74
|
+
# # => Vector2d(200.0, 200.0)
|
75
|
+
#
|
76
|
+
# sizing.fit(Vector2d(500, 500), upscale: true)
|
77
|
+
# # => Vector2d(500.0, 312.5)
|
78
|
+
#
|
79
|
+
def fit(fit_size, options={})
|
80
|
+
fit_size = parse_vector(fit_size)
|
81
|
+
require_dimensions!(fit_size) if options[:crop]
|
82
|
+
fit_size = size.fit(fit_size) unless options[:crop]
|
83
|
+
fit_size = size.contain(fit_size) unless options[:upscale]
|
84
|
+
fit_size
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def crop_gravity
|
90
|
+
if uncropped? && !record.crop_gravity?
|
91
|
+
size / 2
|
92
|
+
else
|
93
|
+
record.crop_gravity
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def crop_start
|
98
|
+
if uncropped?
|
99
|
+
Vector2d.new(0, 0)
|
100
|
+
else
|
101
|
+
record.crop_start
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def size
|
106
|
+
if uncropped?
|
107
|
+
record.real_size
|
108
|
+
else
|
109
|
+
record.size
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Clamps the rectangle defined by +start+ and +size+
|
114
|
+
# to fit inside 0, 0 and +max_size+. It is assumed
|
115
|
+
# that +size+ will always be smaller than +max_size+.
|
116
|
+
#
|
117
|
+
# Returns the start vector.
|
118
|
+
def clamp(start, size, max_size)
|
119
|
+
start += shift_vector(start)
|
120
|
+
start -= shift_vector(max_size - (start + size))
|
121
|
+
start
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_vector(v)
|
125
|
+
v.kind_of?(String) ? str_to_vector(v) : v
|
126
|
+
end
|
127
|
+
|
128
|
+
def record
|
129
|
+
@record
|
130
|
+
end
|
131
|
+
|
132
|
+
def require_dimensions!(v)
|
133
|
+
raise DynamicImage::Errors::InvalidSizeOptions unless v.x > 0 && v.y > 0
|
134
|
+
end
|
135
|
+
|
136
|
+
def shift_vector(vect)
|
137
|
+
vector(
|
138
|
+
vect.x < 0 ? vect.x.abs : 0,
|
139
|
+
vect.y < 0 ? vect.y.abs : 0
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def str_to_vector(str)
|
144
|
+
x, y = str.match(/(\d*)x(\d*)/)[1,2].map(&:to_i)
|
145
|
+
Vector2d.new(x, y)
|
146
|
+
end
|
147
|
+
|
148
|
+
def uncropped?
|
149
|
+
@uncropped
|
150
|
+
end
|
151
|
+
|
152
|
+
def vector(x, y)
|
153
|
+
Vector2d.new(x, y)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DynamicImage
|
4
|
+
# = DynamicImage Metadata
|
5
|
+
#
|
6
|
+
# Parses metadata from an image. Expects to receive image data as a
|
7
|
+
# binary string.
|
8
|
+
class Metadata
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the color space of the image as a string. The result will be one
|
14
|
+
# of the following: "rgb", "cmyk", "gray".
|
15
|
+
def colorspace
|
16
|
+
if valid?
|
17
|
+
case metadata[:colorspace]
|
18
|
+
when /rgb/i
|
19
|
+
"rgb"
|
20
|
+
when /cmyk/i
|
21
|
+
"cmyk"
|
22
|
+
when /gray/i
|
23
|
+
"gray"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the content type of the image.
|
29
|
+
def content_type
|
30
|
+
if valid?
|
31
|
+
"image/#{format.downcase}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the dimensions of the image as a vector.
|
36
|
+
def dimensions
|
37
|
+
if valid?
|
38
|
+
Vector2d.new(*metadata[:dimensions])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the width of the image.
|
43
|
+
def width
|
44
|
+
dimensions.try(:x)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the height of the image.
|
48
|
+
def height
|
49
|
+
dimensions.try(:y)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the format of the image.
|
53
|
+
def format
|
54
|
+
if valid?
|
55
|
+
metadata[:format]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true if the image is valid.
|
60
|
+
def valid?
|
61
|
+
@data && metadata != :invalid
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def metadata
|
67
|
+
@metadata ||= read_metadata
|
68
|
+
end
|
69
|
+
|
70
|
+
def read_metadata
|
71
|
+
image = MiniMagick::Image.read(@data)
|
72
|
+
image.auto_orient
|
73
|
+
metadata = {
|
74
|
+
colorspace: image[:colorspace],
|
75
|
+
dimensions: image[:dimensions],
|
76
|
+
format: image[:format]
|
77
|
+
}
|
78
|
+
image.destroy!
|
79
|
+
metadata
|
80
|
+
rescue MiniMagick::Invalid
|
81
|
+
:invalid
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DynamicImage
|
4
|
+
module Model
|
5
|
+
# = DynamicImage Model Dimensions
|
6
|
+
#
|
7
|
+
module Dimensions
|
8
|
+
|
9
|
+
# Returns the crop gravity.
|
10
|
+
#
|
11
|
+
# DynamicImage will try to keep the pixel represented by
|
12
|
+
# crop_gravity as close to the center as possible when cropping
|
13
|
+
# images.
|
14
|
+
#
|
15
|
+
# It is relative to 0,0 on the original image.
|
16
|
+
#
|
17
|
+
# Unless crop_gravity has been explicitely set, it defaults to
|
18
|
+
# the center of the cropped image.
|
19
|
+
def crop_gravity
|
20
|
+
if crop_gravity?
|
21
|
+
vector(crop_gravity_x, crop_gravity_y)
|
22
|
+
elsif cropped?
|
23
|
+
crop_start + (crop_size / 2)
|
24
|
+
elsif size?
|
25
|
+
size / 2
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns true if crop gravity has been explicitely set.
|
30
|
+
def crop_gravity?
|
31
|
+
crop_gravity_x.present? && crop_gravity_y.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the crop size, or nil if no cropping is applied.
|
35
|
+
def crop_size
|
36
|
+
if crop_size?
|
37
|
+
vector(crop_width, crop_height)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns true if crop size has been set.
|
42
|
+
def crop_size?
|
43
|
+
crop_width? && crop_height?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the crop start if set, or Vector2d(0, 0) if not.
|
47
|
+
def crop_start
|
48
|
+
if crop_start?
|
49
|
+
vector(crop_start_x, crop_start_y)
|
50
|
+
else
|
51
|
+
vector(0, 0)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns true if crop start has been set.
|
56
|
+
def crop_start?
|
57
|
+
crop_start_x.present? && crop_start_y.present?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if the image is cropped.
|
61
|
+
def cropped?
|
62
|
+
crop_size? && real_size? && crop_size != real_size
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the real size of the image, without any cropping applied.
|
66
|
+
def real_size
|
67
|
+
if real_size?
|
68
|
+
vector(real_width, real_height)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns true if the size has been set.
|
73
|
+
def real_size?
|
74
|
+
real_width? && real_height?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the cropped size if the image has been cropped. If not,
|
78
|
+
# it returns the actual size.
|
79
|
+
def size
|
80
|
+
crop_size || real_size
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns true if the image has size set.
|
84
|
+
def size?
|
85
|
+
size ? true : false
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def vector(x, y)
|
91
|
+
Vector2d.new(x, y)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DynamicImage
|
4
|
+
module Model
|
5
|
+
# = DynamicImage Model Validations
|
6
|
+
#
|
7
|
+
# Validates that all necessary attributes are valid. All of these are
|
8
|
+
# managed by +DynamicImage::Model+, so this is mostly for enforcing
|
9
|
+
# integrity.
|
10
|
+
module Validations
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
included do
|
13
|
+
validates_data_presence
|
14
|
+
|
15
|
+
validates :colorspace,
|
16
|
+
presence: true,
|
17
|
+
inclusion: { in: allowed_colorspaces }
|
18
|
+
|
19
|
+
validates :content_type,
|
20
|
+
presence: true,
|
21
|
+
inclusion: { in: allowed_content_types }
|
22
|
+
|
23
|
+
validates :content_length,
|
24
|
+
presence: true,
|
25
|
+
numericality: { greater_than: 0, only_integer: true }
|
26
|
+
|
27
|
+
validates :filename,
|
28
|
+
presence: true,
|
29
|
+
length: { maximum: 255 }
|
30
|
+
|
31
|
+
validates :real_width, :real_height,
|
32
|
+
numericality: { greater_than: 0, only_integer: true }
|
33
|
+
|
34
|
+
validates :real_width, :real_height,
|
35
|
+
numericality: { greater_than: 0, only_integer: true }
|
36
|
+
|
37
|
+
validates :crop_width, :crop_height,
|
38
|
+
:crop_gravity_x, :crop_gravity_y,
|
39
|
+
numericality: { greater_than: 0, only_integer: true },
|
40
|
+
allow_nil: true
|
41
|
+
|
42
|
+
validates :real_width, :real_height,
|
43
|
+
presence: true
|
44
|
+
|
45
|
+
validates :crop_width, presence: true, if: :crop_height?
|
46
|
+
validates :crop_height, presence: true, if: :crop_width?
|
47
|
+
|
48
|
+
validates :crop_start_x, presence: true, if: :crop_start_y?
|
49
|
+
validates :crop_start_y, presence: true, if: :crop_start_x?
|
50
|
+
|
51
|
+
validates :crop_gravity_x, presence: true, if: :crop_gravity_y?
|
52
|
+
validates :crop_gravity_y, presence: true, if: :crop_gravity_x?
|
53
|
+
|
54
|
+
validate :validate_crop_bounds, if: :cropped?
|
55
|
+
validate :validate_image, if: :data_changed?
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
def allowed_colorspaces
|
60
|
+
%w{
|
61
|
+
rgb
|
62
|
+
cmyk
|
63
|
+
gray
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def allowed_content_types
|
68
|
+
%w{
|
69
|
+
image/gif
|
70
|
+
image/jpeg
|
71
|
+
image/pjpeg
|
72
|
+
image/png
|
73
|
+
image/tiff
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def validate_crop_bounds
|
81
|
+
required_size = crop_start + crop_size
|
82
|
+
if required_size.x > real_size.x || required_size.y > real_size.y
|
83
|
+
self.errors.add(:crop_size, "is out of bounds")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_image
|
88
|
+
unless valid_image?
|
89
|
+
self.errors.add(:data, :invalid)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'dynamic_image/model/dimensions'
|
4
|
+
require 'dynamic_image/model/validations'
|
5
|
+
|
6
|
+
module DynamicImage
|
7
|
+
# = DynamicImage Model
|
8
|
+
#
|
9
|
+
# ActiveModel extension for the model holding image data. It assumes your
|
10
|
+
# database table has at least the following attributes:
|
11
|
+
#
|
12
|
+
# create_table :images do |t|
|
13
|
+
# t.string :content_hash
|
14
|
+
# t.string :content_type
|
15
|
+
# t.integer :content_length
|
16
|
+
# t.string :filename
|
17
|
+
# t.string :colorspace
|
18
|
+
# t.integer :real_width, :real_height
|
19
|
+
# t.integer :crop_width, :crop_height
|
20
|
+
# t.integer :crop_start_x, :crop_start_y
|
21
|
+
# t.integer :crop_gravity_x, :crop_gravity_y
|
22
|
+
# t.timestamps
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# To use it, simply include it in your model:
|
26
|
+
#
|
27
|
+
# class Image < ActiveRecord::Base
|
28
|
+
# include DynamicImage::Model
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# == Usage
|
32
|
+
#
|
33
|
+
# To save an image, simply assign to the +file+ attribute.
|
34
|
+
#
|
35
|
+
# image = Image.create(file: params.permit(:file))
|
36
|
+
#
|
37
|
+
# This will automatically parse and validate the image when your record is
|
38
|
+
# saved.
|
39
|
+
#
|
40
|
+
# To read back the image data, access the +data+ attribute. This will lazily
|
41
|
+
# load the data from the store.
|
42
|
+
#
|
43
|
+
# data = image.data
|
44
|
+
#
|
45
|
+
# == Cropping
|
46
|
+
#
|
47
|
+
# Images can be pre-cropped by setting +crop_width+, +crop_height+,
|
48
|
+
# +crop_start_x+ and +crop_start_y+. The crop dimensions cannot exceed the
|
49
|
+
# image size.
|
50
|
+
#
|
51
|
+
# image.update(
|
52
|
+
# crop_start_x: 15, crop_start_y: 20,
|
53
|
+
# crop_width: 300, crop_height: 200
|
54
|
+
# )
|
55
|
+
# image.size # => Vector2d(300, 200)
|
56
|
+
#
|
57
|
+
# By default, images will be cropped from the center. You can control this
|
58
|
+
# by setting +crop_gravity_x+ and +crop_gravity_y+. DynamicImage will make
|
59
|
+
# sure the pixel referred to by these coordinates are present in the cropped
|
60
|
+
# image, and as close to the center as possible without zooming in.
|
61
|
+
module Model
|
62
|
+
extend ActiveSupport::Concern
|
63
|
+
include Dis::Model
|
64
|
+
include DynamicImage::Model::Dimensions
|
65
|
+
include DynamicImage::Model::Validations
|
66
|
+
|
67
|
+
included do
|
68
|
+
before_validation :read_image_metadata, if: :data_changed?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns true if the image is in the CMYK colorspace
|
72
|
+
def cmyk?
|
73
|
+
colorspace == "cmyk"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns true if the image is in the grayscale colorspace
|
77
|
+
def gray?
|
78
|
+
colorspace == "gray"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns true if the image is in the RGB colorspace
|
82
|
+
def rgb?
|
83
|
+
colorspace == "rgb"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Finds a web safe content type. GIF, JPEG and PNG images are allowed,
|
87
|
+
# any other formats should be converted to JPEG.
|
88
|
+
def safe_content_type
|
89
|
+
if safe_content_types.include?(content_type)
|
90
|
+
content_type
|
91
|
+
else
|
92
|
+
'image/jpeg'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Includes a timestamp fingerprint in the URL param, so
|
97
|
+
# that rendered images can be cached indefinitely.
|
98
|
+
def to_param
|
99
|
+
[id, updated_at.utc.to_s(cache_timestamp_format)].join('-')
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def read_image_metadata
|
105
|
+
metadata = DynamicImage::Metadata.new(self.data)
|
106
|
+
if metadata.valid?
|
107
|
+
self.colorspace = metadata.colorspace
|
108
|
+
self.real_width = metadata.width
|
109
|
+
self.real_height = metadata.height
|
110
|
+
self.content_type = metadata.content_type
|
111
|
+
@valid_image = true
|
112
|
+
else
|
113
|
+
@valid_image = false
|
114
|
+
end
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
def valid_image?
|
119
|
+
@valid_image ? true : false
|
120
|
+
end
|
121
|
+
|
122
|
+
def safe_content_types
|
123
|
+
%w{
|
124
|
+
image/png
|
125
|
+
image/gif
|
126
|
+
image/jpeg
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DynamicImage
|
4
|
+
# = DynamicImage Processed Image
|
5
|
+
#
|
6
|
+
# Handles all processing of images. Takes an instance of
|
7
|
+
# +DynamicImage::Model+ as argument.
|
8
|
+
class ProcessedImage
|
9
|
+
def initialize(record, options={})
|
10
|
+
@record = record
|
11
|
+
@uncropped = options[:uncropped] ? true : false
|
12
|
+
@format = options[:format].to_s.upcase if options[:format]
|
13
|
+
@format = "JPEG" if @format == "JPG"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the content type of the processed image.
|
17
|
+
#
|
18
|
+
# ==== Example
|
19
|
+
#
|
20
|
+
# image = Image.find(params[:id])
|
21
|
+
# DynamicImage::ProcessedImage.new(image).content_type
|
22
|
+
# # => 'image/png'
|
23
|
+
# DynamicImage::ProcessedImage.new(image, :jpeg).content_type
|
24
|
+
# # => 'image/jpeg'
|
25
|
+
def content_type
|
26
|
+
"image/#{format}".downcase
|
27
|
+
end
|
28
|
+
|
29
|
+
# Crops and resizes the image. Normalization is performed as well.
|
30
|
+
#
|
31
|
+
# ==== Example
|
32
|
+
#
|
33
|
+
# processed = DynamicImage::ProcessedImage.new(image)
|
34
|
+
# image_data = processed.cropped_and_resized(Vector2d.new(200, 200))
|
35
|
+
#
|
36
|
+
# Returns a binary string.
|
37
|
+
def cropped_and_resized(size)
|
38
|
+
normalized do |image|
|
39
|
+
image.crop image_sizing.crop_geometry_string(size)
|
40
|
+
image.resize size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Normalizes the image.
|
45
|
+
#
|
46
|
+
# * Applies EXIF rotation
|
47
|
+
# * CMYK images are converted to sRGB
|
48
|
+
# * Strips metadata
|
49
|
+
# * Performs format conversion if the requested format is different
|
50
|
+
#
|
51
|
+
# ==== Example
|
52
|
+
#
|
53
|
+
# processed = DynamicImage::ProcessedImage.new(image, :jpeg)
|
54
|
+
# jpg_data = processed.normalized
|
55
|
+
#
|
56
|
+
# Returns a binary string.
|
57
|
+
def normalized(&block)
|
58
|
+
require_valid_image!
|
59
|
+
process_data do |image|
|
60
|
+
image.combine_options do |combined|
|
61
|
+
image.auto_orient
|
62
|
+
image.colorspace('sRGB') if needs_colorspace_conversion?
|
63
|
+
yield(combined) if block_given?
|
64
|
+
image.strip
|
65
|
+
end
|
66
|
+
image.format(format) if needs_format_conversion?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def format
|
73
|
+
@format || record_format
|
74
|
+
end
|
75
|
+
|
76
|
+
def image_sizing
|
77
|
+
@image_sizing ||= DynamicImage::ImageSizing.new(record, uncropped: @uncropped)
|
78
|
+
end
|
79
|
+
|
80
|
+
def needs_colorspace_conversion?
|
81
|
+
record.cmyk?
|
82
|
+
end
|
83
|
+
|
84
|
+
def needs_format_conversion?
|
85
|
+
format != record_format
|
86
|
+
end
|
87
|
+
|
88
|
+
def process_data(&block)
|
89
|
+
image = MiniMagick::Image.read(record.data)
|
90
|
+
yield(image)
|
91
|
+
result = image.to_blob
|
92
|
+
image.destroy!
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def record
|
97
|
+
@record
|
98
|
+
end
|
99
|
+
|
100
|
+
def record_format
|
101
|
+
case record.content_type
|
102
|
+
when 'image/png'
|
103
|
+
'PNG'
|
104
|
+
when 'image/gif'
|
105
|
+
'GIF'
|
106
|
+
when 'image/jpeg', 'image/pjpeg'
|
107
|
+
'JPEG'
|
108
|
+
when 'image/tiff'
|
109
|
+
'TIFF'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def require_valid_image!
|
114
|
+
unless record.valid?
|
115
|
+
raise DynamicImage::Errors::InvalidImage
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DynamicImage
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
initializer "dynamic_image" do
|
6
|
+
ActionDispatch::Routing::Mapper.send :include, DynamicImage::Routing
|
7
|
+
|
8
|
+
config.after_initialize do |app|
|
9
|
+
secret = app.key_generator.generate_key('dynamic_image')
|
10
|
+
DynamicImage.digest_verifier = DynamicImage::DigestVerifier.new(secret)
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
send :include, DynamicImage::BelongsTo
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|