diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f39c7c9..9496948 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- ruby: ["3.3", "3.4", "4.0"]
+ ruby: ["3.4", "4.0"]
services:
postgres:
image: postgres:latest
diff --git a/.rubocop.yml b/.rubocop.yml
index d146353..5928ff8 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -9,7 +9,7 @@ plugins:
AllCops:
NewCops: enable
TargetRailsVersion: 7.2
- TargetRubyVersion: 3.3
+ TargetRubyVersion: 3.4
Exclude:
- 'spec/internal/config/**/*'
- 'spec/internal/db/**/*'
diff --git a/dynamic_image.gemspec b/dynamic_image.gemspec
index 92b8769..586c0cf 100644
--- a/dynamic_image.gemspec
+++ b/dynamic_image.gemspec
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
"README.md"
]
- s.required_ruby_version = ">= 3.3.0"
+ s.required_ruby_version = ">= 3.4.0"
s.add_dependency "dis", "~> 2.1"
s.add_dependency "rails", "> 7.0"
diff --git a/lib/dynamic_image/breakpoints.rb b/lib/dynamic_image/breakpoints.rb
index e5539a4..e8f6361 100644
--- a/lib/dynamic_image/breakpoints.rb
+++ b/lib/dynamic_image/breakpoints.rb
@@ -61,15 +61,15 @@ def widths(available)
private
def fixed(available)
- widths = spec.map(&:to_i).select { _1 <= available }.uniq.sort
+ widths = spec.map(&:to_i).select { it <= available }.uniq.sort
widths.any? ? widths : [available]
end
def stepped(available)
top = [available, spec.end || available].min
- rest = Enumerator.produce(top / step) { _1 / step }
- .take_while { _1 >= spec.begin }
- .map { snap(_1) }
+ rest = Enumerator.produce(top / step) { it / step }
+ .take_while { it >= spec.begin }
+ .map { snap(it) }
[top, *rest].reverse.uniq
end
diff --git a/lib/dynamic_image/errors.rb b/lib/dynamic_image/errors.rb
index b8bae16..7b386bf 100644
--- a/lib/dynamic_image/errors.rb
+++ b/lib/dynamic_image/errors.rb
@@ -28,9 +28,10 @@ class InvalidHeader < DynamicImage::Errors::Error; end
# @see DynamicImage::DigestVerifier#verify
class InvalidSignature < DynamicImage::Errors::Error; end
- # Raised when a size can't be rendered. Either cropping was requested without both dimensions, as in
+ # Raised when a size can't be rendered. Cropping was requested without both dimensions, as in
# size: "400x", crop: true — there is no way to crop to an exact size when one of them is unknown — or
- # the size works out to less than a pixel, as "0x0" does, or "1x1" against a very wide image.
+ # the size is zero on both axes, as "0x0" is, and constrains nothing, or the size works out to less
+ # than a pixel, as "1x1" does against a very wide image.
#
# @see DynamicImage::ImageSizing#fit
# @see DynamicImage::Model::Transformations#resize
diff --git a/lib/dynamic_image/image_sizing.rb b/lib/dynamic_image/image_sizing.rb
index 48b4a19..f4cbfdb 100644
--- a/lib/dynamic_image/image_sizing.rb
+++ b/lib/dynamic_image/image_sizing.rb
@@ -27,6 +27,7 @@ def initialize(record, options = {})
#
# @param ratio_vector [Vector2d] the aspect ratio to crop to
# @return [Array(Vector2d, Vector2d)] the crop size and crop start
+ # @raise [DynamicImage::Errors::InvalidSizeOptions] if the vector is zero on both axes
#
# @example
# image = Image.find(params[:id]) # 320x200 image
@@ -35,6 +36,8 @@ def initialize(record, options = {})
# sizing.crop_geometry(Vector2d(100, 100))
# # => [Vector2d(200, 200), Vector2d(60, 0)]
def crop_geometry(ratio_vector)
+ require_nonzero!(ratio_vector)
+
# Maximize the crop area to fit the image size
crop_size = ratio_vector.fit(size).round
@@ -66,7 +69,7 @@ def available_width(ratio = nil)
ratio = DynamicImage::Ratio.parse(ratio)
return size.x.floor unless ratio
- crop_geometry(vector(ratio, 1)).first.x.floor
+ crop_geometry(Vector2d.new(ratio, 1)).first.x.floor
end
# Adjusts +fit_size+ to fit the image dimensions. Any dimension set to zero will be ignored.
@@ -80,7 +83,7 @@ def available_width(ratio = nil)
# be scaled up.
# @return [Vector2d] the resulting size
# @raise [DynamicImage::Errors::InvalidSizeOptions] if crop: true is given and either dimension is zero,
- # or if the result is less than a pixel in either dimension
+ # if the size is zero on both axes, or if the result is less than a pixel in either dimension
#
# @example
# image = Image.find(params[:id]) # 320x200 image
@@ -100,6 +103,7 @@ def available_width(ratio = nil)
def fit(fit_size, options = {})
fit_size = parse_vector(fit_size)
require_dimensions!(fit_size) if options[:crop]
+ require_nonzero!(fit_size)
fit_size = size.fit(fit_size) unless options[:crop]
fit_size = contain(fit_size) unless options[:upscale]
fit_size = snap(fit_size)
@@ -163,8 +167,16 @@ def require_dimensions!(vector)
"both dimensions are required when cropping"
end
+ # Rejects a vector that is zero on both axes. A single zero axis means the axis is unconstrained, but a vector
+ # that is zero throughout constrains nothing and describes no image.
+ def require_nonzero!(vector)
+ return unless vector.x.zero? && vector.y.zero?
+
+ raise DynamicImage::Errors::InvalidSizeOptions, "#{vector} has no size"
+ end
+
# Rejects sizes that don't round to at least one pixel in each dimension, since there is no image to render at
- # that point. A NaN, which is what an empty size fits to, fails this too.
+ # that point.
def require_pixels!(vector)
return if vector.x >= 1 && vector.y >= 1
@@ -173,7 +185,7 @@ def require_pixels!(vector)
end
def shift_vector(vect)
- vector(
+ Vector2d.new(
vect.x.negative? ? vect.x.abs : 0,
vect.y.negative? ? vect.y.abs : 0
)
@@ -183,7 +195,7 @@ def shift_vector(vect)
# point, so an axis that should land exactly on a pixel can come out a few ulps below it, and callers flooring
# the result would lose that pixel.
def snap(scaled)
- vector(snap_axis(scaled.x), snap_axis(scaled.y))
+ Vector2d.new(snap_axis(scaled.x), snap_axis(scaled.y))
end
def snap_axis(value)
@@ -201,9 +213,5 @@ def str_to_vector(str)
def uncropped?
@uncropped
end
-
- def vector(width, height)
- Vector2d.new(width, height)
- end
end
end
diff --git a/lib/dynamic_image/model/transformations.rb b/lib/dynamic_image/model/transformations.rb
index 4f8dafe..fcc0ad1 100644
--- a/lib/dynamic_image/model/transformations.rb
+++ b/lib/dynamic_image/model/transformations.rb
@@ -17,7 +17,8 @@ module Transformations
# @param max_size [Vector2d, String] the size to fit within, either a vector or a "{width}x{height}"
# string. Either dimension may be omitted to scale by the other alone.
# @return [self]
- # @raise [DynamicImage::Errors::InvalidSizeOptions] if the result is less than a pixel in either dimension
+ # @raise [DynamicImage::Errors::InvalidSizeOptions] if the size is zero on both axes, or if the result is less
+ # than a pixel in either dimension
# @raise [DynamicImage::Errors::InvalidImage] if the stored data can't be processed
def resize(max_size)
transform_image do |image|
@@ -55,9 +56,14 @@ def rotate(degrees = 90)
private
- # Scales +max_size+ against the image, rejecting a result smaller than a pixel in either dimension, as
- # {DynamicImage::ImageSizing#fit} does for the sizes rendered on request.
+ # Scales +max_size+ against the image, rejecting a size that is zero on both axes and a result smaller than a
+ # pixel in either dimension, as {DynamicImage::ImageSizing#fit} does for the sizes rendered on request.
def fit_size(max_size)
+ max_size = Vector2d.parse(max_size)
+ if max_size.x.zero? && max_size.y.zero?
+ raise DynamicImage::Errors::InvalidSizeOptions, "#{max_size} has no size"
+ end
+
new_size = real_size.fit(max_size)
return new_size if new_size.x >= 1 && new_size.y >= 1
diff --git a/spec/dynamic_image/image_sizing_spec.rb b/spec/dynamic_image/image_sizing_spec.rb
index 12fc265..11d37f6 100644
--- a/spec/dynamic_image/image_sizing_spec.rb
+++ b/spec/dynamic_image/image_sizing_spec.rb
@@ -58,6 +58,14 @@ def v(width, height)
it { is_expected.to eq([v(200, 200), v(120, 0)]) }
end
+
+ context "with both dimensions zero" do
+ it "raises an error" do
+ expect { crop_geometry(0, 0) }.to(
+ raise_error(DynamicImage::Errors::InvalidSizeOptions)
+ )
+ end
+ end
end
describe "#crop_geometry (cropped image)" do
diff --git a/spec/dynamic_image/model/transformations_spec.rb b/spec/dynamic_image/model/transformations_spec.rb
index 23ecc22..257cd76 100644
--- a/spec/dynamic_image/model/transformations_spec.rb
+++ b/spec/dynamic_image/model/transformations_spec.rb
@@ -79,6 +79,16 @@
end
context "when the size is smaller than a pixel" do
+ subject(:resized) { image.resize("1x1") }
+
+ it "raises an error" do
+ expect { resized }.to(
+ raise_error(DynamicImage::Errors::InvalidSizeOptions)
+ )
+ end
+ end
+
+ context "with both dimensions zero" do
subject(:resized) { image.resize("0x0") }
it "raises an error" do