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();
}