Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4 convert jpg to png #17

Merged
merged 9 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading