Keep a side computed for a size with one side 0 at 1 or more (closes #283)
check / check (push) Canceled after 0s

The side was rounded down, so for a thin source it came out at 0, and
libvips made the size square: an 8192x1 source at 100x0 came out at
100x100, at 1300x0 with fit=cover libvips asked for about 39 GB, and an
8192x2 source at 100x0 answered 500. fit=contain and fit=inside work
both sides out again from the smaller scale and keep each at 1 or more
as well, as a 1x8192 source at 0x100 answered 500 there.

Model: opus-5-5
This commit is contained in:
2026-10-16 00:39:59 +00:00
parent dbb21e2f8d
commit 5aa44bbdb4
2 changed files with 26 additions and 14 deletions
+12 -5
View File
@@ -26,13 +26,21 @@ resolved. The disk cache is now size-bounded with LRU eviction
# Next Step # Next Step
P1: security: a size with one side `0` whose side computed from the source's P2: security: per-origin rate limiting
proportions rounds down to 0 comes out square, can make libvips ask for about 39
GB (an 8192x1 source at `1300x0`), or answers 500
(https://git.eeqj.de/sneak/pixa/issues/283)
# Completed Steps # Completed Steps
- 2026-10-16 a size with one side `0` keeps the side computed from the source's
proportions at 1 or more (closes #283): it was rounded down, so for a thin
source it came out at 0, and libvips then made the size square. An 8192x1
source at `100x0` came out at 100x100, and at `1300x0` with `fit=cover` made
libvips ask for about 39 GB at once; an 8192x2 source at `100x0` answered 500.
`fit=contain` and `fit=inside` work both sides out again from the smaller of
the two scales, and keep each at 1 or more too, so a 1x8192 source at `0x100`
no longer answers 500 there. An 8192x1 source at `1300x0` now comes out at
1300x1, and the process running it alone peaked at about 34 MiB resident. With
`fit=outside` the image is scaled to cover the size, so that source at `100x0`
comes out at 8192x1.
- 2026-10-15 a thin source requested with `fit=cover` no longer makes libvips - 2026-10-15 a thin source requested with `fit=cover` no longer makes libvips
build a huge image (closes #282): libvips' thumbnail scaled the whole source build a huge image (closes #282): libvips' thumbnail scaled the whole source
to cover the requested size and copied that into memory before it cut out the to cover the requested size and copied that into memory before it cut out the
@@ -1000,7 +1008,6 @@ GB (an 8192x1 source at `1300x0`), or answers 500
# Future Steps # Future Steps
- P2: security: per-origin rate limiting
- P2: HTTP response handling - P2: HTTP response handling
- Last-Modified headers - Last-Modified headers
- P2: configuration - P2: configuration
+14 -9
View File
@@ -311,8 +311,10 @@ func (p *ImageProcessor) Process(
// targetDimensions calculates the output dimensions for a requested size, // targetDimensions calculates the output dimensions for a requested size,
// scaling proportionally when only one dimension is given and keeping the // scaling proportionally when only one dimension is given and keeping the
// original dimensions when both are zero. A side scaled proportionally can // original dimensions when both are zero. A side scaled proportionally is
// come out above MaxOutputDimension, which is ErrOutputTooLarge. // rounded down but kept at 1 or more, as for a thin image it would round
// down to 0, a side of no pixels. It can come out above MaxOutputDimension,
// which is ErrOutputTooLarge.
func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) { func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) {
width, height := size.Width, size.Height width, height := size.Width, size.Height
@@ -322,10 +324,10 @@ func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) {
return origWidth, origHeight, nil return origWidth, origHeight, nil
case width == 0: case width == 0:
// Only height specified: calculate width proportionally // Only height specified: calculate width proportionally
width = origWidth * height / origHeight width = max(1, origWidth*height/origHeight)
case height == 0: case height == 0:
// Only width specified: calculate height proportionally // Only width specified: calculate height proportionally
height = origHeight * width / origWidth height = max(1, origHeight*width/origWidth)
} }
if width > MaxOutputDimension || height > MaxOutputDimension { if width > MaxOutputDimension || height > MaxOutputDimension {
@@ -479,13 +481,16 @@ func (p *ImageProcessor) resize(
return cropCentreAndResize(img, width, height) return cropCentreAndResize(img, width, height)
case FitContain: case FitContain:
// Resize to fit within dimensions, maintaining aspect ratio // Resize to fit within dimensions, maintaining aspect ratio. Scaled
// by the smaller scale, the short side of a thin image can round
// down to 0, which libvips refuses as a width, so each side is at
// least 1.
imgW, imgH := img.Width(), img.Height() imgW, imgH := img.Width(), img.Height()
scaleW := float64(width) / float64(imgW) scaleW := float64(width) / float64(imgW)
scaleH := float64(height) / float64(imgH) scaleH := float64(height) / float64(imgH)
scale := min(scaleW, scaleH) scale := min(scaleW, scaleH)
newW := int(float64(imgW) * scale) newW := max(1, int(float64(imgW)*scale))
newH := int(float64(imgH) * scale) newH := max(1, int(float64(imgH)*scale))
return img.Thumbnail(newW, newH, vips.InterestingNone) return img.Thumbnail(newW, newH, vips.InterestingNone)
@@ -503,8 +508,8 @@ func (p *ImageProcessor) resize(
scaleW := float64(width) / float64(imgW) scaleW := float64(width) / float64(imgW)
scaleH := float64(height) / float64(imgH) scaleH := float64(height) / float64(imgH)
scale := min(scaleW, scaleH) scale := min(scaleW, scaleH)
newW := int(float64(imgW) * scale) newW := max(1, int(float64(imgW)*scale))
newH := int(float64(imgH) * scale) newH := max(1, int(float64(imgH)*scale))
return img.Thumbnail(newW, newH, vips.InterestingNone) return img.Thumbnail(newW, newH, vips.InterestingNone)