Crop the centre before resizing for fit=cover (closes #282) #284
@@ -30,6 +30,18 @@ P2: security: per-origin rate limiting
|
|||||||
|
|
||||||
# Completed Steps
|
# 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 `#`,
|
- 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
|
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
|
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
|
way, and so is an IPv6 address without brackets, which needed a signature
|
||||||
before. Underscores are kept because `allowlist_hosts` accepts host names
|
before. Underscores are kept because `allowlist_hosts` accepts host names
|
||||||
holding them.
|
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
|
- 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
|
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
|
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
|
// cropCentreAndResize scales img by the same amount on both sides, the
|
||||||
// the proportions of width x height, and resizes that to width x height.
|
// least that makes it cover width x height, and cuts the centre width x
|
||||||
// This is the crop libvips' thumbnail makes with InterestingCentre, but
|
// height out of it, as libvips' thumbnail does with InterestingCentre.
|
||||||
// libvips scales the whole image to cover the size and copies the result
|
// thumbnail alone scales the whole image and copies the result into memory
|
||||||
// into memory before it crops, and for a thin image that is far larger than
|
// before it cuts, and for a thin image that is far larger than the image or
|
||||||
// the image or the output: 8192x64 at 8192x8192 is 1048576x8192, about 24
|
// the output: 8192x64 at 8192x8192 is 1048576x8192, about 24 GiB. So the
|
||||||
// GiB. Cropping first makes no image larger than img or the output.
|
// 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 {
|
func cropCentreAndResize(img *vips.ImageRef, width, height int) error {
|
||||||
imgW, imgH := img.Width(), img.Height()
|
imgW, imgH := img.Width(), img.Height()
|
||||||
|
|
||||||
// An image wider than the requested proportions keeps its height and
|
// An image wider than the requested proportions keeps its height and
|
||||||
// loses width, any other keeps its width and loses height; the side
|
// loses width down to imgH*width/height, any other keeps its width and
|
||||||
// shortened is rounded to the nearest pixel and is at least 1
|
// loses height down to imgW*height/width. Half of what is lost comes
|
||||||
cropW, cropH := imgW, imgH
|
// 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 {
|
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 {
|
} 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
|
err := img.ExtractArea(left, top, imgW-2*left, imgH-2*top)
|
||||||
left, top := (imgW-cropW)/2, (imgH-cropH)/2 //nolint:mnd // half the cut
|
|
||||||
|
|
||||||
err := img.ExtractArea(left, top, cropW, cropH)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return img.ThumbnailWithSize(width, height, vips.InterestingNone, vips.SizeForce)
|
return img.Thumbnail(width, height, vips.InterestingCentre)
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultQuality = 85
|
const defaultQuality = 85
|
||||||
|
|||||||
Reference in New Issue
Block a user