update: 2026-03-28 20:59
This commit is contained in:
265
InfoGenie-frontend/public/toolbox/图片处理/图片png转ico格式/index.html
Normal file
265
InfoGenie-frontend/public/toolbox/图片处理/图片png转ico格式/index.html
Normal file
@@ -0,0 +1,265 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user