Crop the centre first for fit=cover (closes #282)

libvips' thumbnail with a centre crop scales the whole source to cover
the requested size and copies that into memory before it crops: 8192x64
at 8192x8192 made it allocate about 24 GiB, and 8192x1 failed with 500.
resize now cuts the centre of the source to the requested size's
proportions first and resizes that, so no image larger than the source
or the output is made. The crop is the same centre as before. TODO.md
says so.

Model: opus-5-5
This commit is contained in:
2026-10-15 22:33:31 +00:00
parent 8038712ca6
commit e664a37e0d
2 changed files with 43 additions and 2 deletions
+33 -2
View File
@@ -475,8 +475,8 @@ func (p *ImageProcessor) resize(
) error {
switch fit {
case FitCover, "":
// Resize and crop to fill exact dimensions (default)
return img.Thumbnail(width, height, vips.InterestingCentre)
// Crop the centre and resize to fill exact dimensions (default)
return cropCentreAndResize(img, width, height)
case FitContain:
// Resize to fit within dimensions, maintaining aspect ratio
@@ -530,6 +530,37 @@ func (p *ImageProcessor) resize(
}
}
// cropCentreAndResize cuts img to its centre, the largest area it holds with
// the proportions of width x height, and resizes that to width x height.
// This is the crop libvips' thumbnail makes with InterestingCentre, but
// libvips scales the whole image to cover the size and copies the result
// into memory before it crops, and for a thin image that is far larger than
// the image or the output: 8192x64 at 8192x8192 is 1048576x8192, about 24
// GiB. Cropping first makes no image larger than img or the output.
func cropCentreAndResize(img *vips.ImageRef, width, height int) error {
imgW, imgH := img.Width(), img.Height()
// An image wider than the requested proportions keeps its height and
// loses width, any other keeps its width and loses height; the side
// shortened is rounded to the nearest pixel and is at least 1
cropW, cropH := imgW, imgH
if imgW*height > imgH*width {
cropW = max(1, (imgH*width+height/2)/height)
} else {
cropH = max(1, (imgW*height+width/2)/width)
}
// Half of what is cut comes off each side
left, top := (imgW-cropW)/2, (imgH-cropH)/2 //nolint:mnd // half the cut
err := img.ExtractArea(left, top, cropW, cropH)
if err != nil {
return err
}
return img.ThumbnailWithSize(width, height, vips.InterestingNone, vips.SizeForce)
}
const defaultQuality = 85
// encode encodes an image to the specified format. govips gives pixa no way