Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crop Image to an aspect ratio (with transform option?) #271

Open
jens-struct opened this issue Jan 11, 2025 · 7 comments
Open

Crop Image to an aspect ratio (with transform option?) #271

jens-struct opened this issue Jan 11, 2025 · 7 comments
Labels
enhancement New feature or request

Comments

@jens-struct
Copy link

jens-struct commented Jan 11, 2025

@zachleat This issue is a followup of: https://indieweb.social/@djenz/113782569330355460

Lets say we want to crop the following js api usecase to an aspect ratio of 1:1.

const stats = await Image('path/to/16x9.jpg', {
  widths: [80, 160, 320, 512, 768, 1024, 1200, 1440, 1600, 1720, 1920, 2560, 3200],
  formats: ['avif'],
});

We could resize path/to/16x9.jpg beforehand and provide the buffer as src for the image api:

const aspectRatio = '1:1';
const [ratioWidth, ratioHeight] = aspectRatio.split(':').map(item => parseInt(item));

const sharpInstance = Sharp('path/to/16x9.jpg');

const {
  width: originalWidth,
  height: originalHeight,
} = await sharpInstance.metadata();

let cropWidth = originalWidth;
let cropHeight = originalHeight;

cropHeight = Math.floor(originalWidth / ratioWidth * ratioHeight);

if (cropHeight > originalHeight) {
  cropWidth = Math.floor(originalHeight / ratioHeight * ratioWidth);
  cropHeight = originalHeight;
}

sharpInstance.resize({
    width: cropWidth,
    height: cropHeight,
    fit: Sharp.fit.cover,
    position: Sharp.strategy.entropy,
});

const src = await sharpInstance.toBuffer();

const stats = await Image(src, {
  widths: [80, 160, 320, 512, 768, 1024, 1200, 1440, 1600, 1720, 1920, 2560, 3200],
  formats: ['avif'],
});

This will work. But it has the disadvantage that it can not fully take advantage of the js api transformation cache. It would resize the original image each time you call this code (i.e. through a shortcode). With a lot of images this alone can take a long time, even though the follow up transformations are cached.

Now there is the transform option, lets say we use it for illustration purpose in the same way:

const aspectRatio = '1:1';
const [ratioWidth, ratioHeight] = aspectRatio.split(':').map(item => parseInt(item));

const sharpInstance = Sharp('path/to/16x9.jpg');

const {
  width: originalWidth,
  height: originalHeight,
} = await sharpInstance.metadata();

let cropWidth = originalWidth;
let cropHeight = originalHeight;

cropHeight = Math.floor(originalWidth / ratioWidth * ratioHeight);

if (cropHeight > originalHeight) {
  cropWidth = Math.floor(originalHeight / ratioHeight * ratioWidth);
  cropHeight = originalHeight;
}

const stats = await Image(src, {
  widths: [80, 160, 320, 512, 768, 1024, 1200, 1440, 1600, 1720, 1920, 2560, 3200],
  formats: ['avif'],
  transform: sharpInstance => sharpInstance.resize({
    width: cropWidth,
    height: cropHeight,
    fit: Sharp.fit.cover,
    position: Sharp.strategy.entropy,
 },
});

It wouldn't work, because it will ignore the widths property (size scale) now, as it will create 13 times an image with the same width/height -> cropWidth/cropHeight. As the transform option is run once per resize (per width of the widths size scale).

We could split it out into a loop, i.e.:

const stats = [];

for (const scaleWidth of [80, 160, 320, 512, 768, 1024, 1200, 1440, 1600, 1720, 1920, 2560, 3200]) {
  stats.push(await Image(src, {
    widths: [scaleWidth],
    formats: ['avif'],
    transform: sharpInstance => sharpInstance.resize({
      width: scaleWidth,
      height: Math.floor(scaleWidth / ratioWidth * ratioHeight),
      fit: Sharp.fit.cover,
      position: Sharp.strategy.entropy,
    };
  }));
}

This would only work if the src is used with one aspect ratio (cropping) throughout the whole codebase, as the output filename would be the same no matter the size differences introduced through the transform option (it is not taken into account by the 11ty img algorhythm that creates the filename hash). So different aspect ratio versions of the same src & width would overwrite each others out files.

crop option

To omit all the extra custom code, i wish we would had something like this:

const stats = await Image('path/to/16x9.jpg', {
  widths: [80, 160, 320, 512, 768, 1024, 1200, 1440, 1600, 1720, 1920, 2560, 3200],
  formats: ['avif'],
  crop: {
    aspectRatio: 1 / 1,
    position: Sharp.strategy.entropy,
});

Which does all the above automatically:

  • crop the provided src image to the biggest possible "cover" portion (once, for the original image)
  • calculate the cropping (height) for each width of the widths scale

transformOriginal option

Or maybe a transformOriginal option that is only run once for the original image provided through src, and not on every resize of the widths scale like the transform option now.

This might be better than the crop option, as it is more general and would enable more usecases than only the cropping. But it would also still require some custom code, which could maybe provided through an example in the 11ty docs.

Other solutions?

But maybe there are other (better) ways, i haven't thought of? Ideas?

Thanks!

@jens-struct jens-struct changed the title Crop Image to an aspect ratio with the transform option Crop Image to an aspect ratio (with transform option?) Jan 11, 2025
@zachleat
Copy link
Member

zachleat commented Jan 13, 2025

A ha — I see what you mean. What about passing the dimensions to the transform callback? Would that solve the iteration problem you’re referencing?

Something like:

 await eleventyImage("./test/exif-sample-large.jpg", {
    widths: [100, 300, 500],
    transform: function(sharp, stats) {
      sharp.resize({
        width: stats.width,
        height: stats.width,
        fit: Sharp.fit.cover,
        position: Sharp.strategy.entropy,
      });
    },
  });

@zachleat zachleat added the enhancement New feature or request label Jan 13, 2025
@jens-struct
Copy link
Author

@zachleat ah, yes! I also had this idea (because it would enable a low effort quick fix), forgot to include it in the examples above 😅

But there would still be the problem mentioned above:

... the output filename would be the same no matter the size differences introduced through the transform option ... So different aspect ratio versions of the same src & width would overwrite each others out files.

Maybe together with a new transformName option, that would be a hook into the filename part that is currently based on the width?

@zachleat
Copy link
Member

zachleat commented Jan 13, 2025

@jens-struct the current v6.0.0 has this code to regenerate file names (and metadata info) after the transform runs:

stat = this.getStat(stat.format, dims.width, dims.height);

@zachleat
Copy link
Member

I would also note this 6.0.0 code that uses the transform callback function name transform: function myTransformName() as an input to the memory cache key computation:

return "<fn>" + (value.name || "");

@jens-struct
Copy link
Author

Oh, ok. I didn't saw this, thanks for the hint. Then simply adding stats as a second param should probably do the trick!

@jens-struct
Copy link
Author

@zachleat Would it be helpful, if i create a PR for this small change?

@zachleat
Copy link
Member

If you’d like to contribute, I’d be happy to review and merge!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants