feat: Add image cropping functionality with a new component, dialog, and canvas utilities.
All checks were successful
Deploy to Production / test (push) Successful in 40s
All checks were successful
Deploy to Production / test (push) Successful in 40s
This commit is contained in:
89
web/src/lib/canvasUtils.ts
Normal file
89
web/src/lib/canvasUtils.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export const createImage = (url: string): Promise<HTMLImageElement> =>
|
||||
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 on CodeSandbox
|
||||
image.src = url
|
||||
})
|
||||
|
||||
export function getRadianAngle(degreeValue: number) {
|
||||
return (degreeValue * Math.PI) / 180
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the new bounding area of a rotated rectangle.
|
||||
*/
|
||||
export function rotateSize(width: number, height: number, rotation: 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),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
|
||||
*/
|
||||
export async function getCroppedImg(
|
||||
imageSrc: string,
|
||||
pixelCrop: { x: number; y: number; width: number; height: number },
|
||||
rotation = 0,
|
||||
flip = { horizontal: false, vertical: false }
|
||||
): Promise<Blob | null> {
|
||||
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)
|
||||
|
||||
// croppedAreaPixels values are bounding box relative
|
||||
// extract the cropped image using these values
|
||||
const data = ctx.getImageData(
|
||||
pixelCrop.x,
|
||||
pixelCrop.y,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height
|
||||
)
|
||||
|
||||
// set canvas width to final desired crop size - this will clear existing context
|
||||
canvas.width = pixelCrop.width
|
||||
canvas.height = pixelCrop.height
|
||||
|
||||
// paste generated rotate image at the top left corner
|
||||
ctx.putImageData(data, 0, 0)
|
||||
|
||||
// As a Blob
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((file) => {
|
||||
resolve(file)
|
||||
}, 'image/jpeg')
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user