2025-03-09 02:35:30 +00:00
import { Box } from '@mui/material' ;
import React , { useState } from 'react' ;
import * as Yup from 'yup' ;
2025-03-26 03:48:45 +00:00
import ToolImageInput from '@components/input/ToolImageInput' ;
2025-03-09 02:35:30 +00:00
import ToolFileResult from '@components/result/ToolFileResult' ;
2025-03-09 03:58:04 +00:00
import { GetGroupsType , UpdateField } from '@components/options/ToolOptions' ;
2025-03-09 02:35:30 +00:00
import TextFieldWithDesc from '@components/options/TextFieldWithDesc' ;
import ToolContent from '@components/ToolContent' ;
import { ToolComponentProps } from '@tools/defineTool' ;
import SimpleRadio from '@components/options/SimpleRadio' ;
const initialValues = {
xPosition : '0' ,
yPosition : '0' ,
cropWidth : '100' ,
cropHeight : '100' ,
cropShape : 'rectangular' as 'rectangular' | 'circular'
} ;
2025-03-09 03:58:04 +00:00
type InitialValuesType = typeof initialValues ;
2025-03-09 02:35:30 +00:00
const validationSchema = Yup . object ( {
xPosition : Yup.number ( )
. min ( 0 , 'X position must be positive' )
. required ( 'X position is required' ) ,
yPosition : Yup.number ( )
. min ( 0 , 'Y position must be positive' )
. required ( 'Y position is required' ) ,
cropWidth : Yup.number ( )
. min ( 1 , 'Width must be at least 1px' )
. required ( 'Width is required' ) ,
cropHeight : Yup.number ( )
. min ( 1 , 'Height must be at least 1px' )
. required ( 'Height is required' )
} ) ;
2025-04-02 20:48:00 +00:00
export default function CropImage ( { title } : ToolComponentProps ) {
2025-03-09 02:35:30 +00:00
const [ input , setInput ] = useState < File | null > ( null ) ;
const [ result , setResult ] = useState < File | null > ( null ) ;
2025-03-09 03:58:04 +00:00
const compute = ( optionsValues : InitialValuesType , input : any ) = > {
2025-03-09 02:35:30 +00:00
if ( ! input ) return ;
const { xPosition , yPosition , cropWidth , cropHeight , cropShape } =
optionsValues ;
const x = parseInt ( xPosition ) ;
const y = parseInt ( yPosition ) ;
const width = parseInt ( cropWidth ) ;
const height = parseInt ( cropHeight ) ;
const isCircular = cropShape === 'circular' ;
const processImage = async (
file : File ,
x : number ,
y : number ,
width : number ,
height : number ,
isCircular : boolean
) = > {
// Create source canvas
const sourceCanvas = document . createElement ( 'canvas' ) ;
const sourceCtx = sourceCanvas . getContext ( '2d' ) ;
if ( sourceCtx == null ) return ;
// Create destination canvas
const destCanvas = document . createElement ( 'canvas' ) ;
const destCtx = destCanvas . getContext ( '2d' ) ;
if ( destCtx == null ) return ;
// Load image
const img = new Image ( ) ;
img . src = URL . createObjectURL ( file ) ;
await img . decode ( ) ;
// Set source canvas dimensions
sourceCanvas . width = img . width ;
sourceCanvas . height = img . height ;
// Draw original image on source canvas
sourceCtx . drawImage ( img , 0 , 0 ) ;
// Set destination canvas dimensions to crop size
destCanvas . width = width ;
destCanvas . height = height ;
if ( isCircular ) {
// For circular crop
destCtx . beginPath ( ) ;
// Create a circle with center at half width/height and radius of half the smaller dimension
const radius = Math . min ( width , height ) / 2 ;
destCtx . arc ( width / 2 , height / 2 , radius , 0 , Math . PI * 2 ) ;
destCtx . closePath ( ) ;
destCtx . clip ( ) ;
// Draw the cropped portion centered in the circle
destCtx . drawImage ( img , x , y , width , height , 0 , 0 , width , height ) ;
} else {
// For rectangular crop, simply draw the specified region
destCtx . drawImage ( img , x , y , width , height , 0 , 0 , width , height ) ;
}
// Convert canvas to blob and create file
destCanvas . toBlob ( ( blob ) = > {
if ( blob ) {
const newFile = new File ( [ blob ] , file . name , {
2025-04-02 20:48:00 +00:00
type : file . type
2025-03-09 02:35:30 +00:00
} ) ;
setResult ( newFile ) ;
}
2025-04-02 20:48:00 +00:00
} , file . type ) ;
2025-03-09 02:35:30 +00:00
} ;
processImage ( input , x , y , width , height , isCircular ) ;
} ;
2025-03-09 03:58:04 +00:00
const handleCropChange =
( values : InitialValuesType , updateField : UpdateField < InitialValuesType > ) = >
(
position : { x : number ; y : number } ,
size : { width : number ; height : number }
) = > {
updateField ( 'xPosition' , position . x . toString ( ) ) ;
updateField ( 'yPosition' , position . y . toString ( ) ) ;
updateField ( 'cropWidth' , size . width . toString ( ) ) ;
updateField ( 'cropHeight' , size . height . toString ( ) ) ;
} ;
2025-03-09 02:35:30 +00:00
2025-03-09 03:58:04 +00:00
const getGroups : GetGroupsType < InitialValuesType > = ( {
2025-03-09 02:35:30 +00:00
values ,
updateField
} ) = > [
{
title : 'Crop Position and Size' ,
component : (
< Box >
< TextFieldWithDesc
value = { values . xPosition }
onOwnChange = { ( val ) = > updateField ( 'xPosition' , val ) }
description = { 'X position (in pixels)' }
inputProps = { {
'data-testid' : 'x-position-input' ,
type : 'number' ,
min : 0
} }
/ >
< TextFieldWithDesc
value = { values . yPosition }
onOwnChange = { ( val ) = > updateField ( 'yPosition' , val ) }
description = { 'Y position (in pixels)' }
inputProps = { {
'data-testid' : 'y-position-input' ,
type : 'number' ,
min : 0
} }
/ >
< TextFieldWithDesc
value = { values . cropWidth }
onOwnChange = { ( val ) = > updateField ( 'cropWidth' , val ) }
description = { 'Crop width (in pixels)' }
inputProps = { {
'data-testid' : 'crop-width-input' ,
type : 'number' ,
min : 1
} }
/ >
< TextFieldWithDesc
value = { values . cropHeight }
onOwnChange = { ( val ) = > updateField ( 'cropHeight' , val ) }
description = { 'Crop height (in pixels)' }
inputProps = { {
'data-testid' : 'crop-height-input' ,
type : 'number' ,
min : 1
} }
/ >
< / Box >
)
} ,
{
title : 'Crop Shape' ,
component : (
< Box >
< SimpleRadio
onClick = { ( ) = > updateField ( 'cropShape' , 'rectangular' ) }
checked = { values . cropShape == 'rectangular' }
2025-04-02 20:48:00 +00:00
description = { 'Crop a rectangular fragment from an image.' }
2025-03-09 02:35:30 +00:00
title = { 'Rectangular Crop Shape' }
/ >
< SimpleRadio
onClick = { ( ) = > updateField ( 'cropShape' , 'circular' ) }
checked = { values . cropShape == 'circular' }
2025-04-02 20:48:00 +00:00
description = { 'Crop a circular fragment from an image.' }
2025-03-09 02:35:30 +00:00
title = { 'Circular Crop Shape' }
/ >
< / Box >
)
}
] ;
2025-03-09 03:58:04 +00:00
const renderCustomInput = (
values : InitialValuesType ,
updateField : UpdateField < InitialValuesType >
) = > (
2025-03-26 03:48:45 +00:00
< ToolImageInput
2025-03-09 03:58:04 +00:00
value = { input }
onChange = { setInput }
2025-04-02 20:48:00 +00:00
accept = { [ 'image/*' ] }
title = { 'Input image' }
2025-03-09 03:58:04 +00:00
showCropOverlay = { ! ! input }
cropShape = { values . cropShape as 'rectangular' | 'circular' }
cropPosition = { {
x : parseInt ( values . xPosition || '0' ) ,
y : parseInt ( values . yPosition || '0' )
} }
cropSize = { {
width : parseInt ( values . cropWidth || '100' ) ,
height : parseInt ( values . cropHeight || '100' )
} }
onCropChange = { handleCropChange ( values , updateField ) }
/ >
) ;
2025-03-09 02:35:30 +00:00
return (
< ToolContent
title = { title }
initialValues = { initialValues }
getGroups = { getGroups }
compute = { compute }
input = { input }
validationSchema = { validationSchema }
2025-03-09 03:58:04 +00:00
renderCustomInput = { renderCustomInput }
2025-03-09 02:35:30 +00:00
resultComponent = {
2025-04-02 20:48:00 +00:00
< ToolFileResult title = { 'Cropped image' } value = { result } / >
2025-03-09 02:35:30 +00:00
}
toolInfo = { {
2025-04-02 20:48:00 +00:00
title : 'Crop Image' ,
2025-03-09 02:35:30 +00:00
description :
2025-04-02 20:48:00 +00:00
'This tool allows you to crop an image by specifying the position, size, and shape of the crop area. You can choose between rectangular or circular cropping.'
2025-03-09 02:35:30 +00:00
} }
/ >
) ;
}