Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
iib0011 committed Jun 28, 2024
2 parents eddb7ac + b7cc30a commit c2a8b00
Show file tree
Hide file tree
Showing 15 changed files with 1,268 additions and 46 deletions.
889 changes: 885 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/omggif": "^1.0.5",
"color": "^4.2.3",
"formik": "^2.4.6",
"jimp": "^0.22.12",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
Expand Down
14 changes: 8 additions & 6 deletions src/components/options/ColorSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
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 IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { globalDescriptionFontSize } from '../../config/uiConfig';

interface ColorSelectorProps {
value: string;
onChange: (val: string) => void;
onColorChange: (val: string) => void;
description: string;
}

const ColorSelector: React.FC<ColorSelectorProps> = ({
const ColorSelector: React.FC<ColorSelectorProps & TextFieldProps> = ({
value = '#ffffff',
onChange,
description
onColorChange,
description,
...props
}) => {
const [color, setColor] = useState<string>(value);
const inputRef = useRef<HTMLInputElement>(null);

const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
const val = event.target.value;
setColor(val);
onChange(val);
onColorChange(val);
};

return (
Expand All @@ -32,6 +33,7 @@ const ColorSelector: React.FC<ColorSelectorProps> = ({
sx={{ backgroundColor: 'white' }}
value={color}
onChange={handleColorChange}
{...props}
/>
<IconButton onClick={() => inputRef.current?.click()}>
<PaletteIcon />
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
});
});
24 changes: 6 additions & 18 deletions src/pages/image/png/change-colors-in-png/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ColorSelector from '../../../../components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color';

const initialValues = {
fromColor: 'white',
Expand Down Expand Up @@ -55,28 +56,13 @@ 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)
);
};
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) {
const currentColor: [number, number, number] = [
data[i],
data[i + 1],
data[i + 2]
];
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
if (areColorsSimilar(currentColor, fromColor, similarity)) {
data[i] = toColor[0]; // Red
data[i + 1] = toColor[1]; // Green
data[i + 2] = toColor[2]; // Blue
Expand Down Expand Up @@ -125,13 +111,15 @@ export default function ChangeColorsInPng() {
<Box>
<ColorSelector
value={values.fromColor}
onChange={(val) => updateField('fromColor', val)}
onColorChange={(val) => updateField('fromColor', val)}
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'from-color-input' }}
/>
<ColorSelector
value={values.toColor}
onChange={(val) => updateField('toColor', val)}
onColorChange={(val) => updateField('toColor', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'to-color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
Expand Down
Binary file added src/pages/image/png/change-colors-in-png/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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 changes: 155 additions & 0 deletions src/pages/image/png/convert-jgp-to-png/index.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions src/pages/image/png/convert-jgp-to-png/meta.ts
Original file line number Diff line number Diff line change
@@ -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'))
});
Binary file added src/pages/image/png/convert-jgp-to-png/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit c2a8b00

Please sign in to comment.