feat: 图片转 ICO 支持逐尺寸单独下载与多尺寸打包 ZIP
This commit is contained in:
parent
e65da3ac8f
commit
00013ee112
@ -1,6 +1,8 @@
|
|||||||
# tool.xpcool.com 变更记录
|
# tool.xpcool.com 变更记录
|
||||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
> 倒序:最新在上。格式: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-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(相对路径索引)
|
2026-08-26 | CFG | CHANGELOG 纳入 git 管理(.gitignore 放行 .workbuddy/memory/CHANGELOG.md),中央工作空间迁移至 ../workbuddy.xpcool.com(相对路径索引)
|
||||||
|
|||||||
@ -55,6 +55,8 @@ export interface IcoBuildResult {
|
|||||||
quantized: boolean
|
quantized: boolean
|
||||||
/** 各尺寸的预览图(DataURL),供页面回显 */
|
/** 各尺寸的预览图(DataURL),供页面回显 */
|
||||||
previews: Array<{ size: number; url: string }>
|
previews: Array<{ size: number; url: string }>
|
||||||
|
/** 每个尺寸单独封装的 ICO 文件(与合成包内条目数据一致,用于逐尺寸下载 / 打包 ZIP) */
|
||||||
|
singles: Array<{ size: number; blob: Blob }>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ICO 内部单条目:尺寸 + 编码后的图像数据 */
|
/** ICO 内部单条目:尺寸 + 编码后的图像数据 */
|
||||||
@ -727,12 +729,15 @@ export async function buildIco(source: HTMLImageElement, params: IcoBuildParams)
|
|||||||
|
|
||||||
const blob = encodeIco(encoded.images)
|
const blob = encodeIco(encoded.images)
|
||||||
const previews = active.map((size) => ({ size, url: imageDataToDataUrl(rendered.get(size) as ImageData) }))
|
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 {
|
return {
|
||||||
blob,
|
blob,
|
||||||
entries: encoded.meta,
|
entries: encoded.meta,
|
||||||
droppedSizes: sizes.filter((s) => !active.includes(s)),
|
droppedSizes: sizes.filter((s) => !active.includes(s)),
|
||||||
quantized: quantize,
|
quantized: quantize,
|
||||||
previews,
|
previews,
|
||||||
|
singles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -111,12 +111,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<t-button theme="primary" @click="downloadIco">下载 ICO</t-button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-divider />
|
<t-divider />
|
||||||
|
|
||||||
<p class="grid-title">各尺寸预览(点击右下角可单独下载 PNG)</p>
|
<p class="grid-title">各尺寸预览(点击右下角可单独下载 PNG / ICO)</p>
|
||||||
<div class="preview-grid">
|
<div class="preview-grid">
|
||||||
<div v-for="p in result.previews" :key="p.size" class="preview-cell">
|
<div v-for="p in result.previews" :key="p.size" class="preview-cell">
|
||||||
<div class="preview-box checker">
|
<div class="preview-box checker">
|
||||||
@ -124,7 +128,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="preview-meta">
|
<div class="preview-meta">
|
||||||
<span class="preview-size">{{ p.size }}×{{ p.size }}</span>
|
<span class="preview-size">{{ p.size }}×{{ p.size }}</span>
|
||||||
<t-button size="small" variant="text" theme="primary" @click="downloadPng(p)">PNG</t-button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,6 +143,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
|
import JSZip from 'jszip'
|
||||||
import UploadDrop from '@/components/common/UploadDrop.vue'
|
import UploadDrop from '@/components/common/UploadDrop.vue'
|
||||||
import ErrorAlert from '@/components/common/ErrorAlert.vue'
|
import ErrorAlert from '@/components/common/ErrorAlert.vue'
|
||||||
import HistoryPanel from '@/components/common/HistoryPanel.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`)
|
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) {
|
function onHistorySelect(text: string) {
|
||||||
const sMatch = /sizes=([\d,]+)/.exec(text)
|
const sMatch = /sizes=([\d,]+)/.exec(text)
|
||||||
@ -457,6 +490,10 @@ onBeforeUnmount(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
.preview-btns {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.preview-size {
|
.preview-size {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user