dynamic_image 3.0.9 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +393 -67
- data/app/models/dynamic_image/variant.rb +10 -0
- data/lib/dynamic_image/backfill.rb +85 -0
- data/lib/dynamic_image/belongs_to.rb +22 -0
- data/lib/dynamic_image/breakpoints.rb +93 -0
- data/lib/dynamic_image/controller.rb +51 -7
- data/lib/dynamic_image/digest_verifier.rb +20 -9
- data/lib/dynamic_image/engine.rb +3 -0
- data/lib/dynamic_image/errors.rb +30 -0
- data/lib/dynamic_image/format.rb +149 -8
- data/lib/dynamic_image/format_negotiator.rb +84 -0
- data/lib/dynamic_image/helper/formats.rb +57 -0
- data/lib/dynamic_image/helper/pictures.rb +124 -0
- data/lib/dynamic_image/helper.rb +100 -60
- data/lib/dynamic_image/image_processor/colors.rb +3 -7
- data/lib/dynamic_image/image_processor/frames.rb +10 -1
- data/lib/dynamic_image/image_processor/transform.rb +17 -2
- data/lib/dynamic_image/image_processor.rb +33 -6
- data/lib/dynamic_image/image_reader.rb +19 -1
- data/lib/dynamic_image/image_sizing.rb +51 -19
- data/lib/dynamic_image/metadata.rb +56 -5
- data/lib/dynamic_image/model/dimensions.rb +32 -13
- data/lib/dynamic_image/model/transformations.rb +47 -11
- data/lib/dynamic_image/model/validations.rb +15 -8
- data/lib/dynamic_image/model/variants.rb +6 -3
- data/lib/dynamic_image/model.rb +58 -34
- data/lib/dynamic_image/picture/format_policy.rb +72 -0
- data/lib/dynamic_image/picture.rb +215 -0
- data/lib/dynamic_image/processed_image.rb +48 -14
- data/lib/dynamic_image/ratio.rb +43 -0
- data/lib/dynamic_image/routing.rb +14 -3
- data/lib/dynamic_image/schema.rb +47 -0
- data/lib/dynamic_image/version.rb +1 -1
- data/lib/dynamic_image.rb +61 -1
- data/lib/rails/generators/dynamic_image/resource/USAGE +17 -0
- data/lib/rails/generators/dynamic_image/resource/resource_generator.rb +57 -21
- data/lib/rails/generators/dynamic_image/resource/templates/create_table_migration.rb.tt +32 -0
- data/lib/rails/generators/dynamic_image/upgrade/USAGE +17 -0
- data/lib/rails/generators/dynamic_image/upgrade/templates/upgrade_migration.rb.tt +10 -0
- data/lib/rails/generators/dynamic_image/upgrade/upgrade_generator.rb +111 -0
- data/lib/tasks/dynamic_image.rake +25 -0
- metadata +23 -12
- data/lib/dynamic_image/jobs/create_variant.rb +0 -22
- data/lib/dynamic_image/jobs.rb +0 -3
|
@@ -3,26 +3,37 @@
|
|
|
3
3
|
module DynamicImage
|
|
4
4
|
# = DynamicImage Image Sizing
|
|
5
5
|
#
|
|
6
|
-
# Calculates cropping and fitting for image sizes.
|
|
6
|
+
# Calculates cropping and fitting for image sizes. The helpers use it to work out the dimensions of a rendered
|
|
7
|
+
# image, and it can be used on its own when you need the size without rendering anything: reserving space in a
|
|
8
|
+
# layout, filling in <tt>og:image:width</tt>, or laying out a PDF.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
|
12
|
+
# sizing.fit("400x400") # => Vector2d(400.0, 250.0)
|
|
7
13
|
class ImageSizing
|
|
14
|
+
# @param record [DynamicImage::Model] the image
|
|
15
|
+
# @param options [Hash]
|
|
16
|
+
# @option options [Boolean] :uncropped Ignore any crop stored on the record and size against the original image
|
|
8
17
|
def initialize(record, options = {})
|
|
9
18
|
@record = record
|
|
10
19
|
@uncropped = options[:uncropped] ? true : false
|
|
11
20
|
end
|
|
12
21
|
|
|
13
|
-
# Calculates crop geometry. The given vector is scaled
|
|
14
|
-
#
|
|
15
|
-
#
|
|
22
|
+
# Calculates crop geometry. The given vector is scaled to match the image size, since cropping happens before
|
|
23
|
+
# resizing.
|
|
24
|
+
#
|
|
25
|
+
# The crop is positioned to keep the record's crop gravity as close to the center as possible, clamped to the
|
|
26
|
+
# bounds of the image.
|
|
16
27
|
#
|
|
17
|
-
#
|
|
28
|
+
# @param ratio_vector [Vector2d] the aspect ratio to crop to
|
|
29
|
+
# @return [Array(Vector2d, Vector2d)] the crop size and crop start
|
|
18
30
|
#
|
|
31
|
+
# @example
|
|
19
32
|
# image = Image.find(params[:id]) # 320x200 image
|
|
20
33
|
# sizing = DynamicImage::ImageSizing.new(image)
|
|
21
34
|
#
|
|
22
35
|
# sizing.crop_geometry(Vector2d(100, 100))
|
|
23
36
|
# # => [Vector2d(200, 200), Vector2d(60, 0)]
|
|
24
|
-
#
|
|
25
|
-
# Returns a tuple with crop size and crop start vectors.
|
|
26
37
|
def crop_geometry(ratio_vector)
|
|
27
38
|
# Maximize the crop area to fit the image size
|
|
28
39
|
crop_size = ratio_vector.fit(size).round
|
|
@@ -36,18 +47,41 @@ module DynamicImage
|
|
|
36
47
|
[crop_size, (start + crop_start)]
|
|
37
48
|
end
|
|
38
49
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
50
|
+
# Returns the widest the image can be rendered at, in pixels.
|
|
51
|
+
#
|
|
52
|
+
# Without a ratio this is the image's own width. With one it is the width of the largest crop matching that
|
|
53
|
+
# ratio.
|
|
54
|
+
#
|
|
55
|
+
# @param ratio [Numeric, Vector2d, String, nil] the aspect ratio, in any form {DynamicImage::Ratio} understands
|
|
56
|
+
# @return [Integer]
|
|
41
57
|
#
|
|
42
|
-
#
|
|
58
|
+
# @example
|
|
59
|
+
# image = Image.find(params[:id]) # 320x200 image
|
|
60
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
|
43
61
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
62
|
+
# sizing.available_width # => 320
|
|
63
|
+
# sizing.available_width(16.0 / 9) # => 320
|
|
64
|
+
# sizing.available_width(9.0 / 16) # => 113
|
|
65
|
+
def available_width(ratio = nil)
|
|
66
|
+
ratio = DynamicImage::Ratio.parse(ratio)
|
|
67
|
+
return size.x.floor unless ratio
|
|
68
|
+
|
|
69
|
+
crop_geometry(vector(ratio, 1)).first.x.floor
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Adjusts +fit_size+ to fit the image dimensions. Any dimension set to zero will be ignored.
|
|
48
73
|
#
|
|
49
|
-
#
|
|
74
|
+
# @param fit_size [Vector2d, String] the size to fit within, either a vector or a <tt>"{width}x{height}"</tt>
|
|
75
|
+
# string. Either dimension may be omitted for a fixed width or height.
|
|
76
|
+
# @param options [Hash]
|
|
77
|
+
# @option options [Boolean] :crop Don't keep aspect ratio. This will allow the image to be cropped to the
|
|
78
|
+
# requested size.
|
|
79
|
+
# @option options [Boolean] :upscale Don't limit to the size of the image. Images smaller than the given size will
|
|
80
|
+
# be scaled up.
|
|
81
|
+
# @return [Vector2d] the resulting size
|
|
82
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt> is given and either dimension is zero
|
|
50
83
|
#
|
|
84
|
+
# @example
|
|
51
85
|
# image = Image.find(params[:id]) # 320x200 image
|
|
52
86
|
# sizing = DynamicImage::ImageSizing.new(image)
|
|
53
87
|
#
|
|
@@ -62,7 +96,6 @@ module DynamicImage
|
|
|
62
96
|
#
|
|
63
97
|
# sizing.fit(Vector2d(500, 500), upscale: true)
|
|
64
98
|
# # => Vector2d(500.0, 312.5)
|
|
65
|
-
#
|
|
66
99
|
def fit(fit_size, options = {})
|
|
67
100
|
fit_size = parse_vector(fit_size)
|
|
68
101
|
require_dimensions!(fit_size) if options[:crop]
|
|
@@ -97,9 +130,8 @@ module DynamicImage
|
|
|
97
130
|
end
|
|
98
131
|
end
|
|
99
132
|
|
|
100
|
-
# Clamps the rectangle defined by +start+ and +size+
|
|
101
|
-
#
|
|
102
|
-
# that +size+ will always be smaller than +max_size+.
|
|
133
|
+
# Clamps the rectangle defined by +start+ and +size+ to fit inside 0, 0 and +max_size+. It is assumed that +size+
|
|
134
|
+
# will always be smaller than +max_size+.
|
|
103
135
|
#
|
|
104
136
|
# Returns the start vector.
|
|
105
137
|
def clamp(start, size, max_size)
|
|
@@ -4,13 +4,24 @@ module DynamicImage
|
|
|
4
4
|
# = DynamicImage Metadata
|
|
5
5
|
#
|
|
6
6
|
# Parses metadata from an image. Accepts a Pathname, IO, or binary string.
|
|
7
|
+
#
|
|
8
|
+
# Every reader returns nil for data that isn't a readable image.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# metadata = DynamicImage::Metadata.new(Pathname.new("image.jpg"))
|
|
12
|
+
# metadata.valid? # => true
|
|
13
|
+
# metadata.content_type # => "image/jpeg"
|
|
14
|
+
# metadata.dimensions # => Vector2d(320, 200)
|
|
7
15
|
class Metadata
|
|
16
|
+
# @param data [Pathname, IO, String] the image, as a path, an open file or a binary string
|
|
8
17
|
def initialize(data)
|
|
9
18
|
@data = data
|
|
10
19
|
end
|
|
11
20
|
|
|
12
|
-
# Returns the color space of the image as a string. The result will be one
|
|
13
|
-
#
|
|
21
|
+
# Returns the color space of the image as a string. The result will be one of the following: "rgb", "cmyk",
|
|
22
|
+
# "gray".
|
|
23
|
+
#
|
|
24
|
+
# @return [String, nil]
|
|
14
25
|
def colorspace
|
|
15
26
|
return unless valid?
|
|
16
27
|
|
|
@@ -25,30 +36,61 @@ module DynamicImage
|
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# Returns the content type of the image.
|
|
39
|
+
#
|
|
40
|
+
# @return [String, nil]
|
|
28
41
|
def content_type
|
|
29
42
|
reader.format.content_type if valid?
|
|
30
43
|
end
|
|
31
44
|
|
|
45
|
+
# Returns the name of the detected format.
|
|
46
|
+
#
|
|
47
|
+
# @return [String, nil] the format name, such as "JPEG"
|
|
32
48
|
def format
|
|
33
49
|
reader.format.name if valid?
|
|
34
50
|
end
|
|
35
51
|
|
|
36
|
-
# Returns the dimensions of the image as a vector.
|
|
52
|
+
# Returns the dimensions of the image as a vector. EXIF rotation is applied, so these are the dimensions the
|
|
53
|
+
# image has once normalized.
|
|
54
|
+
#
|
|
55
|
+
# @return [Vector2d, nil]
|
|
37
56
|
def dimensions
|
|
38
57
|
Vector2d.new(metadata[:width], metadata[:height]) if valid?
|
|
39
58
|
end
|
|
40
59
|
|
|
41
60
|
# Returns the width of the image.
|
|
61
|
+
#
|
|
62
|
+
# @return [Integer, nil] the width in pixels
|
|
42
63
|
def width
|
|
43
64
|
metadata[:width] if valid?
|
|
44
65
|
end
|
|
45
66
|
|
|
46
67
|
# Returns the height of the image.
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer, nil] the height in pixels
|
|
47
70
|
def height
|
|
48
71
|
metadata[:height] if valid?
|
|
49
72
|
end
|
|
50
73
|
|
|
51
|
-
# Returns
|
|
74
|
+
# Returns the number of frames. Animated formats can have more than one; everything else has a single frame.
|
|
75
|
+
#
|
|
76
|
+
# @return [Integer, nil] the number of frames
|
|
77
|
+
def frame_count
|
|
78
|
+
metadata[:frame_count] if valid?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns true if the image has an alpha channel.
|
|
82
|
+
#
|
|
83
|
+
# This is the presence of the channel, not of actual transparency: an image can carry a fully opaque alpha
|
|
84
|
+
# channel. Reading it costs nothing, where scanning the channel for transparency would mean decoding every pixel.
|
|
85
|
+
#
|
|
86
|
+
# @return [Boolean, nil] true if the image has an alpha channel
|
|
87
|
+
def alpha?
|
|
88
|
+
metadata[:alpha] if valid?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns true if the data is a readable image in a supported format.
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean]
|
|
52
94
|
def valid?
|
|
53
95
|
@data && reader.valid_header? && metadata != :invalid
|
|
54
96
|
end
|
|
@@ -67,11 +109,20 @@ module DynamicImage
|
|
|
67
109
|
image = reader.read
|
|
68
110
|
width, height = dimensions_from(image)
|
|
69
111
|
width, height = height, width if rotated?(image)
|
|
70
|
-
{ width:, height:,
|
|
112
|
+
{ width:, height:,
|
|
113
|
+
colorspace: image.get("interpretation"),
|
|
114
|
+
frame_count: frame_count_from(image),
|
|
115
|
+
alpha: image.has_alpha? }
|
|
71
116
|
rescue Vips::Error
|
|
72
117
|
:invalid
|
|
73
118
|
end
|
|
74
119
|
|
|
120
|
+
def frame_count_from(image)
|
|
121
|
+
return 1 unless image.get_fields.include?("n-pages")
|
|
122
|
+
|
|
123
|
+
image.get("n-pages")
|
|
124
|
+
end
|
|
125
|
+
|
|
75
126
|
def dimensions_from(image)
|
|
76
127
|
width = image.get("width")
|
|
77
128
|
height = if image.get_fields.include?("page-height")
|
|
@@ -4,17 +4,17 @@ module DynamicImage
|
|
|
4
4
|
module Model
|
|
5
5
|
# = DynamicImage Model Dimensions
|
|
6
6
|
#
|
|
7
|
+
# Vector accessors over the +real_*+ and +crop_*+ columns.
|
|
8
|
+
#
|
|
9
|
+
# +real_size+ is the size of the stored image. +size+ is the size after cropping, or +real_size+ when no crop
|
|
10
|
+
# is set.
|
|
7
11
|
module Dimensions
|
|
8
|
-
# Returns the crop gravity.
|
|
9
|
-
#
|
|
10
|
-
# DynamicImage will try to keep the pixel represented by
|
|
11
|
-
# crop_gravity as close to the center as possible when cropping
|
|
12
|
-
# images.
|
|
12
|
+
# Returns the crop gravity, the focal point kept as close to the center as possible when cropping.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# The coordinates are relative to 0,0 on the original image. Unless set explicitly, the gravity is the center
|
|
15
|
+
# of the cropped image.
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
# the center of the cropped image.
|
|
17
|
+
# @return [Vector2d, nil]
|
|
18
18
|
def crop_gravity
|
|
19
19
|
if crop_gravity?
|
|
20
20
|
vector(crop_gravity_x, crop_gravity_y)
|
|
@@ -25,22 +25,30 @@ module DynamicImage
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Returns true if crop gravity has been
|
|
28
|
+
# Returns true if the crop gravity has been set explicitly.
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
29
31
|
def crop_gravity?
|
|
30
32
|
crop_gravity_x.present? && crop_gravity_y.present?
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# Returns the crop size, or nil if no cropping is applied.
|
|
36
|
+
#
|
|
37
|
+
# @return [Vector2d, nil]
|
|
34
38
|
def crop_size
|
|
35
39
|
vector(crop_width, crop_height) if crop_size?
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# Returns true if crop size has been set.
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
39
45
|
def crop_size?
|
|
40
46
|
crop_width? && crop_height?
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
# Returns the crop start if set, or Vector2d(0, 0) if not.
|
|
50
|
+
#
|
|
51
|
+
# @return [Vector2d] the top left corner of the crop
|
|
44
52
|
def crop_start
|
|
45
53
|
if crop_start?
|
|
46
54
|
vector(crop_start_x, crop_start_y)
|
|
@@ -50,32 +58,43 @@ module DynamicImage
|
|
|
50
58
|
end
|
|
51
59
|
|
|
52
60
|
# Returns true if crop start has been set.
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean]
|
|
53
63
|
def crop_start?
|
|
54
64
|
crop_start_x.present? && crop_start_y.present?
|
|
55
65
|
end
|
|
56
66
|
|
|
57
67
|
# Returns true if the image is cropped.
|
|
68
|
+
#
|
|
69
|
+
# @return [Boolean]
|
|
58
70
|
def cropped?
|
|
59
71
|
crop_size? && real_size? && crop_size != real_size
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
# Returns the real size of the image, without any cropping applied.
|
|
75
|
+
#
|
|
76
|
+
# @return [Vector2d, nil] the size of the stored image
|
|
63
77
|
def real_size
|
|
64
78
|
vector(real_width, real_height) if real_size?
|
|
65
79
|
end
|
|
66
80
|
|
|
67
|
-
# Returns true if the size has been set.
|
|
81
|
+
# Returns true if the real size has been set.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
68
84
|
def real_size?
|
|
69
85
|
real_width? && real_height?
|
|
70
86
|
end
|
|
71
87
|
|
|
72
|
-
# Returns the cropped size if the image
|
|
73
|
-
#
|
|
88
|
+
# Returns the cropped size, or the real size if the image isn't cropped.
|
|
89
|
+
#
|
|
90
|
+
# @return [Vector2d, nil] the visible size of the image
|
|
74
91
|
def size
|
|
75
92
|
crop_size || real_size
|
|
76
93
|
end
|
|
77
94
|
|
|
78
|
-
# Returns true if the image has size
|
|
95
|
+
# Returns true if the image has a size.
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean]
|
|
79
98
|
def size?
|
|
80
99
|
size ? true : false
|
|
81
100
|
end
|
|
@@ -4,20 +4,32 @@ module DynamicImage
|
|
|
4
4
|
module Model
|
|
5
5
|
# = DynamicImage Model Transformations
|
|
6
6
|
#
|
|
7
|
+
# Transformations that rewrite the stored file. Per-request processing never touches it.
|
|
8
|
+
#
|
|
9
|
+
# Both methods replace the data and update the stored dimensions, adjusting the crop to match. Neither saves
|
|
10
|
+
# the record.
|
|
7
11
|
module Transformations
|
|
8
|
-
# Resizes the image
|
|
12
|
+
# Resizes the image, replacing the stored file with a smaller one. The crop is scaled along with it.
|
|
13
|
+
#
|
|
14
|
+
# @param max_size [Vector2d] the size to scale down to
|
|
15
|
+
# @return [self]
|
|
9
16
|
def resize(max_size)
|
|
10
17
|
transform_image do |image|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
self[attr] = self[attr] * scale if self[attr]
|
|
15
|
-
end
|
|
16
|
-
image.resize(new_size)
|
|
18
|
+
resized = image.resize(real_size.constrain_both(max_size))
|
|
19
|
+
scale_crop(resized.size)
|
|
20
|
+
resized
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
# Rotates the image
|
|
24
|
+
# Rotates the image, taking the crop and crop gravity with it.
|
|
25
|
+
#
|
|
26
|
+
# @param degrees [Integer] the angle, which must be a multiple of 90. Rotating by 0 is a no-op.
|
|
27
|
+
# @return [self]
|
|
28
|
+
# @raise [DynamicImage::Errors::InvalidTransformation] if the angle isn't a multiple of 90
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# image.rotate(90)
|
|
32
|
+
# image.save
|
|
21
33
|
def rotate(degrees = 90)
|
|
22
34
|
degrees = degrees.to_i % 360
|
|
23
35
|
|
|
@@ -36,9 +48,33 @@ module DynamicImage
|
|
|
36
48
|
|
|
37
49
|
private
|
|
38
50
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
def scale_crop(new_size)
|
|
52
|
+
scale = new_size.to_f_vector / real_size
|
|
53
|
+
|
|
54
|
+
scale_crop_start(scale, new_size)
|
|
55
|
+
scale_crop_size(scale, new_size)
|
|
56
|
+
scale_crop_gravity(scale, new_size)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def scale_crop_start(scale, new_size)
|
|
60
|
+
return unless crop_start?
|
|
61
|
+
|
|
62
|
+
self.crop_start_x, self.crop_start_y =
|
|
63
|
+
(crop_start * scale).floor.clamp(0, new_size - 1).to_a
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scale_crop_size(scale, new_size)
|
|
67
|
+
return unless crop_size?
|
|
68
|
+
|
|
69
|
+
self.crop_width, self.crop_height =
|
|
70
|
+
(crop_size * scale).round.clamp(1, new_size - crop_start).to_a
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def scale_crop_gravity(scale, new_size)
|
|
74
|
+
return unless crop_gravity?
|
|
75
|
+
|
|
76
|
+
self.crop_gravity_x, self.crop_gravity_y =
|
|
77
|
+
(crop_gravity * scale).round.clamp(1, new_size).to_a
|
|
42
78
|
end
|
|
43
79
|
|
|
44
80
|
def rotate_dimensions(width, height, degrees)
|
|
@@ -4,9 +4,10 @@ module DynamicImage
|
|
|
4
4
|
module Model
|
|
5
5
|
# = DynamicImage Model Validations
|
|
6
6
|
#
|
|
7
|
-
# Validates
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Validates the image attributes. +DynamicImage::Model+ manages all of them, so these mostly enforce integrity.
|
|
8
|
+
#
|
|
9
|
+
# The two that can fail on ordinary input are the image itself, which is rejected if the data isn't readable in
|
|
10
|
+
# a supported format, and the crop, which has to fit within the image.
|
|
10
11
|
module Validations
|
|
11
12
|
extend ActiveSupport::Concern
|
|
12
13
|
|
|
@@ -30,9 +31,7 @@ module DynamicImage
|
|
|
30
31
|
length: { maximum: 255 }
|
|
31
32
|
|
|
32
33
|
validates :real_width, :real_height,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
validates :real_width, :real_height,
|
|
34
|
+
presence: true,
|
|
36
35
|
numericality: { greater_than: 0, only_integer: true }
|
|
37
36
|
|
|
38
37
|
validates :crop_width, :crop_height,
|
|
@@ -40,8 +39,10 @@ module DynamicImage
|
|
|
40
39
|
numericality: { greater_than: 0, only_integer: true },
|
|
41
40
|
allow_nil: true
|
|
42
41
|
|
|
43
|
-
validates :
|
|
44
|
-
|
|
42
|
+
validates :crop_start_x, :crop_start_y,
|
|
43
|
+
numericality: { greater_than_or_equal_to: 0,
|
|
44
|
+
only_integer: true },
|
|
45
|
+
allow_nil: true
|
|
45
46
|
|
|
46
47
|
validates :crop_width, presence: true, if: :crop_height?
|
|
47
48
|
validates :crop_height, presence: true, if: :crop_width?
|
|
@@ -57,10 +58,16 @@ module DynamicImage
|
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
module ClassMethods
|
|
61
|
+
# Colorspaces an image is allowed to be in.
|
|
62
|
+
#
|
|
63
|
+
# @return [Array<String>] the colorspace names
|
|
60
64
|
def allowed_colorspaces
|
|
61
65
|
%w[rgb cmyk gray]
|
|
62
66
|
end
|
|
63
67
|
|
|
68
|
+
# Content types an image is allowed to have, taken from the registered formats.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<String>]
|
|
64
71
|
def allowed_content_types
|
|
65
72
|
DynamicImage::Format.content_types
|
|
66
73
|
end
|
|
@@ -4,9 +4,12 @@ module DynamicImage
|
|
|
4
4
|
module Model
|
|
5
5
|
# = DynamicImage Model Variants
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Associates the image with its cached renderings.
|
|
8
|
+
#
|
|
9
|
+
# Each processed size is stored as a {DynamicImage::Variant}, so cropping and resizing is done once. Variants
|
|
10
|
+
# are destroyed with the image, and cleared whenever its data changes.
|
|
11
|
+
#
|
|
12
|
+
# @see DynamicImage::ProcessedImage
|
|
10
13
|
module Variants
|
|
11
14
|
extend ActiveSupport::Concern
|
|
12
15
|
|
data/lib/dynamic_image/model.rb
CHANGED
|
@@ -8,8 +8,8 @@ require "dynamic_image/model/variants"
|
|
|
8
8
|
module DynamicImage
|
|
9
9
|
# = DynamicImage Model
|
|
10
10
|
#
|
|
11
|
-
# ActiveModel extension for the model holding image data.
|
|
12
|
-
#
|
|
11
|
+
# ActiveModel extension for the model holding image data. The table needs at least the attributes in
|
|
12
|
+
# {DynamicImage::Schema::ATTRIBUTES}:
|
|
13
13
|
#
|
|
14
14
|
# create_table :images do |t|
|
|
15
15
|
# t.string :content_hash
|
|
@@ -24,7 +24,7 @@ module DynamicImage
|
|
|
24
24
|
# t.timestamps
|
|
25
25
|
# end
|
|
26
26
|
#
|
|
27
|
-
#
|
|
27
|
+
# Include it in your model:
|
|
28
28
|
#
|
|
29
29
|
# class Image < ActiveRecord::Base
|
|
30
30
|
# include DynamicImage::Model
|
|
@@ -32,23 +32,18 @@ module DynamicImage
|
|
|
32
32
|
#
|
|
33
33
|
# == Usage
|
|
34
34
|
#
|
|
35
|
-
# To save an image,
|
|
35
|
+
# To save an image, assign to the +file+ attribute. The image is parsed and validated when the record is saved.
|
|
36
36
|
#
|
|
37
37
|
# image = Image.create(file: params.permit(:file))
|
|
38
38
|
#
|
|
39
|
-
#
|
|
40
|
-
# saved.
|
|
41
|
-
#
|
|
42
|
-
# To read back the image data, access the +data+ attribute. This will lazily
|
|
43
|
-
# load the data from the store.
|
|
39
|
+
# To read back the image data, access the +data+ attribute. The data is loaded lazily from the store.
|
|
44
40
|
#
|
|
45
41
|
# data = image.data
|
|
46
42
|
#
|
|
47
43
|
# == Cropping
|
|
48
44
|
#
|
|
49
|
-
# Images can be pre-cropped by setting +crop_width+, +crop_height+,
|
|
50
|
-
#
|
|
51
|
-
# image size.
|
|
45
|
+
# Images can be pre-cropped by setting +crop_width+, +crop_height+, +crop_start_x+ and +crop_start_y+. The crop
|
|
46
|
+
# dimensions cannot exceed the image size.
|
|
52
47
|
#
|
|
53
48
|
# image.update(
|
|
54
49
|
# crop_start_x: 15, crop_start_y: 20,
|
|
@@ -56,10 +51,14 @@ module DynamicImage
|
|
|
56
51
|
# )
|
|
57
52
|
# image.size # => Vector2d(300, 200)
|
|
58
53
|
#
|
|
59
|
-
# By default, images will be cropped from the center. You can control this
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
54
|
+
# By default, images will be cropped from the center. You can control this by setting +crop_gravity_x+ and
|
|
55
|
+
# +crop_gravity_y+. DynamicImage will make sure the pixel referred to by these coordinates are present in the
|
|
56
|
+
# cropped image, and as close to the center as possible without zooming in.
|
|
57
|
+
#
|
|
58
|
+
# @see DynamicImage::Model::Dimensions
|
|
59
|
+
# @see DynamicImage::Model::Transformations
|
|
60
|
+
# @see DynamicImage::Model::Validations
|
|
61
|
+
# @see DynamicImage::Model::Variants
|
|
63
62
|
module Model
|
|
64
63
|
extend ActiveSupport::Concern
|
|
65
64
|
include Dis::Model
|
|
@@ -72,33 +71,62 @@ module DynamicImage
|
|
|
72
71
|
before_validation :read_image_metadata, if: :data_changed?
|
|
73
72
|
end
|
|
74
73
|
|
|
75
|
-
# Returns true if the image
|
|
74
|
+
# Returns true if the image holds more than one frame.
|
|
75
|
+
#
|
|
76
|
+
# Images stored before +frame_count+ existed have none, and are taken to be still.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def animated?
|
|
80
|
+
has_attribute?(:frame_count) && frame_count.to_i > 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns the alt text for the image, or nil if none has been set.
|
|
84
|
+
#
|
|
85
|
+
# DynamicImage doesn't add this column by default. Either create it yourself or override the method to
|
|
86
|
+
# provide your own implementation.
|
|
87
|
+
#
|
|
88
|
+
# Note that there is a distinction between nil and a blank string. <tt>alt=""</tt> means the image is
|
|
89
|
+
# purely decorative, while a missing attribute is an accessibility defect.
|
|
90
|
+
#
|
|
91
|
+
# @return [String, nil]
|
|
92
|
+
# @see DynamicImage::Helper#dynamic_image_tag
|
|
93
|
+
def alt_text
|
|
94
|
+
self[:alt_text] if has_attribute?(:alt_text)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns true if the image is in the CMYK colorspace.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
76
100
|
def cmyk?
|
|
77
101
|
colorspace == "cmyk"
|
|
78
102
|
end
|
|
79
103
|
|
|
80
|
-
# Returns true if the image is in the grayscale colorspace
|
|
104
|
+
# Returns true if the image is in the grayscale colorspace.
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean]
|
|
81
107
|
def gray?
|
|
82
108
|
colorspace == "gray"
|
|
83
109
|
end
|
|
84
110
|
|
|
85
|
-
# Returns true if the image is in the RGB colorspace
|
|
111
|
+
# Returns true if the image is in the RGB colorspace.
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean]
|
|
86
114
|
def rgb?
|
|
87
115
|
colorspace == "rgb"
|
|
88
116
|
end
|
|
89
117
|
|
|
90
|
-
# Finds a web safe content type
|
|
91
|
-
#
|
|
118
|
+
# Finds a web safe content type, negotiated against {DynamicImage.default_formats}.
|
|
119
|
+
#
|
|
120
|
+
# @return [String]
|
|
121
|
+
# @see DynamicImage::FormatNegotiator
|
|
92
122
|
def safe_content_type
|
|
93
|
-
|
|
94
|
-
content_type
|
|
95
|
-
else
|
|
96
|
-
"image/jpeg"
|
|
97
|
-
end
|
|
123
|
+
DynamicImage::FormatNegotiator
|
|
124
|
+
.new(self).negotiate(DynamicImage.default_formats).content_type
|
|
98
125
|
end
|
|
99
126
|
|
|
100
|
-
# Includes a timestamp fingerprint in the URL param, so
|
|
101
|
-
#
|
|
127
|
+
# Includes a timestamp fingerprint in the URL param, so rendered images can be cached indefinitely.
|
|
128
|
+
#
|
|
129
|
+
# @return [String] the id and an +updated_at+ fingerprint
|
|
102
130
|
def to_param
|
|
103
131
|
[id, updated_at.utc.to_fs(cache_timestamp_format)].join("-")
|
|
104
132
|
end
|
|
@@ -119,17 +147,13 @@ module DynamicImage
|
|
|
119
147
|
self.real_width = metadata.width
|
|
120
148
|
self.real_height = metadata.height
|
|
121
149
|
self.content_type = metadata.content_type
|
|
150
|
+
self.frame_count = metadata.frame_count if has_attribute?(:frame_count)
|
|
151
|
+
self.alpha = metadata.alpha? if has_attribute?(:alpha)
|
|
122
152
|
@valid_image = true
|
|
123
153
|
end
|
|
124
154
|
|
|
125
155
|
def valid_image?
|
|
126
156
|
@valid_image ? true : false
|
|
127
157
|
end
|
|
128
|
-
|
|
129
|
-
def safe_content_types
|
|
130
|
-
%w[image/png
|
|
131
|
-
image/gif
|
|
132
|
-
image/jpeg]
|
|
133
|
-
end
|
|
134
158
|
end
|
|
135
159
|
end
|