Crop the centre before resizing for fit=cover (closes #282) #284
@@ -30,6 +30,18 @@ P2: security: per-origin rate limiting
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 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
|
||||
centre, so an 8192x64 source at 8192x8192 made it allocate about 24 GiB, and
|
||||
an 8192x1 source answered 500. The image processor now first cuts the centre
|
||||
of the source, widened to whole source pixels, and thumbnail scales and cuts
|
||||
only that, by the same amount on both sides as before. What it copies into
|
||||
memory is then larger than the output by less than one scaled source pixel on
|
||||
each side. With the runtime image's libvips and JPEG output at 8192x8192,
|
||||
resident memory peaked at about 242 MiB for the 8192x64 source and 431 MiB for
|
||||
the 8192x1 one. The output can sit a fraction of an output pixel from where it
|
||||
was.
|
||||
- 2026-10-15 a `/v1/image/` URL whose source path or source query holds a `#`,
|
||||
written as `%23`, is refused with 400 (closes #286): the source URL built from
|
||||
it ended at the `#` when fetched, so each text after it fetched the same
|
||||
@@ -46,16 +58,6 @@ P2: security: per-origin rate limiting
|
||||
way, and so is an IPv6 address without brackets, which needed a signature
|
||||
before. Underscores are kept because `allowlist_hosts` accepts host names
|
||||
holding them.
|
||||
- 2026-10-15 a thin source requested with `fit=cover` no longer makes libvips
|
||||
build a huge image (closes #282): libvips scaled the whole source to cover the
|
||||
requested size and copied that into memory before it cut out the centre, so an
|
||||
8192x64 source at 8192x8192 made it allocate about 24 GiB, and an 8192x1
|
||||
source answered 500. The image processor 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.
|
||||
With the runtime image's libvips and JPEG output at 8192x8192, resident memory
|
||||
peaked at about 44 MiB for the 8192x64 source, 40 MiB for the 8192x1 one, and
|
||||
117 MiB for a 4000x3000 one, which took 373 MiB before.
|
||||
- 2026-10-15 the README says how a source URL with a query is written into an
|
||||
image URL, and the image route accepts it (closes #255): the query follows the
|
||||
source path after `%3F`. pixa now reads the image URL's path decoded once, so
|
||||
|
||||
@@ -530,35 +530,36 @@ 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.
|
||||
// cropCentreAndResize scales img by the same amount on both sides, the
|
||||
// least that makes it cover width x height, and cuts the centre width x
|
||||
// height out of it, as libvips' thumbnail does with InterestingCentre.
|
||||
// thumbnail alone scales the whole image and copies the result into memory
|
||||
// before it cuts, and for a thin image that is far larger than the image or
|
||||
// the output: 8192x64 at 8192x8192 is 1048576x8192, about 24 GiB. So the
|
||||
// centre is cut from img first, widened to whole pixels of img, which adds
|
||||
// less than one pixel of img on each side, and thumbnail scales and cuts
|
||||
// only that.
|
||||
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
|
||||
// loses width down to imgH*width/height, any other keeps its width and
|
||||
// loses height down to imgW*height/width. Half of what is lost comes
|
||||
// off each side, rounded down to whole pixels, so the part kept holds
|
||||
// all of the centre.
|
||||
left, top := 0, 0
|
||||
if imgW*height > imgH*width {
|
||||
cropW = max(1, (imgH*width+height/2)/height)
|
||||
left = (imgW*height - imgH*width) / (2 * height) //nolint:mnd // each side
|
||||
} else {
|
||||
cropH = max(1, (imgW*height+width/2)/width)
|
||||
top = (imgH*width - imgW*height) / (2 * width) //nolint:mnd // each side
|
||||
}
|
||||
|
||||
// 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)
|
||||
err := img.ExtractArea(left, top, imgW-2*left, imgH-2*top)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return img.ThumbnailWithSize(width, height, vips.InterestingNone, vips.SizeForce)
|
||||
return img.Thumbnail(width, height, vips.InterestingCentre)
|
||||
}
|
||||
|
||||
const defaultQuality = 85
|
||||
|
||||
Reference in New Issue
Block a user