mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
chore: move from png to image-generic
This commit is contained in:
200
src/pages/tools/image/generic/change-opacity/index.tsx
Normal file
200
src/pages/tools/image/generic/change-opacity/index.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState } from 'react';
|
||||
import ToolImageInput from '@components/input/ToolImageInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { changeOpacity } from './service';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { updateNumberField } from '@utils/string';
|
||||
import { Box } from '@mui/material';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
|
||||
type InitialValuesType = {
|
||||
opacity: number;
|
||||
mode: 'solid' | 'gradient';
|
||||
gradientType: 'linear' | 'radial';
|
||||
gradientDirection: 'left-to-right' | 'inside-out';
|
||||
areaLeft: number;
|
||||
areaTop: number;
|
||||
areaWidth: number;
|
||||
areaHeight: number;
|
||||
};
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
opacity: 0.5,
|
||||
mode: 'solid',
|
||||
gradientType: 'linear',
|
||||
gradientDirection: 'left-to-right',
|
||||
areaLeft: 0,
|
||||
areaTop: 0,
|
||||
areaWidth: 100,
|
||||
areaHeight: 100
|
||||
};
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Semi-transparent PNG',
|
||||
description: 'Make an image 50% transparent',
|
||||
sampleOptions: {
|
||||
opacity: 0.5,
|
||||
mode: 'solid',
|
||||
gradientType: 'linear',
|
||||
gradientDirection: 'left-to-right',
|
||||
areaLeft: 0,
|
||||
areaTop: 0,
|
||||
areaWidth: 100,
|
||||
areaHeight: 100
|
||||
},
|
||||
sampleResult: ''
|
||||
},
|
||||
{
|
||||
title: 'Slightly Faded PNG',
|
||||
description: 'Create a subtle transparency effect',
|
||||
sampleOptions: {
|
||||
opacity: 0.8,
|
||||
mode: 'solid',
|
||||
gradientType: 'linear',
|
||||
gradientDirection: 'left-to-right',
|
||||
areaLeft: 0,
|
||||
areaTop: 0,
|
||||
areaWidth: 100,
|
||||
areaHeight: 100
|
||||
},
|
||||
sampleResult: ''
|
||||
},
|
||||
{
|
||||
title: 'Radial Gradient Opacity',
|
||||
description: 'Apply a radial gradient opacity effect',
|
||||
sampleOptions: {
|
||||
opacity: 0.8,
|
||||
mode: 'gradient',
|
||||
gradientType: 'radial',
|
||||
gradientDirection: 'inside-out',
|
||||
areaLeft: 25,
|
||||
areaTop: 25,
|
||||
areaWidth: 50,
|
||||
areaHeight: 50
|
||||
},
|
||||
sampleResult: ''
|
||||
}
|
||||
];
|
||||
|
||||
export default function ChangeOpacity({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = (values: InitialValuesType, input: any) => {
|
||||
if (input) {
|
||||
changeOpacity(input, values).then(setResult);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={'Input image'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult title={'Changed image'} value={result} />
|
||||
}
|
||||
initialValues={initialValues}
|
||||
// exampleCards={exampleCards}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Opacity Settings',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description="Set opacity between 0 (transparent) and 1 (opaque)"
|
||||
value={values.opacity}
|
||||
onOwnChange={(val) =>
|
||||
updateNumberField(val, 'opacity', updateField)
|
||||
}
|
||||
type="number"
|
||||
inputProps={{ step: 0.1, min: 0, max: 1 }}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('mode', 'solid')}
|
||||
checked={values.mode === 'solid'}
|
||||
description={'Set the same opacity level for all pixels'}
|
||||
title={'Apply Solid Opacity'}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('mode', 'gradient')}
|
||||
checked={values.mode === 'gradient'}
|
||||
description={'Change opacity in a gradient'}
|
||||
title={'Apply Gradient Opacity'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Gradient Options',
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('gradientType', 'linear')}
|
||||
checked={values.gradientType === 'linear'}
|
||||
description={'Linear opacity direction'}
|
||||
title={'Linear Gradient'}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('gradientType', 'radial')}
|
||||
checked={values.gradientType === 'radial'}
|
||||
description={'Radial opacity direction'}
|
||||
title={'Radial Gradient'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Opacity Area',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description="Left position"
|
||||
value={values.areaLeft}
|
||||
onOwnChange={(val) =>
|
||||
updateNumberField(val, 'areaLeft', updateField)
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Top position"
|
||||
value={values.areaTop}
|
||||
onOwnChange={(val) =>
|
||||
updateNumberField(val, 'areaTop', updateField)
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Width"
|
||||
value={values.areaWidth}
|
||||
onOwnChange={(val) =>
|
||||
updateNumberField(val, 'areaWidth', updateField)
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Height"
|
||||
value={values.areaHeight}
|
||||
onOwnChange={(val) =>
|
||||
updateNumberField(val, 'areaHeight', updateField)
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
compute={compute}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/image/generic/change-opacity/meta.ts
Normal file
13
src/pages/tools/image/generic/change-opacity/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Change image Opacity',
|
||||
path: 'change-opacity',
|
||||
icon: 'material-symbols:opacity',
|
||||
description:
|
||||
'Easily adjust the transparency of your images. Simply upload your image, use the slider to set the desired opacity level between 0 (fully transparent) and 1 (fully opaque), and download the modified image.',
|
||||
shortDescription: 'Adjust transparency of images',
|
||||
keywords: ['opacity', 'transparency', 'png', 'alpha', 'jpg', 'jpeg', 'image'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
125
src/pages/tools/image/generic/change-opacity/service.ts
Normal file
125
src/pages/tools/image/generic/change-opacity/service.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
interface OpacityOptions {
|
||||
opacity: number;
|
||||
mode: 'solid' | 'gradient';
|
||||
gradientType: 'linear' | 'radial';
|
||||
gradientDirection: 'left-to-right' | 'inside-out';
|
||||
areaLeft: number;
|
||||
areaTop: number;
|
||||
areaWidth: number;
|
||||
areaHeight: number;
|
||||
}
|
||||
|
||||
export async function changeOpacity(
|
||||
file: File,
|
||||
options: OpacityOptions
|
||||
): Promise<File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Canvas context not supported'));
|
||||
return;
|
||||
}
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
if (options.mode === 'solid') {
|
||||
applySolidOpacity(ctx, img, options);
|
||||
} else {
|
||||
applyGradientOpacity(ctx, img, options);
|
||||
}
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const newFile = new File([blob], file.name, { type: file.type });
|
||||
resolve(newFile);
|
||||
} else {
|
||||
reject(new Error('Failed to generate image blob'));
|
||||
}
|
||||
}, file.type);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image'));
|
||||
img.src = event.target?.result as string;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function applySolidOpacity(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
img: HTMLImageElement,
|
||||
options: OpacityOptions
|
||||
) {
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.globalAlpha = options.opacity;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
}
|
||||
|
||||
function applyGradientOpacity(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
img: HTMLImageElement,
|
||||
options: OpacityOptions
|
||||
) {
|
||||
const { areaLeft, areaTop, areaWidth, areaHeight } = options;
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const gradient =
|
||||
options.gradientType === 'linear'
|
||||
? createLinearGradient(ctx, options)
|
||||
: createRadialGradient(ctx, options);
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(areaLeft, areaTop, areaWidth, areaHeight);
|
||||
}
|
||||
|
||||
function createLinearGradient(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options: OpacityOptions
|
||||
) {
|
||||
const { areaLeft, areaTop, areaWidth, areaHeight } = options;
|
||||
const gradient = ctx.createLinearGradient(
|
||||
areaLeft,
|
||||
areaTop,
|
||||
areaLeft + areaWidth,
|
||||
areaTop
|
||||
);
|
||||
gradient.addColorStop(0, `rgba(255,255,255,${options.opacity})`);
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
return gradient;
|
||||
}
|
||||
|
||||
function createRadialGradient(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options: OpacityOptions
|
||||
) {
|
||||
const { areaLeft, areaTop, areaWidth, areaHeight } = options;
|
||||
const centerX = areaLeft + areaWidth / 2;
|
||||
const centerY = areaTop + areaHeight / 2;
|
||||
const radius = Math.min(areaWidth, areaHeight) / 2;
|
||||
|
||||
const gradient = ctx.createRadialGradient(
|
||||
centerX,
|
||||
centerY,
|
||||
0,
|
||||
centerX,
|
||||
centerY,
|
||||
radius
|
||||
);
|
||||
|
||||
if (options.gradientDirection === 'inside-out') {
|
||||
gradient.addColorStop(0, `rgba(255,255,255,${options.opacity})`);
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
} else {
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,0)');
|
||||
gradient.addColorStop(1, `rgba(255,255,255,${options.opacity})`);
|
||||
}
|
||||
|
||||
return gradient;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { Buffer } from 'buffer';
|
||||
import path from 'path';
|
||||
import Jimp from 'jimp';
|
||||
|
||||
test.describe('Create transparent PNG', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/image-generic/create-transparent');
|
||||
});
|
||||
|
||||
//TODO check why failing
|
||||
// test('should make png color transparent', async ({ page }) => {
|
||||
// // Upload image
|
||||
// const fileInput = page.locator('input[type="file"]');
|
||||
// const imagePath = path.join(__dirname, 'test.png');
|
||||
// await fileInput?.setInputFiles(imagePath);
|
||||
//
|
||||
// await page.getByTestId('color-input').fill('#FF0000');
|
||||
//
|
||||
// // Click on download
|
||||
// const downloadPromise = page.waitForEvent('download');
|
||||
// await page.getByText('Save as').click();
|
||||
//
|
||||
// // Intercept and read downloaded PNG
|
||||
// const download = await downloadPromise;
|
||||
// const downloadStream = await download.createReadStream();
|
||||
//
|
||||
// const chunks = [];
|
||||
// for await (const chunk of downloadStream) {
|
||||
// chunks.push(chunk);
|
||||
// }
|
||||
// const fileContent = Buffer.concat(chunks);
|
||||
//
|
||||
// expect(fileContent.length).toBeGreaterThan(0);
|
||||
//
|
||||
// // Check that the first pixel is transparent
|
||||
// const image = await Jimp.read(fileContent);
|
||||
// const color = image.getPixelColor(0, 0);
|
||||
// expect(color).toBe(0);
|
||||
// });
|
||||
});
|
||||
138
src/pages/tools/image/generic/create-transparent/index.tsx
Normal file
138
src/pages/tools/image/generic/create-transparent/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolImageInput from '@components/input/ToolImageInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import ColorSelector from '@components/options/ColorSelector';
|
||||
import Color from 'color';
|
||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import { areColorsSimilar } from 'utils/color';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
|
||||
const initialValues = {
|
||||
fromColor: 'white',
|
||||
similarity: '10'
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
|
||||
export default function CreateTransparent({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
if (!input) return;
|
||||
const { fromColor, similarity } = optionsValues;
|
||||
|
||||
let fromRgb: [number, number, number];
|
||||
try {
|
||||
//@ts-ignore
|
||||
fromRgb = Color(fromColor).rgb().array();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
const processImage = async (
|
||||
file: File,
|
||||
fromColor: [number, number, number],
|
||||
similarity: number
|
||||
) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx == null) return;
|
||||
const img = new Image();
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data: Uint8ClampedArray = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const currentColor: [number, number, number] = [
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2]
|
||||
];
|
||||
if (areColorsSimilar(currentColor, fromColor, similarity)) {
|
||||
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const newFile = new File([blob], file.name, { type: 'image/png' });
|
||||
setResult(newFile);
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
processImage(input, fromRgb, Number(similarity));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<typeof initialValues> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'From color and similarity',
|
||||
component: (
|
||||
<Box>
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onColorChange={(val) => updateField('fromColor', val)}
|
||||
description={'Replace this color (from color)'}
|
||||
inputProps={{ 'data-testid': 'color-input' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onOwnChange={(val) => updateField('similarity', val)}
|
||||
description={
|
||||
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={'Input image'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Transparent PNG'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
input={input}
|
||||
validationSchema={validationSchema}
|
||||
toolInfo={{
|
||||
title: 'Create Transparent PNG',
|
||||
description:
|
||||
'This tool allows you to make specific colors in an image transparent. You can select the color to replace and adjust the similarity threshold to include similar colors.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/image/generic/create-transparent/meta.ts
Normal file
13
src/pages/tools/image/generic/create-transparent/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Create transparent PNG',
|
||||
path: 'create-transparent',
|
||||
icon: 'mdi:circle-transparent',
|
||||
shortDescription: 'Quickly make an image transparent',
|
||||
description:
|
||||
"World's simplest online Portable Network Graphics transparency maker. Just import your image in the editor on the left and you will instantly get a transparent PNG on the right. Free, quick, and very powerful. Import an image – get a transparent PNG.",
|
||||
keywords: ['create', 'transparent'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
BIN
src/pages/tools/image/generic/create-transparent/test.png
Normal file
BIN
src/pages/tools/image/generic/create-transparent/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
237
src/pages/tools/image/generic/crop/index.tsx
Normal file
237
src/pages/tools/image/generic/crop/index.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolImageInput from '@components/input/ToolImageInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { GetGroupsType, UpdateField } 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'
|
||||
};
|
||||
type InitialValuesType = typeof initialValues;
|
||||
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 CropImage({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = (optionsValues: InitialValuesType, 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: file.type
|
||||
});
|
||||
setResult(newFile);
|
||||
}
|
||||
}, file.type);
|
||||
};
|
||||
|
||||
processImage(input, x, y, width, height, isCircular);
|
||||
};
|
||||
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());
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
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'}
|
||||
description={'Crop a rectangular fragment from an image.'}
|
||||
title={'Rectangular Crop Shape'}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('cropShape', 'circular')}
|
||||
checked={values.cropShape == 'circular'}
|
||||
description={'Crop a circular fragment from an image.'}
|
||||
title={'Circular Crop Shape'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
const renderCustomInput = (
|
||||
values: InitialValuesType,
|
||||
updateField: UpdateField<InitialValuesType>
|
||||
) => (
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={'Input image'}
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
input={input}
|
||||
validationSchema={validationSchema}
|
||||
renderCustomInput={renderCustomInput}
|
||||
resultComponent={
|
||||
<ToolFileResult title={'Cropped image'} value={result} />
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'Crop Image',
|
||||
description:
|
||||
'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.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/pages/tools/image/generic/crop/meta.ts
Normal file
12
src/pages/tools/image/generic/crop/meta.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
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'))
|
||||
});
|
||||
@@ -1,5 +1,17 @@
|
||||
import { tool as resizeImage } from './resize/meta';
|
||||
import { tool as compressImage } from './compress/meta';
|
||||
import { tool as changeColors } from './change-colors/meta';
|
||||
import { tool as removeBackground } from './remove-background/meta';
|
||||
import { tool as cropImage } from './crop/meta';
|
||||
import { tool as changeOpacity } from './change-opacity/meta';
|
||||
import { tool as createTransparent } from './create-transparent/meta';
|
||||
|
||||
export const imageGenericTools = [resizeImage, compressImage, changeColors];
|
||||
export const imageGenericTools = [
|
||||
resizeImage,
|
||||
compressImage,
|
||||
removeBackground,
|
||||
cropImage,
|
||||
changeOpacity,
|
||||
changeColors,
|
||||
createTransparent
|
||||
];
|
||||
|
||||
88
src/pages/tools/image/generic/remove-background/index.tsx
Normal file
88
src/pages/tools/image/generic/remove-background/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolImageInput from '@components/input/ToolImageInput';
|
||||
import { removeBackground } from '@imgly/background-removal';
|
||||
|
||||
const initialValues = {};
|
||||
|
||||
const validationSchema = Yup.object({});
|
||||
|
||||
export default function RemoveBackgroundFromImage({
|
||||
title
|
||||
}: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
const compute = async (_optionsValues: typeof initialValues, input: any) => {
|
||||
if (!input) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
// Convert the input file to a Blob URL
|
||||
const inputUrl = URL.createObjectURL(input);
|
||||
|
||||
// Process the image with the background removal library
|
||||
const blob = await removeBackground(inputUrl, {
|
||||
progress: (progress) => {
|
||||
console.log(`Background removal progress: ${progress}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new file from the blob
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
input.name.replace(/\.[^/.]+$/, '') + '-no-bg.png',
|
||||
{
|
||||
type: 'image/png'
|
||||
}
|
||||
);
|
||||
|
||||
setResult(newFile);
|
||||
} catch (err) {
|
||||
console.error('Error removing background:', err);
|
||||
throw new Error(
|
||||
'Failed to remove background. Please try a different image or try again later.'
|
||||
);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
getGroups={null}
|
||||
compute={compute}
|
||||
input={input}
|
||||
validationSchema={validationSchema}
|
||||
inputComponent={
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={'Input Image'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Transparent PNG'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
loading={isProcessing}
|
||||
loadingText={'Removing background'}
|
||||
/>
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'Remove Background from Image',
|
||||
description:
|
||||
'This tool uses AI to automatically remove the background from your images, creating a transparent PNG. Perfect for product photos, profile pictures, and design assets.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
21
src/pages/tools/image/generic/remove-background/meta.ts
Normal file
21
src/pages/tools/image/generic/remove-background/meta.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Remove Background from Image',
|
||||
path: 'remove-background',
|
||||
icon: 'mdi:image-remove',
|
||||
description:
|
||||
"World's simplest online tool to remove backgrounds from images. Just upload your image and our AI-powered tool will automatically remove the background, giving you a transparent PNG. Perfect for product photos, profile pictures, and design assets.",
|
||||
shortDescription: 'Automatically remove backgrounds from images',
|
||||
keywords: [
|
||||
'remove',
|
||||
'background',
|
||||
'png',
|
||||
'transparent',
|
||||
'image',
|
||||
'ai',
|
||||
'jpg'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
Reference in New Issue
Block a user