Scale both sides of a fit=cover image by the same amount (part of #282)
check / check (push) Waiting to run
check / check (push) Waiting to run
cropCentreAndResize cut the centre to whole source pixels and forced that to the requested size, so the two sides were scaled by different amounts: a 16x16 source at 100x60 came out squashed by 4%. It now widens the centre to whole source pixels and lets thumbnail with a centre crop scale that by the one amount next uses and cut the output from it. thumbnail copies the scaled image into memory, which is larger than the output by less than one scaled source pixel on each side. TODO.md says so. Model: opus-5-5
This commit is contained in:
@@ -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