<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片转 ICO</title>
</head>
<body>
<div class="container">
<h1>🎨 图片转 ICO</h1>
<div class="alert-message" id="alertMessage"></div>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽图片到此处</div>
</div>
<input type="file" id="fileInput" accept="image/*">
<div class="preview-section" id="previewSection">
<img id="previewImage" class="preview-image" alt="预览图片">
</div>
<div class="options-section" id="optionsSection">
<div class="option-group">
<label class="option-label">输出尺寸(可多选)</label>
<div class="size-options">
<div>
<input type="checkbox" id="size16" class="size-checkbox" value="16">
<label for="size16" class="size-label">16×16</label>
</div>
<div>
<input type="checkbox" id="size32" class="size-checkbox" value="32">
<label for="size32" class="size-label">32×32</label>
</div>
<div>
<input type="checkbox" id="size48" class="size-checkbox" value="48">
<label for="size48" class="size-label">48×48</label>
</div>
<div>
<input type="checkbox" id="size64" class="size-checkbox" value="64" checked>
<label for="size64" class="size-label">64×64</label>
</div>
<div>
<input type="checkbox" id="size128" class="size-checkbox" value="128">
<label for="size128" class="size-label">128×128</label>
</div>
<div>
<input type="checkbox" id="size256" class="size-checkbox" value="256">
<label for="size256" class="size-label">256×256</label>
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-secondary" id="resetBtn">重新选择</button>
<button class="btn btn-primary" id="convertBtn">转换为 ICO</button>
</div>
<div class="processing" id="processing">
<div class="spinner"></div>
<div>正在转换中...</div>
</div>
</div>
</div>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.container {
max-width: 800px;
width: 100%;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 32px;
}
.upload-area {
border: 3px dashed #667eea;
border-radius: 10px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 30px;
background: #f8f9ff;
}
.upload-area:hover {
border-color: #764ba2;
background: #f0f2ff;
}
.upload-area.drag-over {
border-color: #764ba2;
background: #e8ebff;
transform: scale(1.02);
}
.upload-icon {
font-size: 48px;
margin-bottom: 10px;
}
.upload-text {
color: #666;
font-size: 16px;
}
#fileInput {
display: none;
}
.preview-section {
display: none;
margin-bottom: 30px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: block;
margin: 0 auto 20px;
}
.options-section {
display: none;
}
.option-group {
margin-bottom: 25px;
}
.option-label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 12px;
font-size: 16px;
}
.size-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
}
.size-checkbox {
display: none;
}
.size-label {
display: block;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
color: #666;
}
.size-label:hover {
border-color: #667eea;
background: #f8f9ff;
}
.size-checkbox:checked+.size-label {
border-color: #667eea;
background: #667eea;
color: white;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
flex: 1;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f0f0f0;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.info-text {
text-align: center;
color: #999;
font-size: 14px;
margin-top: 20px;
}
.alert-message {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-size: 14px;
animation: slideDown 0.3s ease-out;
}
.alert-message.show {
display: block;
}
.alert-error {
background: #fee;
border: 1px solid #fcc;
color: #c33;
}
.alert-success {
background: #efe;
border: 1px solid #cfc;
color: #3c3;
}
.alert-warning {
background: #ffeaa7;
border: 1px solid #fdcb6e;
color: #d63031;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.processing {
text-align: center;
color: #667eea;
font-weight: 600;
display: none;
margin-top: 20px;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const previewSection = document.getElementById('previewSection');
const previewImage = document.getElementById('previewImage');
const optionsSection = document.getElementById('optionsSection');
const convertBtn = document.getElementById('convertBtn');
const resetBtn = document.getElementById('resetBtn');
const processing = document.getElementById('processing');
const alertMessage = document.getElementById('alertMessage');
let currentImage = null;
// 显示提示信息
function showAlert(message, type = 'error') {
alertMessage.textContent = message;
alertMessage.className = 'alert-message alert-' + type + ' show';
// 3秒后自动隐藏
setTimeout(() => {
alertMessage.classList.remove('show');
}, 3000);
}
// 点击上传区域
uploadArea.addEventListener('click', () => {
fileInput.click();
});
// 文件选择
fileInput.addEventListener('change', (e) => {
handleFile(e.target.files[0]);
});
// 拖拽上传
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
handleFile(e.dataTransfer.files[0]);
});
// 重置按钮
resetBtn.addEventListener('click', () => {
currentImage = null;
fileInput.value = '';
previewSection.style.display = 'none';
optionsSection.style.display = 'none';
uploadArea.style.display = 'block';
});
// 转换按钮
convertBtn.addEventListener('click', async () => {
const selectedSizes = getSelectedSizes();
if (selectedSizes.length === 0) {
showAlert('⚠️ 请至少选择一个输出尺寸!', 'warning');
return;
}
await convertToICO(currentImage, selectedSizes);
});
// 处理文件
function handleFile(file) {
if (!file || !file.type.startsWith('image/')) {
showAlert('❌ 请选择有效的图片文件!', 'error');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
currentImage = img;
previewImage.src = e.target.result;
uploadArea.style.display = 'none';
previewSection.style.display = 'block';
optionsSection.style.display = 'block';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 获取选中的尺寸
function getSelectedSizes() {
const checkboxes = document.querySelectorAll('.size-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value));
}
// 转换为 ICO
async function convertToICO(img, sizes) {
try {
processing.style.display = 'block';
convertBtn.disabled = true;
// 生成所有尺寸的图像数据
const images = [];
for (const size of sizes) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// 使用高质量缩放
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 绘制图像
ctx.drawImage(img, 0, 0, size, size);
// 转换为 PNG 数据
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
const arrayBuffer = await blob.arrayBuffer();
const pngData = new Uint8Array(arrayBuffer);
images.push({
size: size,
data: pngData
});
}
// 创建 ICO 文件
const icoData = createICO(images);
// 下载文件
const blob = new Blob([icoData], { type: 'image/x-icon' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'favicon.ico';
a.click();
URL.revokeObjectURL(url);
showAlert('✅ 转换成功!ICO 文件已开始下载。', 'success');
} catch (error) {
console.error('转换失败:', error);
showAlert('❌ 转换失败:' + error.message, 'error');
} finally {
processing.style.display = 'none';
convertBtn.disabled = false;
}
}
// 创建 ICO 文件
function createICO(images) {
// ICO 文件头(6 字节)
const header = new Uint8Array(6);
header[0] = 0; // Reserved
header[1] = 0;
header[2] = 1; // Type: 1 = ICO
header[3] = 0;
header[4] = images.length & 0xFF; // 图像数量
header[5] = (images.length >> 8) & 0xFF;
// 计算每个图像的目录条目和数据偏移
let offset = 6 + (images.length * 16); // 头部 + 所有目录条目
const entries = [];
const datas = [];
for (const img of images) {
// 图像目录条目(16 字节)
const entry = new Uint8Array(16);
entry[0] = img.size === 256 ? 0 : img.size; // 宽度(256 用 0 表示)
entry[1] = img.size === 256 ? 0 : img.size; // 高度(256 用 0 表示)
entry[2] = 0; // 调色板颜色数
entry[3] = 0; // Reserved
entry[4] = 1; // Color planes
entry[5] = 0;
entry[6] = 32; // Bits per pixel
entry[7] = 0;
// 图像数据大小
const size = img.data.length;
entry[8] = size & 0xFF;
entry[9] = (size >> 8) & 0xFF;
entry[10] = (size >> 16) & 0xFF;
entry[11] = (size >> 24) & 0xFF;
// 图像数据偏移
entry[12] = offset & 0xFF;
entry[13] = (offset >> 8) & 0xFF;
entry[14] = (offset >> 16) & 0xFF;
entry[15] = (offset >> 24) & 0xFF;
entries.push(entry);
datas.push(img.data);
offset += img.data.length;
}
// 组合所有数据
const totalSize = offset;
const icoFile = new Uint8Array(totalSize);
let pos = 0;
// 写入头部
icoFile.set(header, pos);
pos += header.length;
// 写入所有目录条目
for (const entry of entries) {
icoFile.set(entry, pos);
pos += entry.length;
}
// 写入所有图像数据
for (const data of datas) {
icoFile.set(data, pos);
pos += data.length;
}
return icoFile;
}