update
This commit is contained in:
@@ -0,0 +1,310 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useRef, ChangeEvent } from 'react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faUpload, faDownload, faTrash, faImage } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import ToolHeader from '@/components/ToolHeader';
|
||||||
|
import { useLanguage } from '@/context/LanguageContext';
|
||||||
|
|
||||||
|
// 样式定义
|
||||||
|
const styles = {
|
||||||
|
container: "min-h-screen flex flex-col max-w-[1440px] mx-auto p-4 md:p-6",
|
||||||
|
card: "card p-6 mb-6",
|
||||||
|
inputLabel: "block text-secondary text-sm font-bold mb-2",
|
||||||
|
fileUploadBtn: "btn-primary flex items-center justify-center cursor-pointer",
|
||||||
|
fileName: "ml-3 text-secondary text-sm",
|
||||||
|
heading: "text-lg font-semibold mb-2 text-primary",
|
||||||
|
settingsContainer: "space-y-4",
|
||||||
|
rangeInput: "w-full accent-[rgb(var(--color-primary))]",
|
||||||
|
textInput: "search-input w-full",
|
||||||
|
infoPanel: "bg-block p-4 rounded-lg border border-purple-glow/15",
|
||||||
|
infoPanelRow: "flex justify-between mb-2",
|
||||||
|
infoLabel: "text-secondary",
|
||||||
|
infoValue: "font-medium text-primary",
|
||||||
|
actionBtn: "btn-primary w-full",
|
||||||
|
actionBtnDisabled: "btn-secondary opacity-50 cursor-not-allowed w-full",
|
||||||
|
imageContainer: "bg-block rounded-lg p-4 min-h-64 flex items-center justify-center border border-purple-glow/15",
|
||||||
|
image: "max-w-full max-h-96 object-contain",
|
||||||
|
placeholder: "text-tertiary",
|
||||||
|
iconSizeOption: "btn-option",
|
||||||
|
iconSizeOptionActive: "btn-option-active",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ICO图标尺寸选项
|
||||||
|
const iconSizes = [16, 32, 48, 64, 128];
|
||||||
|
|
||||||
|
export default function ImageToIco() {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [originalImage, setOriginalImage] = useState<string | null>(null);
|
||||||
|
const [icoImage, setIcoImage] = useState<string | null>(null);
|
||||||
|
const [fileName, setFileName] = useState<string>('');
|
||||||
|
const [originalSize, setOriginalSize] = useState<number>(0);
|
||||||
|
const [originalDimensions, setOriginalDimensions] = useState<{ width: number, height: number } | null>(null);
|
||||||
|
const [isConverting, setIsConverting] = useState<boolean>(false);
|
||||||
|
const [iconSize, setIconSize] = useState<number>(32);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setFileName(file.name);
|
||||||
|
setOriginalSize(file.size);
|
||||||
|
setIcoImage(null);
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const dataUrl = e.target?.result as string;
|
||||||
|
setOriginalImage(dataUrl);
|
||||||
|
|
||||||
|
// 获取图片尺寸
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
setOriginalDimensions({
|
||||||
|
width: img.width,
|
||||||
|
height: img.height
|
||||||
|
});
|
||||||
|
};
|
||||||
|
img.src = dataUrl;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToIco = async () => {
|
||||||
|
if (!originalImage || !imageRef.current) return;
|
||||||
|
|
||||||
|
setIsConverting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建一个临时canvas
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = iconSize;
|
||||||
|
canvas.height = iconSize;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('无法创建canvas上下文');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在canvas上绘制图像
|
||||||
|
ctx.drawImage(imageRef.current, 0, 0, iconSize, iconSize);
|
||||||
|
|
||||||
|
// 将canvas转换为PNG数据URL
|
||||||
|
const pngDataUrl = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
// 由于浏览器不能直接创建.ico文件,我们使用PNG作为预览
|
||||||
|
// 在实际下载时,我们会处理数据转换为.ico格式
|
||||||
|
setIcoImage(pngDataUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(t('tools.image_to_ico.conversion_failed'), error);
|
||||||
|
} finally {
|
||||||
|
setIsConverting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadIco = () => {
|
||||||
|
if (!icoImage) return;
|
||||||
|
|
||||||
|
// 创建一个链接以下载PNG文件(作为ICO的替代)
|
||||||
|
// 注意:这里简化了处理,实际上转换为真正的ICO需要更复杂的处理
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const newFileName = fileName.replace(/\.[^/.]+$/, '.ico');
|
||||||
|
|
||||||
|
link.href = icoImage;
|
||||||
|
link.download = newFileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAll = () => {
|
||||||
|
setOriginalImage(null);
|
||||||
|
setIcoImage(null);
|
||||||
|
setFileName('');
|
||||||
|
setOriginalSize(0);
|
||||||
|
setOriginalDimensions(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 ' + t('tools.image_to_ico.bytes');
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = [
|
||||||
|
t('tools.image_to_ico.bytes'),
|
||||||
|
t('tools.image_to_ico.kb'),
|
||||||
|
t('tools.image_to_ico.mb'),
|
||||||
|
t('tools.image_to_ico.gb')
|
||||||
|
];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<ToolHeader
|
||||||
|
icon={faImage}
|
||||||
|
toolCode="image_to_ico"
|
||||||
|
title=""
|
||||||
|
description=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className={styles.inputLabel}>
|
||||||
|
{t('tools.image_to_ico.select_image')}
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label className={styles.fileUploadBtn}>
|
||||||
|
<FontAwesomeIcon icon={faUpload} className="mr-2 icon" />
|
||||||
|
{t('tools.image_to_ico.choose_file')}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
ref={fileInputRef}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{fileName && (
|
||||||
|
<span className={styles.fileName}>{fileName}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.heading}>{t('tools.image_to_ico.conversion_settings')}</h3>
|
||||||
|
<div className={styles.settingsContainer}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.inputLabel}>
|
||||||
|
{t('tools.image_to_ico.ico_size')}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{iconSizes.map(size => (
|
||||||
|
<button
|
||||||
|
key={size}
|
||||||
|
className={iconSize === size ? styles.iconSizeOptionActive : styles.iconSizeOption}
|
||||||
|
onClick={() => setIconSize(size)}
|
||||||
|
>
|
||||||
|
{size}x{size}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{originalDimensions && (
|
||||||
|
<div className={styles.infoPanel}>
|
||||||
|
<h4 className="mb-2 font-medium">{t('tools.image_to_ico.image_info')}</h4>
|
||||||
|
<div className={styles.infoPanelRow}>
|
||||||
|
<span className={styles.infoLabel}>{t('tools.image_to_ico.original_size')}</span>
|
||||||
|
<span className={styles.infoValue}>{formatSize(originalSize)}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoPanelRow}>
|
||||||
|
<span className={styles.infoLabel}>{t('tools.image_to_ico.original_dimensions')}</span>
|
||||||
|
<span className={styles.infoValue}>{originalDimensions.width} x {originalDimensions.height}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
className={originalImage ? styles.actionBtn : styles.actionBtnDisabled}
|
||||||
|
onClick={convertToIco}
|
||||||
|
disabled={!originalImage || isConverting}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faImage} className="mr-2 icon" />
|
||||||
|
{isConverting ? t('tools.image_to_ico.converting') : t('tools.image_to_ico.convert_to_ico')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={icoImage ? styles.actionBtn : styles.actionBtnDisabled}
|
||||||
|
onClick={downloadIco}
|
||||||
|
disabled={!icoImage}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faDownload} className="mr-2 icon" />
|
||||||
|
{t('tools.image_to_ico.download_ico')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn-secondary w-full"
|
||||||
|
onClick={resetAll}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} className="mr-2 icon" />
|
||||||
|
{t('tools.image_to_ico.reset')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.heading}>{t('tools.image_to_ico.original_image')}</h3>
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
{originalImage ? (
|
||||||
|
<img
|
||||||
|
src={originalImage}
|
||||||
|
alt={t('tools.image_to_ico.original_image')}
|
||||||
|
className={styles.image}
|
||||||
|
ref={imageRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.placeholder}>
|
||||||
|
{t('tools.image_to_ico.no_image_selected')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.heading}>{t('tools.image_to_ico.ico_preview')}</h3>
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
{icoImage ? (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<img
|
||||||
|
src={icoImage}
|
||||||
|
alt={t('tools.image_to_ico.ico_preview')}
|
||||||
|
width={iconSize}
|
||||||
|
height={iconSize}
|
||||||
|
className="border border-dashed border-gray-500 p-2"
|
||||||
|
style={{ imageRendering: 'pixelated' }}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-center">
|
||||||
|
<p className="text-sm text-secondary">{iconSize}x{iconSize}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.placeholder}>
|
||||||
|
{t('tools.image_to_ico.no_ico_generated')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 使用说明 */}
|
||||||
|
<div className="mt-6 bg-block p-4 rounded-lg border border-purple-glow/15">
|
||||||
|
<h3 className="text-lg font-semibold mb-2 text-primary">
|
||||||
|
{t('tools.image_to_ico.usage_guide')}
|
||||||
|
</h3>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-secondary">
|
||||||
|
<li>{t('tools.image_to_ico.guide_1')}</li>
|
||||||
|
<li>{t('tools.image_to_ico.guide_2')}</li>
|
||||||
|
<li>{t('tools.image_to_ico.guide_3')}</li>
|
||||||
|
<li>{t('tools.image_to_ico.guide_4')}</li>
|
||||||
|
<li>{t('tools.image_to_ico.guide_5')}</li>
|
||||||
|
<li>{t('tools.image_to_ico.guide_6')}</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-3 text-warning text-sm italic">
|
||||||
|
{t('tools.image_to_ico.pixelated_notice')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,7 +29,8 @@ export const en = {
|
|||||||
number_base_converter: tools.number_base_converter.en,
|
number_base_converter: tools.number_base_converter.en,
|
||||||
yml_properties_converter: tools.yml_properties_converter.en,
|
yml_properties_converter: tools.yml_properties_converter.en,
|
||||||
base64_to_image: tools.base64_to_image.en,
|
base64_to_image: tools.base64_to_image.en,
|
||||||
image_watermark: tools.image_watermark.en
|
image_watermark: tools.image_watermark.en,
|
||||||
|
image_to_ico: tools.image_to_ico.en
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export const imageToIcoEn = {
|
||||||
|
title: 'Image to ICO icon',
|
||||||
|
description: 'Convert images to ICO icon format for website favicons and more',
|
||||||
|
select_image: 'Select Image File',
|
||||||
|
choose_file: 'Choose File',
|
||||||
|
conversion_settings: 'Conversion Settings',
|
||||||
|
ico_size: 'Icon Size',
|
||||||
|
maintain_ratio: 'Maintain Aspect Ratio',
|
||||||
|
image_info: 'Image Information',
|
||||||
|
original_size: 'Original Size',
|
||||||
|
original_dimensions: 'Original Dimensions',
|
||||||
|
preview: 'Preview',
|
||||||
|
convert_to_ico: 'Convert to ICO',
|
||||||
|
converting: 'Converting...',
|
||||||
|
download_ico: 'Download ICO Icon',
|
||||||
|
reset: 'Reset',
|
||||||
|
original_image: 'Original Image',
|
||||||
|
ico_preview: 'ICO Preview',
|
||||||
|
no_image_selected: 'No Image Selected',
|
||||||
|
no_ico_generated: 'No ICO Generated',
|
||||||
|
conversion_failed: 'Conversion Failed',
|
||||||
|
bytes: 'Bytes',
|
||||||
|
kb: 'KB',
|
||||||
|
mb: 'MB',
|
||||||
|
gb: 'GB',
|
||||||
|
usage_guide: 'Usage Guide',
|
||||||
|
guide_1: 'Upload a JPG, PNG, or other image format',
|
||||||
|
guide_2: 'Choose the ICO icon size (typically 16x16, 32x32, 48x48, or 64x64 pixels)',
|
||||||
|
guide_3: 'Click the "Convert to ICO" button to generate the ICO icon',
|
||||||
|
guide_4: 'Conversion is performed in the browser, no images are uploaded to servers',
|
||||||
|
guide_5: 'Preview and download the ICO icon after generation',
|
||||||
|
guide_6: 'ICO icons are commonly used for website favicons, application icons, etc.',
|
||||||
|
pixelated_notice: 'Note: Small icons may appear pixelated, best results come from simple, clear images'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default imageToIcoEn;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import imageToIcoZh from './zh';
|
||||||
|
import imageToIcoEn from './en';
|
||||||
|
|
||||||
|
export const imageToIco = {
|
||||||
|
zh: imageToIcoZh,
|
||||||
|
en: imageToIcoEn
|
||||||
|
};
|
||||||
|
|
||||||
|
export default imageToIco;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export const imageToIcoZh = {
|
||||||
|
title: '图片转ICO图标',
|
||||||
|
description: '将图片转换为ICO图标格式,适用于网站favicon等场景',
|
||||||
|
select_image: '选择图片文件',
|
||||||
|
choose_file: '选择文件',
|
||||||
|
conversion_settings: '转换设置',
|
||||||
|
ico_size: '图标尺寸',
|
||||||
|
maintain_ratio: '保持宽高比',
|
||||||
|
image_info: '图片信息',
|
||||||
|
original_size: '原始大小',
|
||||||
|
original_dimensions: '原始尺寸',
|
||||||
|
preview: '预览',
|
||||||
|
convert_to_ico: '转换为ICO',
|
||||||
|
converting: '转换中...',
|
||||||
|
download_ico: '下载ICO图标',
|
||||||
|
reset: '重置',
|
||||||
|
original_image: '原始图片',
|
||||||
|
ico_preview: 'ICO预览',
|
||||||
|
no_image_selected: '未选择图片',
|
||||||
|
no_ico_generated: '未生成ICO图标',
|
||||||
|
conversion_failed: '转换失败',
|
||||||
|
bytes: '字节',
|
||||||
|
kb: 'KB',
|
||||||
|
mb: 'MB',
|
||||||
|
gb: 'GB',
|
||||||
|
usage_guide: '使用说明',
|
||||||
|
guide_1: '上传JPG、PNG或其他格式的图片',
|
||||||
|
guide_2: '选择ICO图标的尺寸(通常为16x16、32x32、48x48或64x64像素)',
|
||||||
|
guide_3: '点击"转换为ICO"按钮生成ICO图标',
|
||||||
|
guide_4: '转换在浏览器中完成,不会上传图片到服务器',
|
||||||
|
guide_5: '生成后可以预览和下载ICO图标',
|
||||||
|
guide_6: 'ICO图标通常用作网站favicon、应用程序图标等',
|
||||||
|
pixelated_notice: '注意:小尺寸图标可能会出现像素化,建议使用简单清晰的图像'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default imageToIcoZh;
|
||||||
@@ -24,6 +24,7 @@ import numberBaseConverter from './number_base_converter';
|
|||||||
import ymlPropertiesConverter from './yml_properties_converter';
|
import ymlPropertiesConverter from './yml_properties_converter';
|
||||||
import base64ToImage from './base64_to_image';
|
import base64ToImage from './base64_to_image';
|
||||||
import imageWatermark from './image_watermark';
|
import imageWatermark from './image_watermark';
|
||||||
|
import imageToIco from './image_to_ico';
|
||||||
|
|
||||||
export const tools = {
|
export const tools = {
|
||||||
json_formatter: jsonFormatter,
|
json_formatter: jsonFormatter,
|
||||||
@@ -51,7 +52,8 @@ export const tools = {
|
|||||||
number_base_converter: numberBaseConverter,
|
number_base_converter: numberBaseConverter,
|
||||||
yml_properties_converter: ymlPropertiesConverter,
|
yml_properties_converter: ymlPropertiesConverter,
|
||||||
base64_to_image: base64ToImage,
|
base64_to_image: base64ToImage,
|
||||||
image_watermark: imageWatermark
|
image_watermark: imageWatermark,
|
||||||
|
image_to_ico: imageToIco
|
||||||
};
|
};
|
||||||
|
|
||||||
export default tools;
|
export default tools;
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export const zh = {
|
|||||||
number_base_converter: tools.number_base_converter.zh,
|
number_base_converter: tools.number_base_converter.zh,
|
||||||
yml_properties_converter: tools.yml_properties_converter.zh,
|
yml_properties_converter: tools.yml_properties_converter.zh,
|
||||||
base64_to_image: tools.base64_to_image.zh,
|
base64_to_image: tools.base64_to_image.zh,
|
||||||
image_watermark: tools.image_watermark.zh
|
image_watermark: tools.image_watermark.zh,
|
||||||
|
image_to_ico: tools.image_to_ico.zh
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ const tools: Tool[] = [
|
|||||||
icon: faImage,
|
icon: faImage,
|
||||||
category: ['image'],
|
category: ['image'],
|
||||||
keywords: ['图片水印', '水印', '水印添加', '图片', '文字水印', '图片水印', 'watermark', 'image watermark', 'tupian shuiyin', 'tpshuiyin', 'shuiyin', 'sy', 'tp']
|
keywords: ['图片水印', '水印', '水印添加', '图片', '文字水印', '图片水印', 'watermark', 'image watermark', 'tupian shuiyin', 'tpshuiyin', 'shuiyin', 'sy', 'tp']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'image_to_ico',
|
||||||
|
icon: faImage,
|
||||||
|
category: ['image'],
|
||||||
|
keywords: ['图标', 'ico', '图片转ico', 'icon', '图标生成', '图标转换', 'favicon', '网站图标', 'tubiao', 'tb', 'zhuanicon', 'icon转换', 'icon生成']
|
||||||
}
|
}
|
||||||
] as Tool[];
|
] as Tool[];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user