From c7f5ddad81b396e9c805f27bab836b86069424c8 Mon Sep 17 00:00:00 2001 From: faisolavolut Date: Tue, 18 Feb 2025 07:27:34 +0700 Subject: [PATCH] feat: add image cropping utility with rotation support and update package.json --- package.json | 1 + utils/cropImage.ts | 107 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 utils/cropImage.ts diff --git a/package.json b/package.json index c9ed88a..1bbf30e 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "next": "15.0.3", "react-chartjs-2": "^5.3.0", "react-colorful": "^5.6.1", + "react-easy-crop": "^5.2.0", "react-icons": "^5.3.0", "react-resizable": "^3.0.5", "react-resizable-panels": "^2.1.7", diff --git a/utils/cropImage.ts b/utils/cropImage.ts new file mode 100644 index 0000000..5834c22 --- /dev/null +++ b/utils/cropImage.ts @@ -0,0 +1,107 @@ +export const createImage = (url: string): Promise => + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener("load", () => resolve(image)); + image.addEventListener("error", (error) => reject(error)); + image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues + image.src = url; + }); + +export function getRadianAngle(degreeValue: number): number { + return (degreeValue * Math.PI) / 180; +} + +/** + * Returns the new bounding area of a rotated rectangle. + */ +export function rotateSize( + width: number, + height: number, + rotation: number +): { width: number; height: number } { + const rotRad = getRadianAngle(rotation); + + return { + width: + Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height), + height: + Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height), + }; +} + +/** + * Crops and rotates an image + */ +export default async function getCroppedImg( + imageSrc: string, + pixelCrop: { x: number; y: number; width: number; height: number }, + rotation: number = 0, + flip: { horizontal: boolean; vertical: boolean } = { + horizontal: false, + vertical: false, + } +): Promise { + const image = await createImage(imageSrc); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (!ctx) { + return null; + } + + const rotRad = getRadianAngle(rotation); + + // Calculate bounding box of the rotated image + const { width: bBoxWidth, height: bBoxHeight } = rotateSize( + image.width, + image.height, + rotation + ); + + // Set canvas size to match the bounding box + canvas.width = bBoxWidth; + canvas.height = bBoxHeight; + + // Translate canvas context to a central location to allow rotating and flipping around the center + ctx.translate(bBoxWidth / 2, bBoxHeight / 2); + ctx.rotate(rotRad); + ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1); + ctx.translate(-image.width / 2, -image.height / 2); + + // Draw rotated image + ctx.drawImage(image, 0, 0); + + const croppedCanvas = document.createElement("canvas"); + const croppedCtx = croppedCanvas.getContext("2d"); + + if (!croppedCtx) { + return null; + } + + // Set the size of the cropped canvas + croppedCanvas.width = pixelCrop.width; + croppedCanvas.height = pixelCrop.height; + + // Draw the cropped image onto the new canvas + croppedCtx.drawImage( + canvas, + pixelCrop.x, + pixelCrop.y, + pixelCrop.width, + pixelCrop.height, + 0, + 0, + pixelCrop.width, + pixelCrop.height + ); + + return new Promise((resolve) => { + croppedCanvas.toBlob((file) => { + if (file) { + resolve(URL.createObjectURL(file)); + } else { + resolve(null); + } + }, "image/jpeg"); + }); +}