diff --git a/TODO.md b/TODO.md index 50dd5ba..870f934 100644 --- a/TODO.md +++ b/TODO.md @@ -26,10 +26,7 @@ resolved. The disk cache is now size-bounded with LRU eviction # Next Step -P1: security: a size with one side `0` whose side computed from the source's -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) +P2: security: per-origin rate limiting # Completed Steps @@ -46,6 +43,17 @@ GB (an 8192x1 source at `1300x0`), or answers 500 another way, such as with `%2F` in the path, whatever else the path holds, or with a user name and password. It also answers 400 for a source path holding a `..` segment, as `/v1/image/` refuses one. +- 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 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 @@ -1013,7 +1021,6 @@ GB (an 8192x1 source at `1300x0`), or answers 500 # Future Steps -- P2: security: per-origin rate limiting - P2: HTTP response handling - Last-Modified headers - P2: configuration diff --git a/internal/imageprocessor/imageprocessor.go b/internal/imageprocessor/imageprocessor.go index 716b9ad..0c315ea 100644 --- a/internal/imageprocessor/imageprocessor.go +++ b/internal/imageprocessor/imageprocessor.go @@ -311,8 +311,10 @@ func (p *ImageProcessor) Process( // targetDimensions calculates the output dimensions for a requested size, // scaling proportionally when only one dimension is given and keeping the -// original dimensions when both are zero. A side scaled proportionally can -// come out above MaxOutputDimension, which is ErrOutputTooLarge. +// original dimensions when both are zero. A side scaled proportionally is +// 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) { width, height := size.Width, size.Height @@ -322,10 +324,10 @@ func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) { return origWidth, origHeight, nil case width == 0: // Only height specified: calculate width proportionally - width = origWidth * height / origHeight + width = max(1, origWidth*height/origHeight) case height == 0: // Only width specified: calculate height proportionally - height = origHeight * width / origWidth + height = max(1, origHeight*width/origWidth) } if width > MaxOutputDimension || height > MaxOutputDimension { @@ -479,13 +481,16 @@ func (p *ImageProcessor) resize( return cropCentreAndResize(img, width, height) 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() scaleW := float64(width) / float64(imgW) scaleH := float64(height) / float64(imgH) scale := min(scaleW, scaleH) - newW := int(float64(imgW) * scale) - newH := int(float64(imgH) * scale) + newW := max(1, int(float64(imgW)*scale)) + newH := max(1, int(float64(imgH)*scale)) return img.Thumbnail(newW, newH, vips.InterestingNone) @@ -503,8 +508,8 @@ func (p *ImageProcessor) resize( scaleW := float64(width) / float64(imgW) scaleH := float64(height) / float64(imgH) scale := min(scaleW, scaleH) - newW := int(float64(imgW) * scale) - newH := int(float64(imgH) * scale) + newW := max(1, int(float64(imgW)*scale)) + newH := max(1, int(float64(imgH)*scale)) return img.Thumbnail(newW, newH, vips.InterestingNone)