feat: 图片转 ICO 支持逐尺寸单独下载与多尺寸打包 ZIP
This commit is contained in:
parent
e65da3ac8f
commit
00013ee112
@ -1,6 +1,8 @@
|
||||
# tool.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-09-01 | CHG | 图片转 ICO 结果区增强:逐尺寸单独 ICO 下载(buildIco 新增 singles 字段,复用条目数据零额外编码)+ 多尺寸一键打包 ZIP 下载(jszip);另修复 pnpm wrapper 中 node 版本路径(22.22.2 → 22.22.2-2)
|
||||
|
||||
2026-08-31 | CHG | 新增「图片转 ICO」工具:src/utils/ico.ts(纯前端自实现 PNG/BMP DIB/ICO 编码 + 中位切分量化 + 自适应滤波 + 体积上限自动降级),src/views/image-to-ico/ImageToIcoView.vue(尺寸组合/填充/编码方式/量化/体积限制/预览下载/历史),路由注册于 src/router/tools.ts;Pillow 独立解码验证通过
|
||||
|
||||
2026-08-26 | CFG | CHANGELOG 纳入 git 管理(.gitignore 放行 .workbuddy/memory/CHANGELOG.md),中央工作空间迁移至 ../workbuddy.xpcool.com(相对路径索引)
|
||||
|
||||
@ -55,6 +55,8 @@ export interface IcoBuildResult {
|
||||
quantized: boolean
|
||||
/** 各尺寸的预览图(DataURL),供页面回显 */
|
||||
previews: Array<{ size: number; url: string }>
|
||||
/** 每个尺寸单独封装的 ICO 文件(与合成包内条目数据一致,用于逐尺寸下载 / 打包 ZIP) */
|
||||
singles: Array<{ size: number; blob: Blob }>
|
||||
}
|
||||
|
||||
/** ICO 内部单条目:尺寸 + 编码后的图像数据 */
|
||||
@ -727,12 +729,15 @@ export async function buildIco(source: HTMLImageElement, params: IcoBuildParams)
|
||||
|
||||
const blob = encodeIco(encoded.images)
|
||||
const previews = active.map((size) => ({ size, url: imageDataToDataUrl(rendered.get(size) as ImageData) }))
|
||||
// 单尺寸 ICO 直接复用合成包里已编码的条目数据,只多包一层 6+16 字节的头部,几乎零开销
|
||||
const singles = encoded.images.map((img) => ({ size: img.size, blob: encodeIco([img]) }))
|
||||
return {
|
||||
blob,
|
||||
entries: encoded.meta,
|
||||
droppedSizes: sizes.filter((s) => !active.includes(s)),
|
||||
quantized: quantize,
|
||||
previews,
|
||||
singles,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -111,12 +111,16 @@
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<t-button theme="primary" @click="downloadIco">下载 ICO</t-button>
|
||||
<!-- 多个尺寸时支持把每个尺寸的单独 ICO 打包成 ZIP 一键下载 -->
|
||||
<t-button v-if="result.singles.length > 1" variant="outline" :loading="zipping" @click="downloadAllZip">
|
||||
打包下载 ZIP
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t-divider />
|
||||
|
||||
<p class="grid-title">各尺寸预览(点击右下角可单独下载 PNG)</p>
|
||||
<p class="grid-title">各尺寸预览(点击右下角可单独下载 PNG / ICO)</p>
|
||||
<div class="preview-grid">
|
||||
<div v-for="p in result.previews" :key="p.size" class="preview-cell">
|
||||
<div class="preview-box checker">
|
||||
@ -124,7 +128,10 @@
|
||||
</div>
|
||||
<div class="preview-meta">
|
||||
<span class="preview-size">{{ p.size }}×{{ p.size }}</span>
|
||||
<div class="preview-btns">
|
||||
<t-button size="small" variant="text" theme="primary" @click="downloadPng(p)">PNG</t-button>
|
||||
<t-button size="small" variant="text" theme="primary" @click="downloadSingleIco(p.size)">ICO</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,6 +143,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import JSZip from 'jszip'
|
||||
import UploadDrop from '@/components/common/UploadDrop.vue'
|
||||
import ErrorAlert from '@/components/common/ErrorAlert.vue'
|
||||
import HistoryPanel from '@/components/common/HistoryPanel.vue'
|
||||
@ -266,6 +274,31 @@ function downloadPng(p: { size: number; url: string }) {
|
||||
downloadBlob(dataUrlToBlob(p.url), `${base}_${p.size}x${p.size}.png`)
|
||||
}
|
||||
|
||||
/** 单独下载某个尺寸的 ICO 文件 */
|
||||
function downloadSingleIco(size: number) {
|
||||
const single = result.value?.singles.find((s) => s.size === size)
|
||||
if (!single) return
|
||||
const base = (sourceInfo.value?.name ?? 'icon').replace(/\.[^.]+$/, '')
|
||||
downloadBlob(single.blob, `${base}_${size}x${size}.ico`)
|
||||
}
|
||||
|
||||
/** 一键把所有单尺寸 ICO 打包成 ZIP 下载 */
|
||||
const zipping = ref(false)
|
||||
async function downloadAllZip() {
|
||||
const singles = result.value?.singles
|
||||
if (!singles?.length) return
|
||||
zipping.value = true
|
||||
try {
|
||||
const base = (sourceInfo.value?.name ?? 'icon').replace(/\.[^.]+$/, '')
|
||||
const zip = new JSZip()
|
||||
for (const s of singles) zip.file(`${base}_${s.size}x${s.size}.ico`, s.blob)
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
downloadBlob(blob, `${base}_ico.zip`)
|
||||
} finally {
|
||||
zipping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击历史:解析摘要串恢复转换参数 */
|
||||
function onHistorySelect(text: string) {
|
||||
const sMatch = /sizes=([\d,]+)/.exec(text)
|
||||
@ -457,6 +490,10 @@ onBeforeUnmount(() => {
|
||||
justify-content: space-between;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.preview-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.preview-size {
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-secondary);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user