diff --git a/.eslintrc b/.eslintrc
index 6c7bbbd..a473206 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -42,6 +42,7 @@
"tailwindcss/classnames-order": "warn",
"tailwindcss/no-custom-classname": "warn",
"tailwindcss/no-contradicting-classname": "error",
- "@typescript-eslint/ban-types": "off"
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/ban-ts-comment": "off"
}
}
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index d18ac21..2f9c07e 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,17 +4,15 @@
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
@@ -45,37 +43,37 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "Vitest.mergeText.executor": "Run",
+ "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
+ "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
+ "git-widget-placeholder": "main",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/HP/IdeaProjects/omni-tools/src/assets",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.dev.executor": "Run",
+ "npm.prebuild.executor": "Run",
+ "npm.script:create:tool.executor": "Run",
+ "npm.test.executor": "Run",
+ "prettierjs.PrettierConfiguration.Package": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier",
+ "project.structure.last.edited": "Problems",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.2",
+ "settings.editor.selected.configurable": "settings.typescriptcompiler",
+ "ts.external.directory.path": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
@@ -552,7 +559,6 @@
-
@@ -577,7 +583,8 @@
-
+
+
diff --git a/package-lock.json b/package-lock.json
index 9808b59..7c7f076 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@types/lodash": "^4.17.5",
+ "color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
"notistack": "^3.0.1",
@@ -27,6 +28,8 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
+ "@types/color": "^3.0.6",
+ "@types/color-rgba": "^2.1.2",
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
@@ -2408,6 +2411,36 @@
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
},
+ "node_modules/@types/color": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz",
+ "integrity": "sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-convert": "*"
+ }
+ },
+ "node_modules/@types/color-convert": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.3.tgz",
+ "integrity": "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "*"
+ }
+ },
+ "node_modules/@types/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg==",
+ "dev": true
+ },
+ "node_modules/@types/color-rgba": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@types/color-rgba/-/color-rgba-2.1.2.tgz",
+ "integrity": "sha512-gDV/fgs4Mpc+hcHygYnM2EDgcxaHmvIGrAVxZJjP38f2IXQKHiGf0XMHhFd+dz8EVPSNTwHL5DJ6yXsxEiCQkg==",
+ "dev": true
+ },
"node_modules/@types/conventional-commits-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
@@ -3527,11 +3560,22 @@
"node": ">=6"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -3542,8 +3586,16 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
},
"node_modules/commander": {
"version": "4.1.1",
@@ -7375,6 +7427,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
diff --git a/package.json b/package.json
index 4a5dec7..71c3c43 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@types/lodash": "^4.17.5",
+ "color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
"notistack": "^3.0.1",
@@ -43,6 +44,8 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
+ "@types/color": "^3.0.6",
+ "@types/color-rgba": "^2.1.2",
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
diff --git a/src/components/input/ToolFileInput.tsx b/src/components/input/ToolFileInput.tsx
index ced405f..b2189ea 100644
--- a/src/components/input/ToolFileInput.tsx
+++ b/src/components/input/ToolFileInput.tsx
@@ -5,6 +5,7 @@ import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import greyPattern from '@assets/grey-pattern.png';
+import { globalInputHeight } from '../../config/uiConfig';
interface ToolFileInputProps {
value: File | null;
@@ -57,7 +58,7 @@ export default function ToolFileInput({
) : (
diff --git a/src/components/options/ColorSelector.tsx b/src/components/options/ColorSelector.tsx
new file mode 100644
index 0000000..ec49951
--- /dev/null
+++ b/src/components/options/ColorSelector.tsx
@@ -0,0 +1,48 @@
+import React, { useState, ChangeEvent, useRef } from 'react';
+import { Box, Stack, TextField } from '@mui/material';
+import PaletteIcon from '@mui/icons-material/Palette';
+import IconButton from '@mui/material/IconButton';
+import Typography from '@mui/material/Typography';
+import { descriptionFontSize } from '../../config/uiConfig';
+
+interface ColorSelectorProps {
+ value: string;
+ onChange: (val: string) => void;
+ description: string;
+}
+
+const ColorSelector: React.FC = ({
+ value = '#ffffff',
+ onChange,
+ description
+}) => {
+ const [color, setColor] = useState(value);
+ const inputRef = useRef(null);
+
+ const handleColorChange = (event: ChangeEvent) => {
+ const val = event.target.value;
+ setColor(val);
+ onChange(val);
+ };
+
+ return (
+
+
+
+ inputRef.current?.click()}>
+
+
+
+
+ {description}
+
+ );
+};
+
+export default ColorSelector;
diff --git a/src/components/result/ToolFileResult.tsx b/src/components/result/ToolFileResult.tsx
index d9f0e67..dccc060 100644
--- a/src/components/result/ToolFileResult.tsx
+++ b/src/components/result/ToolFileResult.tsx
@@ -2,6 +2,7 @@ import { Box } from '@mui/material';
import React from 'react';
import InputHeader from '../InputHeader';
import greyPattern from '@assets/grey-pattern.png';
+import { globalInputHeight } from '../../config/uiConfig';
export default function ToolFileResult({
title = 'Result',
@@ -29,7 +30,7 @@ export default function ToolFileResult({
)}
diff --git a/src/config/uiConfig.ts b/src/config/uiConfig.ts
new file mode 100644
index 0000000..5ec7d7b
--- /dev/null
+++ b/src/config/uiConfig.ts
@@ -0,0 +1,2 @@
+export const globalInputHeight = 300;
+export const descriptionFontSize = 12;
diff --git a/src/pages/image/png/change-colors-in-png/index.tsx b/src/pages/image/png/change-colors-in-png/index.tsx
index 59da401..62179a6 100644
--- a/src/pages/image/png/change-colors-in-png/index.tsx
+++ b/src/pages/image/png/change-colors-in-png/index.tsx
@@ -1,14 +1,18 @@
-import { Box } from '@mui/material';
+import { Box, Stack } from '@mui/material';
import React, { useEffect, useState } from 'react';
import * as Yup from 'yup';
import Grid from '@mui/material/Grid';
-import ToolTextInput from '../../../../components/input/ToolTextInput';
-import ToolTextResult from '../../../../components/result/ToolTextResult';
import ToolFileInput from '../../../../components/input/ToolFileInput';
import ToolFileResult from '../../../../components/result/ToolFileResult';
+import ToolOptions from '../../../../components/options/ToolOptions';
+import Typography from '@mui/material/Typography';
+import { Formik, useFormikContext } from 'formik';
+import ColorSelector from '../../../../components/options/ColorSelector';
+import Color from 'color';
const initialValues = {
- rgba: [0, 0, 0, 0]
+ fromColor: 'white',
+ toColor: 'black'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
@@ -17,9 +21,27 @@ export default function ChangeColorsInPng() {
const [input, setInput] = useState(null);
const [result, setResult] = useState(null);
- useEffect(() => {
- if (input) {
- const processImage = async (file: File) => {
+ const FormikListenerComponent = ({ input }: { input: File }) => {
+ const { values } = useFormikContext();
+ const { fromColor, toColor } = values;
+
+ useEffect(() => {
+ let fromRgb: [number, number, number];
+ let toRgb: [number, number, number];
+ try {
+ //@ts-ignore
+ fromRgb = Color(fromColor).rgb().array();
+ //@ts-ignore
+ toRgb = Color(toColor).rgb().array();
+ } catch (err) {
+ return;
+ }
+ const processImage = async (
+ file: File,
+ fromColor: [number, number, number],
+ toColor: [number, number, number],
+ similarity: number
+ ) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
@@ -34,12 +56,28 @@ export default function ChangeColorsInPng() {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
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)
+ );
+ };
+
for (let i = 0; i < data.length; i += 4) {
- // Check for white pixel
- if (data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) {
- data[i] = 255; // Red
- data[i + 1] = 0; // Green
- data[i + 2] = 0; // Blue
+ const currentColor: [number, number, number] = [
+ data[i],
+ data[i + 1],
+ data[i + 2]
+ ];
+ if (colorDistance(currentColor, fromColor) <= similarity) {
+ data[i] = toColor[0]; // Red
+ data[i + 1] = toColor[1]; // Green
+ data[i + 2] = toColor[2]; // Blue
}
}
@@ -53,9 +91,12 @@ export default function ChangeColorsInPng() {
}, 'image/png');
};
- processImage(input);
- }
- }, [input]);
+ processImage(input, fromRgb, toRgb, 10);
+ }, [input, fromColor, toColor]);
+
+ return null;
+ };
+
return (
@@ -71,6 +112,33 @@ export default function ChangeColorsInPng() {
+
+ {}}
+ >
+ {({ setFieldValue, values }) => (
+
+ {input && }
+
+ From color and to color
+ setFieldValue('fromColor', val)}
+ description={'Replace this color (from color)'}
+ />
+ setFieldValue('toColor', val)}
+ description={'With this color (to color).\n'}
+ />
+
+
+
+ )}
+
+
);
}
diff --git a/src/pages/image/png/change-colors-in-png/meta.ts b/src/pages/image/png/change-colors-in-png/meta.ts
index c6aac38..dd87e03 100644
--- a/src/pages/image/png/change-colors-in-png/meta.ts
+++ b/src/pages/image/png/change-colors-in-png/meta.ts
@@ -6,7 +6,8 @@ export const tool = defineTool('png', {
name: 'Change colors in png',
path: 'change-colors-in-png',
image,
- description: '',
+ description:
+ "World's simplest online Portable Network Graphics (PNG) color changer. Just import your PNG image in the editor on the left, select which colors to change, and you'll instantly get a new PNG with the new colors on the right. Free, quick, and very powerful. Import a PNG – replace its colors.",
keywords: ['change', 'colors', 'in', 'png'],
component: lazy(() => import('./index'))
});