This commit is contained in:
star7th
2025-04-22 20:48:34 +08:00
parent d3d96df0f5
commit bbfd4d9f93
8 changed files with 404 additions and 3 deletions
+310
View File
@@ -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>
);
}
+2 -1
View File
@@ -29,7 +29,8 @@ export const en = {
number_base_converter: tools.number_base_converter.en,
yml_properties_converter: tools.yml_properties_converter.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
}
};
+36
View File
@@ -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;
+36
View File
@@ -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;
+3 -1
View File
@@ -24,6 +24,7 @@ import numberBaseConverter from './number_base_converter';
import ymlPropertiesConverter from './yml_properties_converter';
import base64ToImage from './base64_to_image';
import imageWatermark from './image_watermark';
import imageToIco from './image_to_ico';
export const tools = {
json_formatter: jsonFormatter,
@@ -51,7 +52,8 @@ export const tools = {
number_base_converter: numberBaseConverter,
yml_properties_converter: ymlPropertiesConverter,
base64_to_image: base64ToImage,
image_watermark: imageWatermark
image_watermark: imageWatermark,
image_to_ico: imageToIco
};
export default tools;
+2 -1
View File
@@ -29,7 +29,8 @@ export const zh = {
number_base_converter: tools.number_base_converter.zh,
yml_properties_converter: tools.yml_properties_converter.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
}
};
+6
View File
@@ -165,6 +165,12 @@ const tools: Tool[] = [
icon: faImage,
category: ['image'],
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[];