forked from zguiy/utils
工具完成
This commit is contained in:
838
src/components/tools/ImageToIco.vue
Normal file
838
src/components/tools/ImageToIco.vue
Normal file
@ -0,0 +1,838 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 工具栏 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="convertToIco"
|
||||
:disabled="!selectedImage"
|
||||
class="btn-primary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'magic']" class="mr-2" />
|
||||
转换为ICO
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="downloadIco"
|
||||
:disabled="!icoData"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
|
||||
下载ICO
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="clearAll"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
|
||||
清除
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="loadSample"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'image']" class="mr-2" />
|
||||
示例图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 图片上传区域 -->
|
||||
<div class="space-y-4">
|
||||
<!-- 上传区域 -->
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-semibold text-primary mb-4">选择图片</h3>
|
||||
|
||||
<!-- 拖拽上传区域 -->
|
||||
<div
|
||||
@drop="handleDrop"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@click="selectFile"
|
||||
:class="[
|
||||
'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors',
|
||||
isDragOver
|
||||
? 'border-primary bg-primary bg-opacity-10'
|
||||
: 'border-border hover:border-primary hover:bg-block-hover'
|
||||
]"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleFileSelect"
|
||||
class="hidden"
|
||||
>
|
||||
|
||||
<div class="space-y-3">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'cloud-upload-alt']"
|
||||
class="text-4xl text-secondary"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-primary font-medium">
|
||||
点击选择或拖拽图片到此处
|
||||
</div>
|
||||
<div class="text-sm text-secondary mt-1">
|
||||
支持 JPG、PNG、GIF、BMP、WebP 格式
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支持的格式说明 -->
|
||||
<div class="mt-4 text-sm text-secondary">
|
||||
<div class="font-medium mb-2">支持的输入格式:</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'check']" class="text-success text-xs" />
|
||||
<span>JPEG (.jpg, .jpeg)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'check']" class="text-success text-xs" />
|
||||
<span>PNG (.png)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'check']" class="text-success text-xs" />
|
||||
<span>GIF (.gif)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'check']" class="text-success text-xs" />
|
||||
<span>BMP (.bmp)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'check']" class="text-success text-xs" />
|
||||
<span>WebP (.webp)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'check']" class="text-success text-xs" />
|
||||
<span>SVG (.svg)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片信息 -->
|
||||
<div v-if="selectedImage" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">图片信息</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">文件名:</span>
|
||||
<span class="text-primary font-medium">{{ imageInfo.name }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">格式:</span>
|
||||
<span class="text-primary font-medium">{{ imageInfo.type }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">大小:</span>
|
||||
<span class="text-primary font-medium">{{ formatFileSize(imageInfo.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">宽度:</span>
|
||||
<span class="text-primary font-medium">{{ imageInfo.width }}px</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">高度:</span>
|
||||
<span class="text-primary font-medium">{{ imageInfo.height }}px</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">宽高比:</span>
|
||||
<span class="text-primary font-medium">{{ imageInfo.aspectRatio }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 转换设置 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">转换设置</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- ICO尺寸设置 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">ICO尺寸 (像素)</label>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<button
|
||||
v-for="size in icoSizes"
|
||||
:key="size"
|
||||
@click="selectIcoSize(size)"
|
||||
:class="[
|
||||
'p-2 text-sm rounded border transition-colors',
|
||||
selectedSizes.includes(size)
|
||||
? 'border-primary bg-primary text-white'
|
||||
: 'border-border hover:border-primary text-secondary'
|
||||
]"
|
||||
>
|
||||
{{ size }}×{{ size }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-tertiary mt-1">
|
||||
可选择多个尺寸,生成多尺寸ICO文件
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义尺寸 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">自定义尺寸</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
v-model.number="customSize"
|
||||
type="number"
|
||||
min="16"
|
||||
max="256"
|
||||
class="input-field w-20 text-sm"
|
||||
placeholder="32"
|
||||
>
|
||||
<span class="text-secondary text-sm">像素</span>
|
||||
<button
|
||||
@click="addCustomSize"
|
||||
:disabled="!isValidCustomSize"
|
||||
class="btn-sm btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'plus']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片质量设置 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">
|
||||
图片质量: {{ quality }}%
|
||||
</label>
|
||||
<input
|
||||
v-model.number="quality"
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
step="5"
|
||||
class="w-full"
|
||||
>
|
||||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||||
<span>较小文件</span>
|
||||
<span>较高质量</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背景颜色设置 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">背景颜色 (透明图片)</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
v-model="backgroundColor"
|
||||
type="color"
|
||||
class="w-12 h-8 border border-border rounded cursor-pointer"
|
||||
>
|
||||
<span class="text-sm text-secondary">{{ backgroundColor }}</span>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input
|
||||
v-model="preserveTransparency"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
>
|
||||
<span class="text-sm text-secondary">保持透明度</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览和结果区域 -->
|
||||
<div class="space-y-4">
|
||||
<!-- 原图预览 -->
|
||||
<div v-if="selectedImage" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">原图预览</h3>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="max-w-full max-h-64 overflow-hidden border border-border rounded-lg">
|
||||
<img
|
||||
:src="imagePreview"
|
||||
:alt="imageInfo.name"
|
||||
class="max-w-full max-h-64 object-contain"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICO预览 -->
|
||||
<div v-if="icoPreview" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">ICO预览</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 多尺寸预览 -->
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="size in selectedSizes"
|
||||
:key="size"
|
||||
class="text-center"
|
||||
>
|
||||
<div class="border border-border rounded p-2 bg-checkerboard">
|
||||
<img
|
||||
:src="icoPreview"
|
||||
:alt="`ICO ${size}x${size}`"
|
||||
:style="{ width: size + 'px', height: size + 'px' }"
|
||||
class="mx-auto object-contain"
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs text-secondary mt-1">{{ size }}×{{ size }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICO文件信息 -->
|
||||
<div v-if="icoInfo" class="bg-block rounded-lg p-3 text-sm">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">文件大小:</span>
|
||||
<span class="text-primary font-medium">{{ formatFileSize(icoInfo.size) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">包含尺寸:</span>
|
||||
<span class="text-primary font-medium">{{ icoInfo.iconCount }}个</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">格式:</span>
|
||||
<span class="text-primary font-medium">ICO</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-secondary">颜色深度:</span>
|
||||
<span class="text-primary font-medium">32位</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 转换历史 -->
|
||||
<div v-if="conversionHistory.length > 0" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">转换历史</h3>
|
||||
|
||||
<div class="space-y-2 max-h-40 overflow-y-auto">
|
||||
<div
|
||||
v-for="(record, index) in conversionHistory"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-2 bg-block rounded text-sm"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-primary">{{ record.filename }}</div>
|
||||
<div class="text-xs text-tertiary">
|
||||
{{ record.sizes.join(', ') }} | {{ record.time }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="downloadHistoryFile(record)"
|
||||
class="text-secondary hover:text-primary transition-colors"
|
||||
title="重新下载"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'download']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">使用说明</h3>
|
||||
|
||||
<div class="space-y-3 text-sm text-secondary">
|
||||
<div>
|
||||
<div class="font-medium">ICO格式特点:</div>
|
||||
<ul class="list-disc list-inside text-tertiary ml-2 mt-1">
|
||||
<li>Windows图标标准格式</li>
|
||||
<li>支持多尺寸存储在一个文件中</li>
|
||||
<li>常用尺寸: 16×16, 32×32, 48×48, 256×256</li>
|
||||
<li>支持透明背景</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium">转换建议:</div>
|
||||
<ul class="list-disc list-inside text-tertiary ml-2 mt-1">
|
||||
<li>使用正方形图片效果最佳</li>
|
||||
<li>PNG格式可保持透明度</li>
|
||||
<li>选择多个尺寸以适应不同显示场景</li>
|
||||
<li>16×16和32×32是Windows系统最常用尺寸</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态消息 -->
|
||||
<div v-if="statusMessage" class="card p-4">
|
||||
<div :class="[
|
||||
'flex items-center space-x-2',
|
||||
statusType === 'success' ? 'text-success' : 'text-error'
|
||||
]">
|
||||
<FontAwesomeIcon
|
||||
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
|
||||
/>
|
||||
<span>{{ statusMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { useLanguage } from '@/composables/useLanguage'
|
||||
|
||||
const { t } = useLanguage()
|
||||
|
||||
// 响应式状态
|
||||
const selectedImage = ref<File | null>(null)
|
||||
const imagePreview = ref('')
|
||||
const icoData = ref<Blob | null>(null)
|
||||
const icoPreview = ref('')
|
||||
const isDragOver = ref(false)
|
||||
const quality = ref(90)
|
||||
const backgroundColor = ref('#ffffff')
|
||||
const preserveTransparency = ref(true)
|
||||
const customSize = ref<number | null>(null)
|
||||
const statusMessage = ref('')
|
||||
const statusType = ref<'success' | 'error'>('success')
|
||||
|
||||
// DOM引用
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
// 图片信息
|
||||
const imageInfo = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
size: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
aspectRatio: ''
|
||||
})
|
||||
|
||||
// ICO信息
|
||||
const icoInfo = reactive({
|
||||
size: 0,
|
||||
iconCount: 0
|
||||
})
|
||||
|
||||
// ICO尺寸选项
|
||||
const icoSizes = [16, 24, 32, 48, 64, 96, 128, 256]
|
||||
const selectedSizes = ref<number[]>([16, 32, 48])
|
||||
|
||||
// 转换历史
|
||||
const conversionHistory = ref<Array<{
|
||||
filename: string
|
||||
sizes: string[]
|
||||
time: string
|
||||
data: Blob
|
||||
}>>([])
|
||||
|
||||
// 计算属性
|
||||
const isValidCustomSize = computed(() => {
|
||||
return customSize.value &&
|
||||
customSize.value >= 16 &&
|
||||
customSize.value <= 256 &&
|
||||
!selectedSizes.value.includes(customSize.value)
|
||||
})
|
||||
|
||||
// 文件选择
|
||||
const selectFile = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (file) {
|
||||
handleImageFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽处理
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragOver.value = false
|
||||
|
||||
const files = event.dataTransfer?.files
|
||||
if (files && files.length > 0) {
|
||||
handleImageFile(files[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片文件
|
||||
const handleImageFile = async (file: File) => {
|
||||
// 验证文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showStatus('请选择有效的图片文件', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
selectedImage.value = file
|
||||
|
||||
// 更新图片信息
|
||||
imageInfo.name = file.name
|
||||
imageInfo.type = file.type
|
||||
imageInfo.size = file.size
|
||||
|
||||
// 创建预览
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
imagePreview.value = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
// 获取图片尺寸
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
imageInfo.width = img.width
|
||||
imageInfo.height = img.height
|
||||
imageInfo.aspectRatio = `${(img.width / img.height).toFixed(2)}:1`
|
||||
|
||||
// 如果图片不是正方形,给出提示
|
||||
if (img.width !== img.height) {
|
||||
showStatus('建议使用正方形图片以获得最佳ICO效果', 'error')
|
||||
}
|
||||
}
|
||||
img.src = URL.createObjectURL(file)
|
||||
|
||||
// 清除之前的ICO数据
|
||||
icoData.value = null
|
||||
icoPreview.value = ''
|
||||
}
|
||||
|
||||
// ICO尺寸选择
|
||||
const selectIcoSize = (size: number) => {
|
||||
const index = selectedSizes.value.indexOf(size)
|
||||
if (index >= 0) {
|
||||
selectedSizes.value.splice(index, 1)
|
||||
} else {
|
||||
selectedSizes.value.push(size)
|
||||
}
|
||||
selectedSizes.value.sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
// 添加自定义尺寸
|
||||
const addCustomSize = () => {
|
||||
if (customSize.value && isValidCustomSize.value) {
|
||||
selectedSizes.value.push(customSize.value)
|
||||
selectedSizes.value.sort((a, b) => a - b)
|
||||
customSize.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为ICO
|
||||
const convertToIco = async () => {
|
||||
if (!selectedImage.value) {
|
||||
showStatus('请先选择图片', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedSizes.value.length === 0) {
|
||||
showStatus('请至少选择一个ICO尺寸', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('正在转换中...', 'success')
|
||||
|
||||
// 创建canvas来处理图片
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('无法创建canvas上下文')
|
||||
}
|
||||
|
||||
// 加载原图
|
||||
const img = new Image()
|
||||
img.src = imagePreview.value
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = reject
|
||||
})
|
||||
|
||||
// 生成多尺寸图标数据
|
||||
const iconData: Array<{ size: number; data: Uint8Array }> = []
|
||||
|
||||
for (const size of selectedSizes.value) {
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
|
||||
// 设置背景颜色(如果不保持透明度)
|
||||
if (!preserveTransparency.value) {
|
||||
ctx.fillStyle = backgroundColor.value
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
}
|
||||
|
||||
// 绘制图片
|
||||
ctx.drawImage(img, 0, 0, size, size)
|
||||
|
||||
// 获取图片数据
|
||||
const imageData = ctx.getImageData(0, 0, size, size)
|
||||
iconData.push({
|
||||
size,
|
||||
data: new Uint8Array(imageData.data)
|
||||
})
|
||||
}
|
||||
|
||||
// 生成ICO文件数据(简化实现)
|
||||
const icoBlob = await createIcoBlob(iconData)
|
||||
icoData.value = icoBlob
|
||||
|
||||
// 创建预览
|
||||
icoPreview.value = URL.createObjectURL(icoBlob)
|
||||
|
||||
// 更新ICO信息
|
||||
icoInfo.size = icoBlob.size
|
||||
icoInfo.iconCount = selectedSizes.value.length
|
||||
|
||||
// 添加到历史记录
|
||||
conversionHistory.value.unshift({
|
||||
filename: selectedImage.value.name.replace(/\.[^/.]+$/, '.ico'),
|
||||
sizes: selectedSizes.value.map(s => `${s}×${s}`),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
data: icoBlob
|
||||
})
|
||||
|
||||
// 保持历史记录不超过10条
|
||||
if (conversionHistory.value.length > 10) {
|
||||
conversionHistory.value = conversionHistory.value.slice(0, 10)
|
||||
}
|
||||
|
||||
showStatus('转换成功!', 'success')
|
||||
|
||||
} catch (error) {
|
||||
showStatus('转换失败: ' + (error instanceof Error ? error.message : '未知错误'), 'error')
|
||||
icoData.value = null
|
||||
icoPreview.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 创建ICO文件数据 (简化实现)
|
||||
const createIcoBlob = async (iconData: Array<{ size: number; data: Uint8Array }>): Promise<Blob> => {
|
||||
// 这是一个简化的ICO文件格式实现
|
||||
// 实际应用中建议使用专门的ICO库
|
||||
|
||||
const iconCount = iconData.length
|
||||
const headerSize = 6 + iconCount * 16 // ICO文件头 + 图标目录项
|
||||
|
||||
// 计算每个图标的PNG数据
|
||||
const pngData: Uint8Array[] = []
|
||||
for (const icon of iconData) {
|
||||
// 将RGBA数据转换为PNG (这里简化处理,实际需要PNG编码)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = icon.size
|
||||
canvas.height = icon.size
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(icon.data),
|
||||
icon.size,
|
||||
icon.size
|
||||
)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
|
||||
// 获取PNG数据
|
||||
const dataUrl = canvas.toDataURL('image/png', quality.value / 100)
|
||||
const base64 = dataUrl.split(',')[1]
|
||||
const binary = atob(base64)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i)
|
||||
}
|
||||
pngData.push(bytes)
|
||||
}
|
||||
|
||||
// 计算总文件大小
|
||||
let totalSize = headerSize
|
||||
for (const png of pngData) {
|
||||
totalSize += png.length
|
||||
}
|
||||
|
||||
// 构建ICO文件
|
||||
const icoFile = new Uint8Array(totalSize)
|
||||
let offset = 0
|
||||
|
||||
// ICO文件头
|
||||
icoFile[0] = 0 // 保留字段
|
||||
icoFile[1] = 0
|
||||
icoFile[2] = 1 // 类型: ICO
|
||||
icoFile[3] = 0
|
||||
icoFile[4] = iconCount & 0xFF // 图标数量
|
||||
icoFile[5] = (iconCount >> 8) & 0xFF
|
||||
offset = 6
|
||||
|
||||
// 图标目录项
|
||||
let dataOffset = headerSize
|
||||
for (let i = 0; i < iconCount; i++) {
|
||||
const size = iconData[i].size
|
||||
const pngSize = pngData[i].length
|
||||
|
||||
icoFile[offset] = size === 256 ? 0 : size // 宽度
|
||||
icoFile[offset + 1] = size === 256 ? 0 : size // 高度
|
||||
icoFile[offset + 2] = 0 // 颜色数
|
||||
icoFile[offset + 3] = 0 // 保留
|
||||
icoFile[offset + 4] = 1 // 颜色平面数
|
||||
icoFile[offset + 5] = 0
|
||||
icoFile[offset + 6] = 32 // 位深度
|
||||
icoFile[offset + 7] = 0
|
||||
|
||||
// PNG数据大小
|
||||
icoFile[offset + 8] = pngSize & 0xFF
|
||||
icoFile[offset + 9] = (pngSize >> 8) & 0xFF
|
||||
icoFile[offset + 10] = (pngSize >> 16) & 0xFF
|
||||
icoFile[offset + 11] = (pngSize >> 24) & 0xFF
|
||||
|
||||
// PNG数据偏移
|
||||
icoFile[offset + 12] = dataOffset & 0xFF
|
||||
icoFile[offset + 13] = (dataOffset >> 8) & 0xFF
|
||||
icoFile[offset + 14] = (dataOffset >> 16) & 0xFF
|
||||
icoFile[offset + 15] = (dataOffset >> 24) & 0xFF
|
||||
|
||||
offset += 16
|
||||
dataOffset += pngSize
|
||||
}
|
||||
|
||||
// 写入PNG数据
|
||||
for (const png of pngData) {
|
||||
icoFile.set(png, offset)
|
||||
offset += png.length
|
||||
}
|
||||
|
||||
return new Blob([icoFile], { type: 'image/x-icon' })
|
||||
}
|
||||
|
||||
// 下载ICO
|
||||
const downloadIco = () => {
|
||||
if (!icoData.value || !selectedImage.value) return
|
||||
|
||||
const filename = selectedImage.value.name.replace(/\.[^/.]+$/, '.ico')
|
||||
const url = URL.createObjectURL(icoData.value)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
showStatus('ICO文件下载完成', 'success')
|
||||
}
|
||||
|
||||
// 从历史记录下载
|
||||
const downloadHistoryFile = (record: any) => {
|
||||
const url = URL.createObjectURL(record.data)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = record.filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 加载示例图片
|
||||
const loadSample = () => {
|
||||
// 创建一个简单的示例图片
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 128
|
||||
canvas.height = 128
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
// 绘制渐变背景
|
||||
const gradient = ctx.createLinearGradient(0, 0, 128, 128)
|
||||
gradient.addColorStop(0, '#3b82f6')
|
||||
gradient.addColorStop(1, '#1d4ed8')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, 128, 128)
|
||||
|
||||
// 绘制图标形状
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = 'bold 60px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText('ICO', 64, 64)
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const file = new File([blob], 'sample.png', { type: 'image/png' })
|
||||
handleImageFile(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清除所有
|
||||
const clearAll = () => {
|
||||
selectedImage.value = null
|
||||
imagePreview.value = ''
|
||||
icoData.value = null
|
||||
icoPreview.value = ''
|
||||
selectedSizes.value = [16, 32, 48]
|
||||
customSize.value = null
|
||||
statusMessage.value = ''
|
||||
|
||||
// 重置图片信息
|
||||
Object.assign(imageInfo, {
|
||||
name: '',
|
||||
type: '',
|
||||
size: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
aspectRatio: ''
|
||||
})
|
||||
|
||||
// 重置ICO信息
|
||||
Object.assign(icoInfo, {
|
||||
size: 0,
|
||||
iconCount: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 显示状态消息
|
||||
const showStatus = (message: string, type: 'success' | 'error') => {
|
||||
statusMessage.value = message
|
||||
statusType.value = type
|
||||
setTimeout(() => {
|
||||
statusMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-checkerboard {
|
||||
background-image:
|
||||
linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
|
||||
background-size: 8px 8px;
|
||||
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user