dynamic_image 3.1.3 → 3.4.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 +9 -5
- data/lib/dynamic_image/breakpoints.rb +4 -4
- data/lib/dynamic_image/controller.rb +10 -0
- data/lib/dynamic_image/errors.rb +5 -2
- data/lib/dynamic_image/helper/pictures.rb +2 -0
- data/lib/dynamic_image/helper.rb +15 -3
- data/lib/dynamic_image/image_processor/transform.rb +20 -6
- data/lib/dynamic_image/image_processor.rb +1 -1
- data/lib/dynamic_image/image_sizing.rb +106 -27
- data/lib/dynamic_image/model/transformations.rb +28 -3
- data/lib/dynamic_image/picture.rb +14 -3
- data/lib/dynamic_image/processed_image.rb +1 -1
- data/lib/dynamic_image/version.rb +1 -1
- metadata +13 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3053f66d3a18e5e0ebc6cc6ab5efe548d8fe296e80708fbde264d808184b54cf
|
|
4
|
+
data.tar.gz: 45d1d51fa23b0501d3af442abcac9d3bbc13e943ebe89343a2746cdda92e979d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aaa7011dc360af36d61ec8435a11a37704a7062271edacea578c0f8c5d5322a913abebf3d11840702153d22f5f1308fcda74764ed120f4effb0fd8427f91febc
|
|
7
|
+
data.tar.gz: 96d997eb20a89e071e8dbcf6e4aee0289ecb11def3a089f03f1a87235a897b4c0b5a0b233d2d54792f729ea83bef3e90fa46e52e8e296d4e74a0b073bca734e8
|
data/README.md
CHANGED
|
@@ -357,7 +357,9 @@ image.save
|
|
|
357
357
|
`rotate` turns the image, taking the crop along with it. The angle must
|
|
358
358
|
be a multiple of 90.
|
|
359
359
|
|
|
360
|
-
`resize` scales the stored file
|
|
360
|
+
`resize` scales the stored file to fit within the given size and replaces
|
|
361
|
+
the original. Note that it scales up as well as down: an image smaller
|
|
362
|
+
than the given size will be upscaled.
|
|
361
363
|
|
|
362
364
|
### Outside of views
|
|
363
365
|
|
|
@@ -456,10 +458,12 @@ and the [changelog](CHANGELOG.md) for release notes.
|
|
|
456
458
|
|
|
457
459
|
## Contributing
|
|
458
460
|
|
|
459
|
-
Bug reports and pull requests are welcome
|
|
460
|
-
[
|
|
461
|
-
[
|
|
462
|
-
|
|
461
|
+
Bug reports, feature requests, and pull requests are welcome. Read
|
|
462
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) first. Report security vulnerabilities
|
|
463
|
+
privately as described in [SECURITY.md](SECURITY.md).
|
|
464
|
+
|
|
465
|
+
CONTRIBUTING.md also covers how to run the tests and how commits are
|
|
466
|
+
formatted. Everyone participating is expected to follow the
|
|
463
467
|
[code of conduct](CODE_OF_CONDUCT.md).
|
|
464
468
|
|
|
465
469
|
## License
|
|
@@ -61,15 +61,15 @@ module DynamicImage
|
|
|
61
61
|
private
|
|
62
62
|
|
|
63
63
|
def fixed(available)
|
|
64
|
-
widths = spec.map(&:to_i).select {
|
|
64
|
+
widths = spec.map(&:to_i).select { it <= available }.uniq.sort
|
|
65
65
|
widths.any? ? widths : [available]
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def stepped(available)
|
|
69
69
|
top = [available, spec.end || available].min
|
|
70
|
-
rest = Enumerator.produce(top / step) {
|
|
71
|
-
.take_while {
|
|
72
|
-
.map { snap(
|
|
70
|
+
rest = Enumerator.produce(top / step) { it / step }
|
|
71
|
+
.take_while { it >= spec.begin }
|
|
72
|
+
.map { snap(it) }
|
|
73
73
|
|
|
74
74
|
[top, *rest].reverse.uniq
|
|
75
75
|
end
|
|
@@ -37,6 +37,7 @@ module DynamicImage
|
|
|
37
37
|
included do
|
|
38
38
|
before_action :verify_signed_params
|
|
39
39
|
before_action :find_record
|
|
40
|
+
before_action :redirect_to_canonical_id
|
|
40
41
|
after_action :cache_expiration_header
|
|
41
42
|
helper_method :requested_size
|
|
42
43
|
end
|
|
@@ -97,6 +98,15 @@ module DynamicImage
|
|
|
97
98
|
@record = model.find(params[:id])
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
# Redirects permanently to the same URL with the record's +to_param+ as the id, unless the URL already uses it.
|
|
102
|
+
def redirect_to_canonical_id
|
|
103
|
+
return if params[:id] == @record.to_param
|
|
104
|
+
|
|
105
|
+
expires_now
|
|
106
|
+
redirect_to url_for(params.permit(:digest, :size, :format).merge(id: @record.to_param, only_path: true)),
|
|
107
|
+
status: :moved_permanently
|
|
108
|
+
end
|
|
109
|
+
|
|
100
110
|
# The filename the image is served as, the record's own filename with the extension of the rendered format.
|
|
101
111
|
# Override to name downloads something else.
|
|
102
112
|
def filename(format = nil)
|
data/lib/dynamic_image/errors.rb
CHANGED
|
@@ -28,10 +28,13 @@ module DynamicImage
|
|
|
28
28
|
# @see DynamicImage::DigestVerifier#verify
|
|
29
29
|
class InvalidSignature < DynamicImage::Errors::Error; end
|
|
30
30
|
|
|
31
|
-
# Raised when
|
|
32
|
-
# way to crop to an exact size when one of them is unknown
|
|
31
|
+
# Raised when a size can't be rendered. Cropping was requested without both dimensions, as in
|
|
32
|
+
# <tt>size: "400x", crop: true</tt> — there is no way to crop to an exact size when one of them is unknown — or
|
|
33
|
+
# the size is zero on both axes, as <tt>"0x0"</tt> is, and constrains nothing, or the size works out to less
|
|
34
|
+
# than a pixel, as <tt>"1x1"</tt> does against a very wide image.
|
|
33
35
|
#
|
|
34
36
|
# @see DynamicImage::ImageSizing#fit
|
|
37
|
+
# @see DynamicImage::Model::Transformations#resize
|
|
35
38
|
class InvalidSizeOptions < DynamicImage::Errors::Error; end
|
|
36
39
|
|
|
37
40
|
# Raised when a transformation can't be applied, either because a rotation isn't a multiple of 90 degrees or
|
|
@@ -45,6 +45,8 @@ module DynamicImage
|
|
|
45
45
|
# @option options [Integer] :fallback_width The width to ask for the <tt>img</tt>, overriding
|
|
46
46
|
# {DynamicImage.picture_fallback_width}
|
|
47
47
|
# @return [String] the picture element
|
|
48
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if the image has less than a pixel to render at the
|
|
49
|
+
# requested ratio
|
|
48
50
|
#
|
|
49
51
|
# @example
|
|
50
52
|
# dynamic_picture_tag(image, sizes: "50vw", alt: "A kitten")
|
data/lib/dynamic_image/helper.rb
CHANGED
|
@@ -54,9 +54,11 @@ module DynamicImage
|
|
|
54
54
|
# # => <img alt="Avatar" src="..." width="100" height="62" />
|
|
55
55
|
def dynamic_image_tag(record_or_array, options = {})
|
|
56
56
|
record = extract_dynamic_image_record(record_or_array)
|
|
57
|
+
requested = options[:size]
|
|
57
58
|
size = fit_size!(record_or_array, options)
|
|
58
59
|
url_options = options.extract!(*allowed_dynamic_image_url_options)
|
|
59
|
-
html_options = { size
|
|
60
|
+
html_options = { size: display_size(size, requested), alt: record.alt_text }
|
|
61
|
+
.merge(options)
|
|
60
62
|
|
|
61
63
|
image_tag(dynamic_image_path_with_size(record_or_array,
|
|
62
64
|
size,
|
|
@@ -78,7 +80,8 @@ module DynamicImage
|
|
|
78
80
|
# an array is negotiated against the image by {DynamicImage::FormatNegotiator}. Defaults to
|
|
79
81
|
# {DynamicImage.default_formats}, or {DynamicImage.mailer_formats} in a mailer view.
|
|
80
82
|
# @return [String]
|
|
81
|
-
# @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt>
|
|
83
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if the size is zero on both axes, or if <tt>crop: true</tt>
|
|
84
|
+
# is given without both dimensions or leaves less than a pixel to render
|
|
82
85
|
#
|
|
83
86
|
# Any options supported by +polymorphic_url+ are also accepted.
|
|
84
87
|
#
|
|
@@ -217,7 +220,16 @@ module DynamicImage
|
|
|
217
220
|
def image_sizing(record, size_opts, uncropped)
|
|
218
221
|
ImageSizing
|
|
219
222
|
.new(record, uncropped:)
|
|
220
|
-
.
|
|
223
|
+
.fit_renderable(size_opts[:size], size_opts).floor.to_s
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Ensure either dimension is at least 1px
|
|
227
|
+
def display_size(size, requested)
|
|
228
|
+
return size unless requested
|
|
229
|
+
|
|
230
|
+
Vector2d.parse(size)
|
|
231
|
+
.fit(Vector2d.parse(requested), upscale: false)
|
|
232
|
+
.floor.max(1).to_s
|
|
221
233
|
end
|
|
222
234
|
end
|
|
223
235
|
end
|
|
@@ -25,16 +25,30 @@ module DynamicImage
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Resizes the image to fit within +new_size+, retaining its aspect ratio. Crop first if the image needs to
|
|
29
|
+
# fill the size exactly.
|
|
29
30
|
#
|
|
30
|
-
# @param new_size [Vector2d] the size to
|
|
31
|
+
# @param new_size [Vector2d] the size to fit within
|
|
31
32
|
# @return [DynamicImage::ImageProcessor] a new processor
|
|
32
33
|
def resize(new_size)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
resize_exact(size.fit(Vector2d(new_size)))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Resizes the image to exactly +new_size+, rounded to whole pixels. The aspect ratio is not retained, so the
|
|
38
|
+
# image is stretched if the size doesn't match its own ratio.
|
|
39
|
+
#
|
|
40
|
+
# Use this when the size has already been worked out and has to be honored as given, as it is for a size
|
|
41
|
+
# coming from a URL. {DynamicImage::ImageSizing} computed it against this image, the helper committed to it in
|
|
42
|
+
# the markup, and refitting it here would land on a different size than the one advertised.
|
|
43
|
+
#
|
|
44
|
+
# @param new_size [Vector2d] the size to render
|
|
45
|
+
# @return [DynamicImage::ImageProcessor] a new processor
|
|
46
|
+
def resize_exact(new_size)
|
|
47
|
+
new_size = Vector2d(new_size).round
|
|
48
|
+
apply image.thumbnail_image(new_size.x,
|
|
49
|
+
height: new_size.y,
|
|
36
50
|
crop: :none,
|
|
37
|
-
size: :
|
|
51
|
+
size: :force)
|
|
38
52
|
end
|
|
39
53
|
|
|
40
54
|
# Rotates the image. The rotation must be a multiple of 90 degrees.
|
|
@@ -7,6 +7,11 @@ module DynamicImage
|
|
|
7
7
|
# image, and it can be used on its own when you need the size without rendering anything: reserving space in a
|
|
8
8
|
# layout, filling in <tt>og:image:width</tt>, or laying out a PDF.
|
|
9
9
|
#
|
|
10
|
+
# The size it returns is the size that gets rendered. The helper floors it into the URL and the markup, and
|
|
11
|
+
# {DynamicImage::ImageProcessor::Transform#resize_exact} honors that number instead of fitting it again. Scaling
|
|
12
|
+
# is done in floating point, so +snap+ pulls an axis that lands a rounding error below a whole pixel back onto it
|
|
13
|
+
# before the helper's floor can drop the pixel.
|
|
14
|
+
#
|
|
10
15
|
# @example
|
|
11
16
|
# sizing = DynamicImage::ImageSizing.new(image)
|
|
12
17
|
# sizing.fit("400x400") # => Vector2d(400.0, 250.0)
|
|
@@ -27,6 +32,7 @@ module DynamicImage
|
|
|
27
32
|
#
|
|
28
33
|
# @param ratio_vector [Vector2d] the aspect ratio to crop to
|
|
29
34
|
# @return [Array(Vector2d, Vector2d)] the crop size and crop start
|
|
35
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if the vector is zero on both axes
|
|
30
36
|
#
|
|
31
37
|
# @example
|
|
32
38
|
# image = Image.find(params[:id]) # 320x200 image
|
|
@@ -35,6 +41,8 @@ module DynamicImage
|
|
|
35
41
|
# sizing.crop_geometry(Vector2d(100, 100))
|
|
36
42
|
# # => [Vector2d(200, 200), Vector2d(60, 0)]
|
|
37
43
|
def crop_geometry(ratio_vector)
|
|
44
|
+
require_nonzero!(ratio_vector)
|
|
45
|
+
|
|
38
46
|
# Maximize the crop area to fit the image size
|
|
39
47
|
crop_size = ratio_vector.fit(size).round
|
|
40
48
|
|
|
@@ -66,7 +74,7 @@ module DynamicImage
|
|
|
66
74
|
ratio = DynamicImage::Ratio.parse(ratio)
|
|
67
75
|
return size.x.floor unless ratio
|
|
68
76
|
|
|
69
|
-
crop_geometry(
|
|
77
|
+
crop_geometry(Vector2d.new(ratio, 1)).first.x.floor
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
# Adjusts +fit_size+ to fit the image dimensions. Any dimension set to zero will be ignored.
|
|
@@ -79,7 +87,8 @@ module DynamicImage
|
|
|
79
87
|
# @option options [Boolean] :upscale Don't limit to the size of the image. Images smaller than the given size will
|
|
80
88
|
# be scaled up.
|
|
81
89
|
# @return [Vector2d] the resulting size
|
|
82
|
-
# @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt> is given and either dimension is zero
|
|
90
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt> is given and either dimension is zero,
|
|
91
|
+
# if the size is zero on both axes, or if the result is less than a pixel in either dimension
|
|
83
92
|
#
|
|
84
93
|
# @example
|
|
85
94
|
# image = Image.find(params[:id]) # 320x200 image
|
|
@@ -89,7 +98,7 @@ module DynamicImage
|
|
|
89
98
|
# # => Vector2d(160.0, 100.0)
|
|
90
99
|
#
|
|
91
100
|
# sizing.fit(Vector2d(500, 500))
|
|
92
|
-
# # => Vector2d(320
|
|
101
|
+
# # => Vector2d(320, 200)
|
|
93
102
|
#
|
|
94
103
|
# sizing.fit(Vector2d(500, 500), crop: true)
|
|
95
104
|
# # => Vector2d(200.0, 200.0)
|
|
@@ -98,36 +107,66 @@ module DynamicImage
|
|
|
98
107
|
# # => Vector2d(500.0, 312.5)
|
|
99
108
|
def fit(fit_size, options = {})
|
|
100
109
|
fit_size = parse_vector(fit_size)
|
|
101
|
-
require_dimensions!(fit_size)
|
|
102
|
-
|
|
103
|
-
fit_size =
|
|
110
|
+
require_dimensions!(fit_size) if options[:crop]
|
|
111
|
+
require_nonzero!(fit_size)
|
|
112
|
+
fit_size = snap(scale(fit_size, options))
|
|
113
|
+
require_pixels!(fit_size)
|
|
104
114
|
fit_size
|
|
105
115
|
end
|
|
106
116
|
|
|
117
|
+
# Fits the size like {#fit}, but returns the smallest size the image can be rendered at rather than raising when
|
|
118
|
+
# the result lands under a pixel.
|
|
119
|
+
#
|
|
120
|
+
# @param fit_size [Vector2d, String] the size to fit within, as taken by {#fit}
|
|
121
|
+
# @param options [Hash] as taken by {#fit}
|
|
122
|
+
# @return [Vector2d] the resulting size
|
|
123
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if the size is zero on both axes, or if <tt>crop: true</tt>
|
|
124
|
+
# is given and the crop is less than a pixel
|
|
125
|
+
#
|
|
126
|
+
# @example
|
|
127
|
+
# image = Image.find(params[:id]) # 2000x1 image
|
|
128
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
|
129
|
+
#
|
|
130
|
+
# sizing.fit_renderable("1200x") # => Vector2d(2000.0, 1.0)
|
|
131
|
+
def fit_renderable(fit_size, options = {})
|
|
132
|
+
require_nonzero!(parse_vector(fit_size))
|
|
133
|
+
return fit(fit_size, options) if options[:crop] || renderable?(fit_size, options)
|
|
134
|
+
|
|
135
|
+
size.cover(1).round
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns true if the image can be rendered at +fit_size+, false if {#fit} rejects it.
|
|
139
|
+
#
|
|
140
|
+
# @param fit_size [Vector2d, String] the size to fit within, as taken by {#fit}
|
|
141
|
+
# @param options [Hash] as taken by {#fit}
|
|
142
|
+
# @return [Boolean]
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# image = Image.find(params[:id]) # 320x200 image
|
|
146
|
+
# sizing = DynamicImage::ImageSizing.new(image)
|
|
147
|
+
#
|
|
148
|
+
# sizing.renderable?(Vector2d(100, 0)) # => true
|
|
149
|
+
# sizing.renderable?(Vector2d(1, 0)) # => false
|
|
150
|
+
def renderable?(fit_size, options = {})
|
|
151
|
+
vector = parse_vector(fit_size)
|
|
152
|
+
return false if options[:crop] && !(vector.x.positive? && vector.y.positive?)
|
|
153
|
+
return false if vector.x.zero? && vector.y.zero?
|
|
154
|
+
|
|
155
|
+
pixels?(snap(scale(vector, options)))
|
|
156
|
+
end
|
|
157
|
+
|
|
107
158
|
private
|
|
108
159
|
|
|
109
160
|
def crop_gravity
|
|
110
|
-
|
|
111
|
-
size / 2
|
|
112
|
-
else
|
|
113
|
-
record.crop_gravity
|
|
114
|
-
end
|
|
161
|
+
uncropped? && !record.crop_gravity? ? size / 2 : record.crop_gravity
|
|
115
162
|
end
|
|
116
163
|
|
|
117
164
|
def crop_start
|
|
118
|
-
|
|
119
|
-
Vector2d.new(0, 0)
|
|
120
|
-
else
|
|
121
|
-
record.crop_start
|
|
122
|
-
end
|
|
165
|
+
uncropped? ? Vector2d.new(0, 0) : record.crop_start
|
|
123
166
|
end
|
|
124
167
|
|
|
125
168
|
def size
|
|
126
|
-
|
|
127
|
-
record.real_size
|
|
128
|
-
else
|
|
129
|
-
record.size
|
|
130
|
-
end
|
|
169
|
+
uncropped? ? record.real_size : record.size
|
|
131
170
|
end
|
|
132
171
|
|
|
133
172
|
# Clamps the rectangle defined by +start+ and +size+ to fit inside 0, 0 and +max_size+. It is assumed that +size+
|
|
@@ -140,6 +179,14 @@ module DynamicImage
|
|
|
140
179
|
start
|
|
141
180
|
end
|
|
142
181
|
|
|
182
|
+
# Scales +fit_size+ against the image, honouring <tt>:crop</tt> and <tt>:upscale</tt>.
|
|
183
|
+
def scale(fit_size, options)
|
|
184
|
+
upscale = options[:upscale] ? true : false
|
|
185
|
+
return size.fit(fit_size, upscale:) unless options[:crop]
|
|
186
|
+
|
|
187
|
+
upscale ? fit_size : fit_size.fit(size, upscale: false)
|
|
188
|
+
end
|
|
189
|
+
|
|
143
190
|
def parse_vector(vector)
|
|
144
191
|
vector.is_a?(String) ? str_to_vector(vector) : vector
|
|
145
192
|
end
|
|
@@ -149,16 +196,52 @@ module DynamicImage
|
|
|
149
196
|
def require_dimensions!(vector)
|
|
150
197
|
return if vector.x.positive? && vector.y.positive?
|
|
151
198
|
|
|
152
|
-
raise DynamicImage::Errors::InvalidSizeOptions
|
|
199
|
+
raise DynamicImage::Errors::InvalidSizeOptions,
|
|
200
|
+
"both dimensions are required when cropping"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Rejects a vector that is zero on both axes. A single zero axis means the axis is unconstrained, but a vector
|
|
204
|
+
# that is zero throughout constrains nothing and describes no image.
|
|
205
|
+
def require_nonzero!(vector)
|
|
206
|
+
return unless vector.x.zero? && vector.y.zero?
|
|
207
|
+
|
|
208
|
+
raise DynamicImage::Errors::InvalidSizeOptions, "#{vector} has no size"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Rejects sizes that don't round to at least one pixel in each dimension, since there is no image to render at
|
|
212
|
+
# that point.
|
|
213
|
+
def require_pixels!(vector)
|
|
214
|
+
return if pixels?(vector)
|
|
215
|
+
|
|
216
|
+
raise DynamicImage::Errors::InvalidSizeOptions,
|
|
217
|
+
"#{vector} has a dimension smaller than one pixel"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def pixels?(vector)
|
|
221
|
+
vector.x >= 1 && vector.y >= 1
|
|
153
222
|
end
|
|
154
223
|
|
|
155
224
|
def shift_vector(vect)
|
|
156
|
-
|
|
225
|
+
Vector2d.new(
|
|
157
226
|
vect.x.negative? ? vect.x.abs : 0,
|
|
158
227
|
vect.y.negative? ? vect.y.abs : 0
|
|
159
228
|
)
|
|
160
229
|
end
|
|
161
230
|
|
|
231
|
+
# Snaps each axis of +scaled+ onto the whole pixel it is a rounding error away from. Scaling is done in floating
|
|
232
|
+
# point, so an axis that should land exactly on a pixel can come out a few ulps below it, and callers flooring
|
|
233
|
+
# the result would lose that pixel.
|
|
234
|
+
def snap(scaled)
|
|
235
|
+
Vector2d.new(snap_axis(scaled.x), snap_axis(scaled.y))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def snap_axis(value)
|
|
239
|
+
return value unless value.is_a?(Float) && value.finite?
|
|
240
|
+
|
|
241
|
+
rounded = value.round
|
|
242
|
+
(value - rounded).abs <= value.abs * 8 * Float::EPSILON ? rounded.to_f : value
|
|
243
|
+
end
|
|
244
|
+
|
|
162
245
|
def str_to_vector(str)
|
|
163
246
|
x, y = str.match(/(\d*)x(\d*)/)[1, 2].map(&:to_i)
|
|
164
247
|
Vector2d.new(x, y)
|
|
@@ -167,9 +250,5 @@ module DynamicImage
|
|
|
167
250
|
def uncropped?
|
|
168
251
|
@uncropped
|
|
169
252
|
end
|
|
170
|
-
|
|
171
|
-
def vector(width, height)
|
|
172
|
-
Vector2d.new(width, height)
|
|
173
|
-
end
|
|
174
253
|
end
|
|
175
254
|
end
|
|
@@ -9,13 +9,20 @@ module DynamicImage
|
|
|
9
9
|
# Both methods replace the data and update the stored dimensions, adjusting the crop to match. Neither saves
|
|
10
10
|
# the record.
|
|
11
11
|
module Transformations
|
|
12
|
-
# Resizes the image, replacing the stored file
|
|
12
|
+
# Resizes the image, replacing the stored file. The crop is scaled along with it.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# The image is scaled to fit within +max_size+, retaining its aspect ratio. Images smaller than +max_size+
|
|
15
|
+
# will be scaled up.
|
|
16
|
+
#
|
|
17
|
+
# @param max_size [Vector2d, String] the size to fit within, either a vector or a <tt>"{width}x{height}"</tt>
|
|
18
|
+
# string. Either dimension may be omitted to scale by the other alone.
|
|
15
19
|
# @return [self]
|
|
20
|
+
# @raise [DynamicImage::Errors::InvalidSizeOptions] if the size is zero on both axes, or if the result is less
|
|
21
|
+
# than a pixel in either dimension
|
|
22
|
+
# @raise [DynamicImage::Errors::InvalidImage] if the stored data can't be processed
|
|
16
23
|
def resize(max_size)
|
|
17
24
|
transform_image do |image|
|
|
18
|
-
resized = image.
|
|
25
|
+
resized = image.resize_exact(fit_size(max_size))
|
|
19
26
|
scale_crop(resized.size)
|
|
20
27
|
resized
|
|
21
28
|
end
|
|
@@ -26,6 +33,7 @@ module DynamicImage
|
|
|
26
33
|
# @param degrees [Integer] the angle, which must be a multiple of 90. Rotating by 0 is a no-op.
|
|
27
34
|
# @return [self]
|
|
28
35
|
# @raise [DynamicImage::Errors::InvalidTransformation] if the angle isn't a multiple of 90
|
|
36
|
+
# @raise [DynamicImage::Errors::InvalidImage] if the stored data can't be processed
|
|
29
37
|
#
|
|
30
38
|
# @example
|
|
31
39
|
# image.rotate(90)
|
|
@@ -48,6 +56,21 @@ module DynamicImage
|
|
|
48
56
|
|
|
49
57
|
private
|
|
50
58
|
|
|
59
|
+
# Scales +max_size+ against the image, rejecting a size that is zero on both axes and a result smaller than a
|
|
60
|
+
# pixel in either dimension, as {DynamicImage::ImageSizing#fit} does for the sizes rendered on request.
|
|
61
|
+
def fit_size(max_size)
|
|
62
|
+
max_size = Vector2d.parse(max_size)
|
|
63
|
+
if max_size.x.zero? && max_size.y.zero?
|
|
64
|
+
raise DynamicImage::Errors::InvalidSizeOptions, "#{max_size} has no size"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
new_size = real_size.fit(max_size)
|
|
68
|
+
return new_size if new_size.x >= 1 && new_size.y >= 1
|
|
69
|
+
|
|
70
|
+
raise DynamicImage::Errors::InvalidSizeOptions,
|
|
71
|
+
"#{new_size} has a dimension smaller than one pixel"
|
|
72
|
+
end
|
|
73
|
+
|
|
51
74
|
def scale_crop(new_size)
|
|
52
75
|
scale = new_size.to_f_vector / real_size
|
|
53
76
|
|
|
@@ -117,6 +140,8 @@ module DynamicImage
|
|
|
117
140
|
end
|
|
118
141
|
read_image_metadata
|
|
119
142
|
self
|
|
143
|
+
rescue Vips::Error => e
|
|
144
|
+
raise DynamicImage::Errors::InvalidImage, e.message
|
|
120
145
|
end
|
|
121
146
|
end
|
|
122
147
|
end
|
|
@@ -28,7 +28,7 @@ module DynamicImage
|
|
|
28
28
|
# @!attribute [r] breakpoints
|
|
29
29
|
# @return [DynamicImage::Breakpoints] the candidate widths
|
|
30
30
|
# @!attribute [r] fallback_width
|
|
31
|
-
# @return [Integer] the width
|
|
31
|
+
# @return [Integer] the width configured for the fallback image
|
|
32
32
|
attr_reader :template, :record_or_array, :ratio, :sizes, :breakpoints, :fallback_width, :url_options
|
|
33
33
|
|
|
34
34
|
# @param template [ActionView::Base] the view context, for routing
|
|
@@ -77,9 +77,13 @@ module DynamicImage
|
|
|
77
77
|
|
|
78
78
|
# The candidate widths, smallest first.
|
|
79
79
|
#
|
|
80
|
+
# Widths the image can't be rendered at are left out. One that can't be rendered at any of them gets a single
|
|
81
|
+
# candidate at its available width.
|
|
82
|
+
#
|
|
80
83
|
# @return [Array<Integer>]
|
|
81
84
|
def widths
|
|
82
|
-
@widths ||= breakpoints.widths(available_width)
|
|
85
|
+
@widths ||= breakpoints.widths(available_width).select { renderable?(it) }.presence ||
|
|
86
|
+
[available_width].select { renderable?(it) }
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
# Every candidate, as the URL and the size it is actually rendered at.
|
|
@@ -128,9 +132,12 @@ module DynamicImage
|
|
|
128
132
|
|
|
129
133
|
# The size asked for the fallback image, as a <tt>"{width}x{height}"</tt> string.
|
|
130
134
|
#
|
|
135
|
+
# This is {#fallback_width}, unless the image can't be rendered that wide, in which case the widest candidate
|
|
136
|
+
# it can be rendered at stands in.
|
|
137
|
+
#
|
|
131
138
|
# @return [String]
|
|
132
139
|
def fallback_size
|
|
133
|
-
@fallback_size ||= size_for(fallback_width)
|
|
140
|
+
@fallback_size ||= size_for(renderable?(fallback_width) ? fallback_width : (widths.last || fallback_width))
|
|
134
141
|
end
|
|
135
142
|
|
|
136
143
|
# The size the fallback image is actually rendered at. Smaller than {#fallback_size} when the image is.
|
|
@@ -171,6 +178,10 @@ module DynamicImage
|
|
|
171
178
|
DynamicImage::Breakpoints.new(options[:breakpoints], step: options[:step])
|
|
172
179
|
end
|
|
173
180
|
|
|
181
|
+
def renderable?(width)
|
|
182
|
+
sizing.renderable?(size_for(width), crop: crop?)
|
|
183
|
+
end
|
|
184
|
+
|
|
174
185
|
def record
|
|
175
186
|
record_or_array.last
|
|
176
187
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dynamic_image
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Inge Jørgensen
|
|
@@ -61,16 +61,22 @@ dependencies:
|
|
|
61
61
|
name: vector2d
|
|
62
62
|
requirement: !ruby/object:Gem::Requirement
|
|
63
63
|
requirements:
|
|
64
|
-
- - "
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.0'
|
|
67
|
+
- - "<"
|
|
65
68
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
69
|
+
version: '4.0'
|
|
67
70
|
type: :runtime
|
|
68
71
|
prerelease: false
|
|
69
72
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
73
|
requirements:
|
|
71
|
-
- - "
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.0'
|
|
77
|
+
- - "<"
|
|
72
78
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
79
|
+
version: '4.0'
|
|
74
80
|
description: DynamicImage is a Rails plugin that simplifies image uploading and processing
|
|
75
81
|
email:
|
|
76
82
|
- inge@elektronaut.no
|
|
@@ -139,14 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
139
145
|
requirements:
|
|
140
146
|
- - ">="
|
|
141
147
|
- !ruby/object:Gem::Version
|
|
142
|
-
version: 3.
|
|
148
|
+
version: 3.4.0
|
|
143
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
150
|
requirements:
|
|
145
151
|
- - ">="
|
|
146
152
|
- !ruby/object:Gem::Version
|
|
147
153
|
version: '0'
|
|
148
154
|
requirements: []
|
|
149
|
-
rubygems_version: 4.0.
|
|
155
|
+
rubygems_version: 4.0.20
|
|
150
156
|
specification_version: 4
|
|
151
157
|
summary: Rails plugin that simplifies image uploading and processing
|
|
152
158
|
test_files: []
|