Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins:
AllCops:
NewCops: enable
TargetRailsVersion: 7.2
TargetRubyVersion: 3.3
TargetRubyVersion: 3.4
Exclude:
- 'spec/internal/config/**/*'
- 'spec/internal/db/**/*'
Expand Down
2 changes: 1 addition & 1 deletion dynamic_image.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions lib/dynamic_image/breakpoints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/dynamic_image/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
# <tt>size: "400x", crop: true</tt> — 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 <tt>"0x0"</tt> does, or <tt>"1x1"</tt> against a very wide image.
# the size is zero on both axes, as <tt>"0x0"</tt> is, and constrains nothing, or the size works out to less
# than a pixel, as <tt>"1x1"</tt> does against a very wide image.
#
# @see DynamicImage::ImageSizing#fit
# @see DynamicImage::Model::Transformations#resize
Expand Down
26 changes: 17 additions & 9 deletions lib/dynamic_image/image_sizing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -80,7 +83,7 @@ def available_width(ratio = nil)
# be scaled up.
# @return [Vector2d] the resulting size
# @raise [DynamicImage::Errors::InvalidSizeOptions] if <tt>crop: true</tt> 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
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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
)
Expand All @@ -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)
Expand All @@ -201,9 +213,5 @@ def str_to_vector(str)
def uncropped?
@uncropped
end

def vector(width, height)
Vector2d.new(width, height)
end
end
end
12 changes: 9 additions & 3 deletions lib/dynamic_image/model/transformations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ module Transformations
# @param max_size [Vector2d, String] the size to fit within, either a vector or a <tt>"{width}x{height}"</tt>
# 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|
Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions spec/dynamic_image/image_sizing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions spec/dynamic_image/model/transformations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading