From 81e0942fa94b8486d068f22ed4e31c0eabaf45a4 Mon Sep 17 00:00:00 2001 From: wz Date: Thu, 19 Mar 2026 10:59:39 +0800 Subject: [PATCH] feat: i18n translation and icon for convert to webp --- public/locales/en/image.json | 5 + .../image/generic/convert-to-webp/index.tsx | 148 ++++++++++++++++++ .../image/generic/convert-to-webp/meta.ts | 16 ++ src/pages/tools/image/generic/index.ts | 5 +- 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/image/generic/convert-to-webp/index.tsx create mode 100644 src/pages/tools/image/generic/convert-to-webp/meta.ts diff --git a/public/locales/en/image.json b/public/locales/en/image.json index 9a03c73..b7c269e 100644 --- a/public/locales/en/image.json +++ b/public/locales/en/image.json @@ -101,5 +101,10 @@ "description": "Rotate an image by a specified angle.", "shortDescription": "Rotate an image easily.", "title": "Rotate Image" + }, + "convertToWebp": { + "description": "Convert various image formats (PNG, JPG, JPEG) to WebP format.", + "shortDescription": "Convert Images to WebP Format with maximum file size.", + "title": "Convert Image to WebP" } } diff --git a/src/pages/tools/image/generic/convert-to-webp/index.tsx b/src/pages/tools/image/generic/convert-to-webp/index.tsx new file mode 100644 index 0000000..11d9baa --- /dev/null +++ b/src/pages/tools/image/generic/convert-to-webp/index.tsx @@ -0,0 +1,148 @@ +import { Box, Slider, Typography } from '@mui/material'; +import ToolImageInput from 'components/input/ToolImageInput'; +import ColorSelector from 'components/options/ColorSelector'; +import ToolFileResult from 'components/result/ToolFileResult'; +import Color from 'color'; +import React, { useState } from 'react'; +import * as Yup from 'yup'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; + +const initialValues = { + quality: 85, + backgroundColor: '#ffffff' +}; + +const validationSchema = Yup.object({ + quality: Yup.number().min(1).max(100).required('Quality is required'), + backgroundColor: Yup.string().required('Background color is required') +}); + +export default function ConvertToJpg({ title }: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + + const compute = async ( + optionsValues: typeof initialValues, + input: any + ): Promise => { + if (!input) return; + + const processImage = async ( + file: File, + quality: number, + backgroundColor: string + ) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (ctx == null) return; + + const img = new Image(); + img.src = URL.createObjectURL(file); + + try { + await img.decode(); + + canvas.width = img.width; + canvas.height = img.height; + + // Fill background with selected color (important for transparency) + let bgColor: [number, number, number]; + try { + //@ts-ignore + bgColor = Color(backgroundColor).rgb().array(); + } catch (err) { + bgColor = [255, 255, 255]; // Default to white + } + + ctx.fillStyle = `rgb(${bgColor[0]}, ${bgColor[1]}, ${bgColor[2]})`; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw the image on top + ctx.drawImage(img, 0, 0); + + // Convert to JPG with specified quality + canvas.toBlob( + (blob) => { + if (blob) { + const fileName = file.name.replace(/\.[^/.]+$/, '') + '.jpg'; + const newFile = new File([blob], fileName, { + type: 'image/jpeg' + }); + setResult(newFile); + } + }, + 'image/jpeg', + quality / 100 + ); + } catch (error) { + console.error('Error processing image:', error); + } finally { + URL.revokeObjectURL(img.src); + } + }; + + processImage(input, optionsValues.quality, optionsValues.backgroundColor); + }; + + return ( + + } + resultComponent={ + + } + initialValues={initialValues} + validationSchema={validationSchema} + getGroups={({ values, updateField }) => [ + { + title: 'JPG Quality Settings', + component: ( + + + + JPG Quality: {values.quality}% + + + updateField( + 'quality', + Array.isArray(value) ? value[0] : value + ) + } + min={1} + max={100} + step={1} + valueLabelDisplay="auto" + valueLabelFormat={(value) => `${value}%`} + sx={{ mt: 1 }} + /> + + Higher values = better quality, larger file size + + + + updateField('backgroundColor', val)} + description={'Background color (for transparent images)'} + inputProps={{ 'data-testid': 'background-color-input' }} + /> + + ) + } + ]} + compute={compute} + setInput={setInput} + /> + ); +} diff --git a/src/pages/tools/image/generic/convert-to-webp/meta.ts b/src/pages/tools/image/generic/convert-to-webp/meta.ts new file mode 100644 index 0000000..6d25662 --- /dev/null +++ b/src/pages/tools/image/generic/convert-to-webp/meta.ts @@ -0,0 +1,16 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('image-generic', { + i18n: { + name: 'image:convertToWebp.title', + description: 'image:convertToWebp.description', + shortDescription: 'image:convertToWebp.shortDescription' + }, + + path: 'comvert-to-webp', + icon: 'iconoir:media-image-list', + + keywords: ['convert', 'jpg', 'jpeg', 'png', 'webp', 'compression'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/image/generic/index.ts b/src/pages/tools/image/generic/index.ts index d87de31..95cef42 100644 --- a/src/pages/tools/image/generic/index.ts +++ b/src/pages/tools/image/generic/index.ts @@ -10,6 +10,8 @@ import { tool as qrCodeGenerator } from './qr-code/meta'; import { tool as rotateImage } from './rotate/meta'; import { tool as convertToJpg } from './convert-to-jpg/meta'; import { tool as imageEditor } from './editor/meta'; +import { tool as convertToWebp } from './convert-to-webp/meta'; + export const imageGenericTools = [ imageEditor, resizeImage, @@ -22,5 +24,6 @@ export const imageGenericTools = [ imageToText, qrCodeGenerator, rotateImage, - convertToJpg + convertToJpg, + convertToWebp ];