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,
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user