Files
InfoGenie/InfoGenie-frontend/public/toolbox/图片处理/图片png转ico格式/index.html
2026-03-28 20:59:52 +08:00

266 lines
8.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PNG 转 ICO</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 24px; line-height: 1.5; }
.row { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 16px; max-width: 860px; }
.sizes { display: flex; gap: 10px; flex-wrap: wrap; margin: 8px 0 12px; }
label { user-select: none; }
button { padding: 10px 14px; border-radius: 10px; border: 1px solid #ddd; cursor: pointer; }
button:disabled { opacity: .6; cursor: not-allowed; }
.hint { color: #555; font-size: 14px; }
canvas { border: 1px dashed #ccc; border-radius: 8px; }
.preview { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.preview-item { text-align: center; font-size: 12px; color: #555; }
</style>
</head>
<body>
<h2>PNG 转化 ICO</h2>
<div class="card">
<div class="row">
<input id="file" type="file" accept="image/png" />
<button id="convert" disabled>转换并下载 .ico</button>
</div>
<div class="hint" style="margin-top:10px;">
默认生成16/32/48/64/128/256。<br>
建议上传至少 256×256 的 PNG避免小图强行放大后发糊。
</div>
<div style="margin-top:14px;">
<div><strong>选择尺寸:</strong></div>
<div class="sizes" id="sizes"></div>
<div class="row">
<label>缩放方式:
<select id="fit">
<option value="contain" selected>Contain完整显示留透明边</option>
<option value="cover">Cover铺满裁切</option>
</select>
</label>
<label>
背景:
<select id="bg">
<option value="transparent" selected>透明</option>
<option value="white">白色</option>
<option value="black">黑色</option>
</select>
</label>
</div>
</div>
<div class="preview" id="preview"></div>
</div>
<script>
// 常用 icon 尺寸
const DEFAULT_SIZES = [16, 32, 48, 64, 128, 256];
const sizesWrap = document.getElementById('sizes');
const previewWrap = document.getElementById('preview');
const fileInput = document.getElementById('file');
const convertBtn = document.getElementById('convert');
// 生成尺寸复选框
for (const s of DEFAULT_SIZES) {
const id = `sz_${s}`;
const label = document.createElement('label');
label.innerHTML = `<input type="checkbox" id="${id}" value="${s}" checked> ${s}×${s}`;
sizesWrap.appendChild(label);
}
let currentImageBitmap = null;
fileInput.addEventListener('change', async () => {
previewWrap.innerHTML = '';
currentImageBitmap = null;
convertBtn.disabled = true;
const file = fileInput.files?.[0];
if (!file) return;
if (file.type !== 'image/png') {
alert('请上传 PNG 文件');
return;
}
const bitmap = await createImageBitmap(file);
currentImageBitmap = bitmap;
// 生成简单预览(只预览 64 和 128
for (const s of [64, 128]) {
const canvas = await renderToCanvas(bitmap, s, getFit(), getBg());
const item = document.createElement('div');
item.className = 'preview-item';
item.appendChild(canvas);
const cap = document.createElement('div');
cap.textContent = `${s}×${s}`;
item.appendChild(cap);
previewWrap.appendChild(item);
}
convertBtn.disabled = false;
});
convertBtn.addEventListener('click', async () => {
try {
if (!currentImageBitmap) return;
const sizes = getSelectedSizes();
if (sizes.length === 0) {
alert('请至少选择一个尺寸');
return;
}
convertBtn.disabled = true;
convertBtn.textContent = '转换中...';
// 1) 生成每个尺寸的 PNG ArrayBuffer
const pngBuffers = [];
for (const s of sizes) {
const canvas = await renderToCanvas(currentImageBitmap, s, getFit(), getBg());
const ab = await canvasToPngArrayBuffer(canvas);
pngBuffers.push({ size: s, buffer: ab });
}
// 2) 打包成 ICO每张图像使用 PNG payload
const icoBytes = buildIcoFromPngBuffers(pngBuffers);
// 3) 下载
const blob = new Blob([icoBytes], { type: 'image/x-icon' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'favicon.ico';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
console.error(e);
alert('转换失败:' + (e?.message || e));
} finally {
convertBtn.disabled = false;
convertBtn.textContent = '转换并下载 .ico';
}
});
function getSelectedSizes() {
const cbs = sizesWrap.querySelectorAll('input[type="checkbox"]');
const sizes = [];
cbs.forEach(cb => { if (cb.checked) sizes.push(parseInt(cb.value, 10)); });
// 从小到大排序(无硬性要求,但更直观)
sizes.sort((a,b) => a - b);
return sizes;
}
function getFit() {
return document.getElementById('fit').value;
}
function getBg() {
return document.getElementById('bg').value;
}
async function renderToCanvas(bitmap, size, fit = 'contain', bg = 'transparent') {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// 背景
if (bg !== 'transparent') {
ctx.fillStyle = bg;
ctx.fillRect(0, 0, size, size);
} else {
ctx.clearRect(0, 0, size, size);
}
const iw = bitmap.width, ih = bitmap.height;
// 计算绘制区域
let dw, dh, dx, dy;
const scaleContain = Math.min(size / iw, size / ih);
const scaleCover = Math.max(size / iw, size / ih);
const scale = (fit === 'cover') ? scaleCover : scaleContain;
dw = Math.round(iw * scale);
dh = Math.round(ih * scale);
dx = Math.round((size - dw) / 2);
dy = Math.round((size - dh) / 2);
// 更好的缩放质量
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(bitmap, dx, dy, dw, dh);
// cover 模式下如果超出画布drawImage 已经裁切了
return canvas;
}
function canvasToPngArrayBuffer(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob(async (blob) => {
try {
if (!blob) return reject(new Error('canvas.toBlob() 返回空'));
const ab = await blob.arrayBuffer();
resolve(ab);
} catch (e) {
reject(e);
}
}, 'image/png');
});
}
// 按 ICO 格式打包ICONDIR(6) + ICONDIRENTRY(n*16) + PNG data...
// 目录项字段常见解释width/height(1B, 0 表示 256), colorCount(1B), reserved(1B),
// planes(2B), bitCount(2B), bytesInRes(4B), imageOffset(4B)
function buildIcoFromPngBuffers(pngs) {
const count = pngs.length;
const headerSize = 6;
const entrySize = 16;
const dirSize = headerSize + count * entrySize;
// 计算总大小
let totalSize = dirSize;
for (const p of pngs) totalSize += p.buffer.byteLength;
const out = new Uint8Array(totalSize);
const dv = new DataView(out.buffer);
// ICONDIR
dv.setUint16(0, 0, true); // reserved
dv.setUint16(2, 1, true); // type = 1 (icon)
dv.setUint16(4, count, true); // count
// ICONDIRENTRYs
let dataOffset = dirSize;
for (let i = 0; i < count; i++) {
const { size, buffer } = pngs[i];
const entryOff = headerSize + i * entrySize;
dv.setUint8(entryOff + 0, size >= 256 ? 0 : size); // width
dv.setUint8(entryOff + 1, size >= 256 ? 0 : size); // height
dv.setUint8(entryOff + 2, 0); // colorCount
dv.setUint8(entryOff + 3, 0); // reserved
dv.setUint16(entryOff + 4, 1, true); // planes (常用 1)
dv.setUint16(entryOff + 6, 32, true); // bitCount (常用 32)
dv.setUint32(entryOff + 8, buffer.byteLength, true); // bytesInRes
dv.setUint32(entryOff + 12, dataOffset, true); // imageOffset
// 拷贝 PNG 数据
out.set(new Uint8Array(buffer), dataOffset);
dataOffset += buffer.byteLength;
}
return out;
}
</script>
</body>
</html>