mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-17 18:44:02 +01:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
889
package-lock.json
generated
889
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
|||||||
"@types/omggif": "^1.0.5",
|
"@types/omggif": "^1.0.5",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"jimp": "^0.22.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"morsee": "^1.0.9",
|
"morsee": "^1.0.9",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { ChangeEvent, useRef, useState } from 'react';
|
import React, { ChangeEvent, useRef, useState } from 'react';
|
||||||
import { Box, Stack, TextField } from '@mui/material';
|
import { Box, Stack, TextField, TextFieldProps } from '@mui/material';
|
||||||
import PaletteIcon from '@mui/icons-material/Palette';
|
import PaletteIcon from '@mui/icons-material/Palette';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
@@ -7,14 +7,15 @@ import { globalDescriptionFontSize } from '../../config/uiConfig';
|
|||||||
|
|
||||||
interface ColorSelectorProps {
|
interface ColorSelectorProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onColorChange: (val: string) => void;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColorSelector: React.FC<ColorSelectorProps> = ({
|
const ColorSelector: React.FC<ColorSelectorProps & TextFieldProps> = ({
|
||||||
value = '#ffffff',
|
value = '#ffffff',
|
||||||
onChange,
|
onColorChange,
|
||||||
description
|
description,
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [color, setColor] = useState<string>(value);
|
const [color, setColor] = useState<string>(value);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -22,7 +23,7 @@ const ColorSelector: React.FC<ColorSelectorProps> = ({
|
|||||||
const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const val = event.target.value;
|
const val = event.target.value;
|
||||||
setColor(val);
|
setColor(val);
|
||||||
onChange(val);
|
onColorChange(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,6 +33,7 @@ const ColorSelector: React.FC<ColorSelectorProps> = ({
|
|||||||
sx={{ backgroundColor: 'white' }}
|
sx={{ backgroundColor: 'white' }}
|
||||||
value={color}
|
value={color}
|
||||||
onChange={handleColorChange}
|
onChange={handleColorChange}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={() => inputRef.current?.click()}>
|
<IconButton onClick={() => inputRef.current?.click()}>
|
||||||
<PaletteIcon />
|
<PaletteIcon />
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import path from 'path';
|
||||||
|
import Jimp from 'jimp';
|
||||||
|
import { convertHexToRGBA } from '../../../../utils/color';
|
||||||
|
|
||||||
|
test.describe('Change colors in png', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/png/change-colors-in-png');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should change pixel color', 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('from-color-input').fill('#FF0000');
|
||||||
|
const toColor = '#0000FF';
|
||||||
|
await page.getByTestId('to-color-input').fill(toColor);
|
||||||
|
|
||||||
|
// 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(convertHexToRGBA(toColor));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import ColorSelector from '../../../../components/options/ColorSelector';
|
|||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
|
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
|
||||||
|
import { areColorsSimilar } from 'utils/color';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
fromColor: 'white',
|
fromColor: 'white',
|
||||||
@@ -55,28 +56,13 @@ export default function ChangeColorsInPng() {
|
|||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
const data: Uint8ClampedArray = imageData.data;
|
const data: Uint8ClampedArray = imageData.data;
|
||||||
|
|
||||||
const colorDistance = (
|
|
||||||
c1: [number, number, number],
|
|
||||||
c2: [number, number, number]
|
|
||||||
) => {
|
|
||||||
return Math.sqrt(
|
|
||||||
Math.pow(c1[0] - c2[0], 2) +
|
|
||||||
Math.pow(c1[1] - c2[1], 2) +
|
|
||||||
Math.pow(c1[2] - c2[2], 2)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const maxColorDistance = Math.sqrt(
|
|
||||||
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
|
|
||||||
);
|
|
||||||
const similarityThreshold = (similarity / 100) * maxColorDistance;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
const currentColor: [number, number, number] = [
|
const currentColor: [number, number, number] = [
|
||||||
data[i],
|
data[i],
|
||||||
data[i + 1],
|
data[i + 1],
|
||||||
data[i + 2]
|
data[i + 2]
|
||||||
];
|
];
|
||||||
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
|
if (areColorsSimilar(currentColor, fromColor, similarity)) {
|
||||||
data[i] = toColor[0]; // Red
|
data[i] = toColor[0]; // Red
|
||||||
data[i + 1] = toColor[1]; // Green
|
data[i + 1] = toColor[1]; // Green
|
||||||
data[i + 2] = toColor[2]; // Blue
|
data[i + 2] = toColor[2]; // Blue
|
||||||
@@ -125,13 +111,15 @@ export default function ChangeColorsInPng() {
|
|||||||
<Box>
|
<Box>
|
||||||
<ColorSelector
|
<ColorSelector
|
||||||
value={values.fromColor}
|
value={values.fromColor}
|
||||||
onChange={(val) => updateField('fromColor', val)}
|
onColorChange={(val) => updateField('fromColor', val)}
|
||||||
description={'Replace this color (from color)'}
|
description={'Replace this color (from color)'}
|
||||||
|
inputProps={{ 'data-testid': 'from-color-input' }}
|
||||||
/>
|
/>
|
||||||
<ColorSelector
|
<ColorSelector
|
||||||
value={values.toColor}
|
value={values.toColor}
|
||||||
onChange={(val) => updateField('toColor', val)}
|
onColorChange={(val) => updateField('toColor', val)}
|
||||||
description={'With this color (to color)'}
|
description={'With this color (to color)'}
|
||||||
|
inputProps={{ 'data-testid': 'to-color-input' }}
|
||||||
/>
|
/>
|
||||||
<TextFieldWithDesc
|
<TextFieldWithDesc
|
||||||
value={values.similarity}
|
value={values.similarity}
|
||||||
|
|||||||
BIN
src/pages/image/png/change-colors-in-png/test.png
Normal file
BIN
src/pages/image/png/change-colors-in-png/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,72 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import path from 'path';
|
||||||
|
import Jimp from 'jimp';
|
||||||
|
|
||||||
|
test.describe('Convert JPG to PNG tool', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/png/convert-jgp-to-png');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert jpg to png', async ({ page }) => {
|
||||||
|
// Upload image
|
||||||
|
const fileInput = page.locator('input[type="file"]');
|
||||||
|
const imagePath = path.join(__dirname, 'test.jpg');
|
||||||
|
await fileInput?.setInputFiles(imagePath);
|
||||||
|
|
||||||
|
// 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 0x808080ff
|
||||||
|
const image = await Jimp.read(fileContent);
|
||||||
|
const color = image.getPixelColor(0, 0);
|
||||||
|
expect(color).toBe(0x808080ff);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply transparency before converting jpg to png', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
// Upload image
|
||||||
|
const fileInput = page.locator('input[type="file"]');
|
||||||
|
const imagePath = path.join(__dirname, 'test.jpg');
|
||||||
|
await fileInput?.setInputFiles(imagePath);
|
||||||
|
|
||||||
|
// Enable transparency on color 0x808080
|
||||||
|
await page.getByLabel('Enable PNG Transparency').check();
|
||||||
|
await page.getByTestId('color-input').fill('#808080');
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
155
src/pages/image/png/convert-jgp-to-png/index.tsx
Normal file
155
src/pages/image/png/convert-jgp-to-png/index.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { Box } from '@mui/material';
|
||||||
|
import ToolInputAndResult from 'components/ToolInputAndResult';
|
||||||
|
import ToolFileInput from 'components/input/ToolFileInput';
|
||||||
|
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
|
||||||
|
import ColorSelector from 'components/options/ColorSelector';
|
||||||
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
|
import ToolOptions from 'components/options/ToolOptions';
|
||||||
|
import ToolFileResult from 'components/result/ToolFileResult';
|
||||||
|
import Color from 'color';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { areColorsSimilar } from 'utils/color';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
enableTransparency: false,
|
||||||
|
color: 'white',
|
||||||
|
similarity: '10'
|
||||||
|
};
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
// splitSeparator: Yup.string().required('The separator is required')
|
||||||
|
});
|
||||||
|
export default function ConvertJgpToPng() {
|
||||||
|
const [input, setInput] = useState<File | null>(null);
|
||||||
|
const [result, setResult] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const compute = async (
|
||||||
|
optionsValues: typeof initialValues,
|
||||||
|
input: any
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const processImage = async (
|
||||||
|
file: File,
|
||||||
|
transparencyTransform?: {
|
||||||
|
color: [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);
|
||||||
|
|
||||||
|
if (transparencyTransform) {
|
||||||
|
const { color, similarity } = transparencyTransform;
|
||||||
|
|
||||||
|
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, color, similarity)) {
|
||||||
|
data[i + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
const newFile = new File([blob], file.name, {
|
||||||
|
type: 'image/png'
|
||||||
|
});
|
||||||
|
setResult(newFile);
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (optionsValues.enableTransparency) {
|
||||||
|
let rgb: [number, number, number];
|
||||||
|
try {
|
||||||
|
//@ts-ignore
|
||||||
|
rgb = Color(optionsValues.color).rgb().array();
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processImage(input, {
|
||||||
|
color: rgb,
|
||||||
|
similarity: Number(optionsValues.similarity)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
processImage(input);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<ToolInputAndResult
|
||||||
|
input={
|
||||||
|
<ToolFileInput
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
accept={['image/jpeg']}
|
||||||
|
title={'Input JPG'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
result={
|
||||||
|
<ToolFileResult
|
||||||
|
title={'Output PNG'}
|
||||||
|
value={result}
|
||||||
|
extension={'png'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ToolOptions
|
||||||
|
compute={compute}
|
||||||
|
getGroups={({ values, updateField }) => [
|
||||||
|
{
|
||||||
|
title: 'PNG Transparency Color',
|
||||||
|
component: (
|
||||||
|
<Box>
|
||||||
|
<CheckboxWithDesc
|
||||||
|
key="enableTransparency"
|
||||||
|
title="Enable PNG Transparency"
|
||||||
|
checked={!!values.enableTransparency}
|
||||||
|
onChange={(value) => updateField('enableTransparency', value)}
|
||||||
|
description="Make the color below transparent."
|
||||||
|
/>
|
||||||
|
<ColorSelector
|
||||||
|
value={values.color}
|
||||||
|
onColorChange={(val) => updateField('color', val)}
|
||||||
|
description={'With this color (to color)'}
|
||||||
|
inputProps={{ 'data-testid': 'color-input' }}
|
||||||
|
/>
|
||||||
|
<TextFieldWithDesc
|
||||||
|
value={values.similarity}
|
||||||
|
onOwnChange={(val) => updateField('similarity', val)}
|
||||||
|
description={
|
||||||
|
'Match this % of similar. For example, 10% white will match white and a little bit of gray.'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
initialValues={initialValues}
|
||||||
|
input={input}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/pages/image/png/convert-jgp-to-png/meta.ts
Normal file
14
src/pages/image/png/convert-jgp-to-png/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineTool } from '@tools/defineTool';
|
||||||
|
import { lazy } from 'react';
|
||||||
|
import image from '@assets/image.png';
|
||||||
|
|
||||||
|
export const tool = defineTool('png', {
|
||||||
|
name: 'Convert JPG to PNG',
|
||||||
|
path: 'convert-jgp-to-png',
|
||||||
|
image,
|
||||||
|
description:
|
||||||
|
'Quickly convert your JPG images to PNG. Just import your PNG image in the editor on the left',
|
||||||
|
shortDescription: 'Quickly convert your JPG images to PNG',
|
||||||
|
keywords: ['convert', 'jgp', 'png'],
|
||||||
|
component: lazy(() => import('./index'))
|
||||||
|
});
|
||||||
BIN
src/pages/image/png/convert-jgp-to-png/test.jpg
Normal file
BIN
src/pages/image/png/convert-jgp-to-png/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
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('/png/create-transparent');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import ColorSelector from '../../../../components/options/ColorSelector';
|
|||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
|
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
|
||||||
|
import { areColorsSimilar } from 'utils/color';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
fromColor: 'white',
|
fromColor: 'white',
|
||||||
@@ -51,28 +52,13 @@ export default function ChangeColorsInPng() {
|
|||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
const data: Uint8ClampedArray = imageData.data;
|
const data: Uint8ClampedArray = imageData.data;
|
||||||
|
|
||||||
const colorDistance = (
|
|
||||||
c1: [number, number, number],
|
|
||||||
c2: [number, number, number]
|
|
||||||
) => {
|
|
||||||
return Math.sqrt(
|
|
||||||
Math.pow(c1[0] - c2[0], 2) +
|
|
||||||
Math.pow(c1[1] - c2[1], 2) +
|
|
||||||
Math.pow(c1[2] - c2[2], 2)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const maxColorDistance = Math.sqrt(
|
|
||||||
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
|
|
||||||
);
|
|
||||||
const similarityThreshold = (similarity / 100) * maxColorDistance;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
const currentColor: [number, number, number] = [
|
const currentColor: [number, number, number] = [
|
||||||
data[i],
|
data[i],
|
||||||
data[i + 1],
|
data[i + 1],
|
||||||
data[i + 2]
|
data[i + 2]
|
||||||
];
|
];
|
||||||
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
|
if (areColorsSimilar(currentColor, fromColor, similarity)) {
|
||||||
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,8 +104,9 @@ export default function ChangeColorsInPng() {
|
|||||||
<Box>
|
<Box>
|
||||||
<ColorSelector
|
<ColorSelector
|
||||||
value={values.fromColor}
|
value={values.fromColor}
|
||||||
onChange={(val) => updateField('fromColor', val)}
|
onColorChange={(val) => updateField('fromColor', val)}
|
||||||
description={'Replace this color (from color)'}
|
description={'Replace this color (from color)'}
|
||||||
|
inputProps={{ 'data-testid': 'color-input' }}
|
||||||
/>
|
/>
|
||||||
<TextFieldWithDesc
|
<TextFieldWithDesc
|
||||||
value={values.similarity}
|
value={values.similarity}
|
||||||
|
|||||||
BIN
src/pages/image/png/create-transparent/test.png
Normal file
BIN
src/pages/image/png/create-transparent/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,4 +1,9 @@
|
|||||||
|
import { tool as convertJgpToPng } from './convert-jgp-to-png/meta';
|
||||||
import { tool as pngCreateTransparent } from './create-transparent/meta';
|
import { tool as pngCreateTransparent } from './create-transparent/meta';
|
||||||
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
|
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
|
||||||
|
|
||||||
export const pngTools = [changeColorsInPng, pngCreateTransparent];
|
export const pngTools = [
|
||||||
|
changeColorsInPng,
|
||||||
|
pngCreateTransparent,
|
||||||
|
convertJgpToPng
|
||||||
|
];
|
||||||
|
|||||||
34
src/utils/color.ts
Normal file
34
src/utils/color.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export function areColorsSimilar(
|
||||||
|
color1: [number, number, number],
|
||||||
|
color2: [number, number, number],
|
||||||
|
similarity: number
|
||||||
|
): boolean {
|
||||||
|
const colorDistance = (
|
||||||
|
c1: [number, number, number],
|
||||||
|
c2: [number, number, number]
|
||||||
|
) => {
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(c1[0] - c2[0], 2) +
|
||||||
|
Math.pow(c1[1] - c2[1], 2) +
|
||||||
|
Math.pow(c1[2] - c2[2], 2)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const maxColorDistance = Math.sqrt(
|
||||||
|
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
|
||||||
|
);
|
||||||
|
const similarityThreshold = (similarity / 100) * maxColorDistance;
|
||||||
|
|
||||||
|
return colorDistance(color1, color2) <= similarityThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertHexToRGBA(color: string): number {
|
||||||
|
// Remove the leading '#' if present
|
||||||
|
if (color.startsWith('#')) {
|
||||||
|
color = color.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the color to a number and add the alpha channel
|
||||||
|
const colorValue = parseInt(color, 16);
|
||||||
|
const alphaChannel = 0xff;
|
||||||
|
return (colorValue << 8) | alphaChannel;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user