From bbfd4d9f9331fe2c222f57aa0ab3dc70fbe19cb5 Mon Sep 17 00:00:00 2001 From: star7th Date: Tue, 22 Apr 2025 20:48:34 +0800 Subject: [PATCH] update --- src/app/tools/image_to_ico/page.tsx | 310 ++++++++++++++++++++ src/config/i18n/en.ts | 3 +- src/config/i18n/tools/image_to_ico/en.ts | 36 +++ src/config/i18n/tools/image_to_ico/index.ts | 9 + src/config/i18n/tools/image_to_ico/zh.ts | 36 +++ src/config/i18n/tools/index.ts | 4 +- src/config/i18n/zh.ts | 3 +- src/config/tools.ts | 6 + 8 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 src/app/tools/image_to_ico/page.tsx create mode 100644 src/config/i18n/tools/image_to_ico/en.ts create mode 100644 src/config/i18n/tools/image_to_ico/index.ts create mode 100644 src/config/i18n/tools/image_to_ico/zh.ts diff --git a/src/app/tools/image_to_ico/page.tsx b/src/app/tools/image_to_ico/page.tsx new file mode 100644 index 0000000..5acb212 --- /dev/null +++ b/src/app/tools/image_to_ico/page.tsx @@ -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(null); + const [icoImage, setIcoImage] = useState(null); + const [fileName, setFileName] = useState(''); + const [originalSize, setOriginalSize] = useState(0); + const [originalDimensions, setOriginalDimensions] = useState<{ width: number, height: number } | null>(null); + const [isConverting, setIsConverting] = useState(false); + const [iconSize, setIconSize] = useState(32); + const fileInputRef = useRef(null); + const imageRef = useRef(null); + + const handleFileChange = (event: ChangeEvent) => { + 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 ( +
+ + +
+
+ +
+ + {fileName && ( + {fileName} + )} +
+
+ +
+
+

{t('tools.image_to_ico.conversion_settings')}

+
+
+ +
+ {iconSizes.map(size => ( + + ))} +
+
+ + {originalDimensions && ( +
+

{t('tools.image_to_ico.image_info')}

+
+ {t('tools.image_to_ico.original_size')} + {formatSize(originalSize)} +
+
+ {t('tools.image_to_ico.original_dimensions')} + {originalDimensions.width} x {originalDimensions.height} +
+
+ )} +
+ +
+ + + + + +
+
+ +
+
+
+

{t('tools.image_to_ico.original_image')}

+
+ {originalImage ? ( + {t('tools.image_to_ico.original_image')} + ) : ( +
+ {t('tools.image_to_ico.no_image_selected')} +
+ )} +
+
+ +
+

{t('tools.image_to_ico.ico_preview')}

+
+ {icoImage ? ( +
+ {t('tools.image_to_ico.ico_preview')} +
+

{iconSize}x{iconSize}

+
+
+ ) : ( +
+ {t('tools.image_to_ico.no_ico_generated')} +
+ )} +
+
+
+
+
+ + {/* 使用说明 */} +
+

+ {t('tools.image_to_ico.usage_guide')} +

+
    +
  • {t('tools.image_to_ico.guide_1')}
  • +
  • {t('tools.image_to_ico.guide_2')}
  • +
  • {t('tools.image_to_ico.guide_3')}
  • +
  • {t('tools.image_to_ico.guide_4')}
  • +
  • {t('tools.image_to_ico.guide_5')}
  • +
  • {t('tools.image_to_ico.guide_6')}
  • +
+

+ {t('tools.image_to_ico.pixelated_notice')} +

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/config/i18n/en.ts b/src/config/i18n/en.ts index 521c739..2c0a6f5 100644 --- a/src/config/i18n/en.ts +++ b/src/config/i18n/en.ts @@ -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 } }; diff --git a/src/config/i18n/tools/image_to_ico/en.ts b/src/config/i18n/tools/image_to_ico/en.ts new file mode 100644 index 0000000..9d74227 --- /dev/null +++ b/src/config/i18n/tools/image_to_ico/en.ts @@ -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; \ No newline at end of file diff --git a/src/config/i18n/tools/image_to_ico/index.ts b/src/config/i18n/tools/image_to_ico/index.ts new file mode 100644 index 0000000..5a54850 --- /dev/null +++ b/src/config/i18n/tools/image_to_ico/index.ts @@ -0,0 +1,9 @@ +import imageToIcoZh from './zh'; +import imageToIcoEn from './en'; + +export const imageToIco = { + zh: imageToIcoZh, + en: imageToIcoEn +}; + +export default imageToIco; \ No newline at end of file diff --git a/src/config/i18n/tools/image_to_ico/zh.ts b/src/config/i18n/tools/image_to_ico/zh.ts new file mode 100644 index 0000000..5499d3c --- /dev/null +++ b/src/config/i18n/tools/image_to_ico/zh.ts @@ -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; \ No newline at end of file diff --git a/src/config/i18n/tools/index.ts b/src/config/i18n/tools/index.ts index 730baf4..b1f3fc8 100644 --- a/src/config/i18n/tools/index.ts +++ b/src/config/i18n/tools/index.ts @@ -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; diff --git a/src/config/i18n/zh.ts b/src/config/i18n/zh.ts index dd588ac..a6cde19 100644 --- a/src/config/i18n/zh.ts +++ b/src/config/i18n/zh.ts @@ -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 } }; diff --git a/src/config/tools.ts b/src/config/tools.ts index 13cb600..f059639 100644 --- a/src/config/tools.ts +++ b/src/config/tools.ts @@ -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[];