0

I'm trying to rotate an image (e.g., by 45 degrees) using sharp without increasing the canvas size or reducing the content size (i.e. non-transparent pixels).

Currently, when I rotate the image, the canvas is extended (or the pixeldata is shrinked) to fit the entire rotated image to the canvas so the original content appears scaled down relative to the canvas.

Here’s my current code:

import got from "got";
import sharp from "sharp";

export default async function generateImage({
  image,
  values,
  canvasSize,
}) {
  const sourceImageBuffer = await got(image).buffer();

  let processedImage = sharp(sourceImageBuffer)
    .resize({
      width: canvasSize,
      height: canvasSize,
      fit: "cover", // Ensures image covers the canvas
      position: "center",
    })
    .rotate(values.angle)  // Rotate by user-specified degrees
    .toBuffer();

  return await processedImage;
}

Desired Outcome: Rotate the image in place, keeping the dimensions fixed (e.g., 1000x1000). If the rotated image goes out of bounds the parts that exceed the canvas should get cropped.

My Attempts: I tried using extend before and extract() after rotation but then I wasn’t able to get the right crop anymore.

Update

After some more trial and error I ended up with the following, which seems to work, but I’m not sure if the math is right (i.e. should I use Math.round, Math.floor or Math.ceil?) Also I’m wondering if the image loses quality this way.

import got from "got";
import sharp from "sharp";

function calculateRotatedCanvasSize(width, height, angleInDegrees) {
  const angleInRadians = (angleInDegrees * Math.PI) / 180;
  const newWidth =
    Math.abs(width * Math.cos(angleInRadians)) +
    Math.abs(height * Math.sin(angleInRadians));
  const newHeight =
    Math.abs(width * Math.sin(angleInRadians)) +
    Math.abs(height * Math.cos(angleInRadians));

  return {
    newWidth: Math.round(newWidth),
    newHeight: Math.round(newHeight),
  };
}

export default async function generateImage({ image, angle, canvasSize }) {
  const sourceImageBuffer = await got(image).buffer();

  const { newWidth, newHeight } = calculateRotatedCanvasSize(
    canvasSize,
    canvasSize,
    angle
  );

  return await sharp(sourceImageBuffer)
    .resize({
      width: canvasSize,
      height: canvasSize,
      fit: "cover", // This ensures that the image covers the canvas (like object-fit: cover)
      position: "center", // Center the image while cropping
    })
    .rotate(angle)
    .extract({
      left: Math.round((newWidth - canvasSize) / 2),
      top: Math.round((newHeight - canvasSize) / 2),
      width: canvasSize,
      height: canvasSize,
    })
    .toBuffer();
}

1 Answer 1

0

If you want to maintain a transparent background and image size, try replacing your .rotate with this

 .rotate(values.angle, { background: { r: 0, g: 0, b: 0, alpha: 0 } }) // Rotate with transparent background
    .extract({
      left: 0,
      top: 0,
      width: canvasSize,
      height: canvasSize,
    }) // Crop to ensure it fits the canvas size
    .toBuffer();

Another good thing is move the await from here:

 return await processedImage;

to here:

 let processedImage = await sharp(sourceImageBuffer)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.