From 5e1aceccd1acd948e7b6a48069f1fe213aa743bc Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Sun, 9 Mar 2025 02:35:30 +0000 Subject: [PATCH] feat: crop png --- .../tools/image/png/crop/crop.service.test.ts | 6 + src/pages/tools/image/png/crop/index.tsx | 216 ++++++++++++++++++ src/pages/tools/image/png/crop/meta.ts | 12 + src/pages/tools/image/png/crop/service.ts | 0 4 files changed, 234 insertions(+) create mode 100644 src/pages/tools/image/png/crop/crop.service.test.ts create mode 100644 src/pages/tools/image/png/crop/index.tsx create mode 100644 src/pages/tools/image/png/crop/meta.ts create mode 100644 src/pages/tools/image/png/crop/service.ts diff --git a/src/pages/tools/image/png/crop/crop.service.test.ts b/src/pages/tools/image/png/crop/crop.service.test.ts new file mode 100644 index 0000000..8516196 --- /dev/null +++ b/src/pages/tools/image/png/crop/crop.service.test.ts @@ -0,0 +1,6 @@ +import { expect, describe, it } from 'vitest'; +// import { } from './service'; +// +// describe('crop', () => { +// +// }) diff --git a/src/pages/tools/image/png/crop/index.tsx b/src/pages/tools/image/png/crop/index.tsx new file mode 100644 index 0000000..4385588 --- /dev/null +++ b/src/pages/tools/image/png/crop/index.tsx @@ -0,0 +1,216 @@ +import { Box } from '@mui/material'; +import React, { useState } from 'react'; +import * as Yup from 'yup'; +import ToolFileInput from '@components/input/ToolFileInput'; +import ToolFileResult from '@components/result/ToolFileResult'; +import { GetGroupsType } from '@components/options/ToolOptions'; +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' +}; + +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') +}); + +export default function CropPng({ title }: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + + const compute = (optionsValues: typeof initialValues, input: any) => { + 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, { + type: 'image/png' + }); + setResult(newFile); + } + }, 'image/png'); + }; + + processImage(input, x, y, width, height, isCircular); + }; + + const getGroups: GetGroupsType = ({ + values, + updateField + }) => [ + { + title: 'Crop Position and Size', + component: ( + + updateField('xPosition', val)} + description={'X position (in pixels)'} + inputProps={{ + 'data-testid': 'x-position-input', + type: 'number', + min: 0 + }} + /> + updateField('yPosition', val)} + description={'Y position (in pixels)'} + inputProps={{ + 'data-testid': 'y-position-input', + type: 'number', + min: 0 + }} + /> + updateField('cropWidth', val)} + description={'Crop width (in pixels)'} + inputProps={{ + 'data-testid': 'crop-width-input', + type: 'number', + min: 1 + }} + /> + updateField('cropHeight', val)} + description={'Crop height (in pixels)'} + inputProps={{ + 'data-testid': 'crop-height-input', + type: 'number', + min: 1 + }} + /> + + ) + }, + { + title: 'Crop Shape', + component: ( + + updateField('cropShape', 'rectangular')} + checked={values.cropShape == 'rectangular'} + description={'Crop a rectangular fragment from a PNG.'} + title={'Rectangular Crop Shape'} + /> + updateField('cropShape', 'circular')} + checked={values.cropShape == 'circular'} + description={'Crop a circular fragment from a PNG.'} + title={'Circular Crop Shape'} + /> + + ) + } + ]; + + return ( + + } + resultComponent={ + + } + toolInfo={{ + title: 'Crop PNG Image', + description: + 'This tool allows you to crop a PNG image by specifying the position, size, and shape of the crop area. You can choose between rectangular or circular cropping.' + }} + /> + ); +} diff --git a/src/pages/tools/image/png/crop/meta.ts b/src/pages/tools/image/png/crop/meta.ts new file mode 100644 index 0000000..d10b27b --- /dev/null +++ b/src/pages/tools/image/png/crop/meta.ts @@ -0,0 +1,12 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('png', { + name: 'Crop', + path: 'crop', + icon: 'mdi:crop', // Iconify icon as a string + description: 'A tool to crop images with precision and ease.', + shortDescription: 'Crop images quickly.', + keywords: ['crop', 'image', 'edit', 'resize', 'trim'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/image/png/crop/service.ts b/src/pages/tools/image/png/crop/service.ts new file mode 100644 index 0000000..e69de29