lovell / sharp

High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.
https://sharp.pixelplumbing.com
Apache License 2.0
29.31k stars 1.3k forks source link

Resize with fit: "inside" sometimes creates 1px wrong output dimensions #4231

Closed mowolf closed 1 month ago

mowolf commented 1 month ago

Resize with fit: "inside" creates 1px wrong output dimensions

v0.33.5

Output of running npx envinfo --binaries --system --npmPackages=sharp --npmGlobalPackages=sharp:

  System:
    OS: macOS 15.0.1
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
    Memory: 338.12 MB / 32.00 GB
    Shell: 3.6.0 - /usr/local/bin/fish
  Binaries:
    Node: 20.10.0 - ~/.local/share/nvm/v20.10.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.5 - ~/.local/share/nvm/v20.10.0/bin/npm
    pnpm: 8.13.1 - ~/.local/share/nvm/v20.10.0/bin/pnpm
  npmPackages:
    sharp: ^0.33.5 => 0.33.5 

What are the steps to reproduce?

Apply the resizing operation { width: 1800, height: 1200, fit: "inside" } on an image of size 3279x6016 px.

This creates an image with a width of one pixel less than expected (=changes the aspect ratio without any need to do so).

The original aspect ratio is 3279/6016=0.545.

The resulting image that sharp creates is of size width: 653, height: 1200 with an aspect ratio of 653 px/1200 px =0.544

This does only happen for certain image dimensions. We used this code on over >1M images and only a few dimensions have this problem.

What is the expected behaviour?

The created image should be 654 px wide. Resizing an image with fit: "inside" should keep the aspect ratio (as close as possible to the original).

The limiting dimension here is the width. Calculating the down-scaling factor:

f = 1200 px/6016 px = 0.1994680851

Calculating the resulting dimensions:

expectedWidth = f * 3279 px = 654.05585106383 px = 654 px expectedHeight = f * 6016 px = 1200 px

Expected Aspect Ratio = 0.545

This is a small difference, but lead to a long bug hunt as our program computed the expected dimensions and these did not fit with the created image from sharp.

Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this problem

const sharp = require("sharp");

const main = async () => {
  const imgSharpObject = sharp("./example.jpeg");

  imgSharpObject.resize({ width: 1800, height: 1200, fit: "inside" });

  const { _, info } = await imgSharpObject.toBuffer({
    resolveWithObject: true,
  });

  console.log("Info", info);
};

main();

This logs:

{
  format: 'jpeg',
  width: 653,
   ...
}

Please provide sample image(s) that help explain this problem

https://github.com/user-attachments/assets/7aba2e9a-f0d6-4a8c-9214-0c650216e1e8

lovell commented 1 month ago

Hi, did you see the fastShrinkOnLoad option?

https://sharp.pixelplumbing.com/api-resize

Param Type Default Description
[options.fastShrinkOnLoad] Boolean true Take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern or round-down of an auto-scaled dimension.
mowolf commented 1 month ago

Thank you, I was blind! Disabling fastShrinkOnLoad fixes the issue as expected.