这是为您生成的“图片批量光标工具CUR制作”HTML文件,它是一款在浏览器中运行的离线工具,可以将多张图片批量转换为Windows光标(.cur)格式。

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">图片批量光标工具 · CUR制作</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #1e1e2f;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 1rem;
color: #e4e4f0;
}
.card {
max-width: 1000px;
width: 100%;
background: #2a2a3d;
padding: 2rem 1.8rem 2.2rem;
border-radius: 2.5rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
border: 1px solid #3d3d5a;
transition: all 0.2s;
}
h1 {
font-weight: 500;
font-size: 2.2rem;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
border-bottom: 2px solid #4b4b6b;
padding-bottom: 0.6rem;
margin-bottom: 1.8rem;
}
h1 small {
font-size: 1rem;
font-weight: 400;
color: #a8a8cf;
margin-left: auto;
}
.flex-row {
display: flex;
flex-wrap: wrap;
gap: 1.2rem;
align-items: flex-start;
justify-content: space-between;
}
.upload-area {
flex: 2 1 280px;
background: #22223b;
border: 2px dashed #5b5b85;
border-radius: 2rem;
padding: 1.6rem 1.2rem;
text-align: center;
transition: background 0.2s, border-color 0.2s;
cursor: pointer;
}
.upload-area:hover {
background: #2c2c48;
border-color: #8a8ac0;
}
.upload-area label {
display: block;
cursor: pointer;
}
.upload-area input[type="file"] {
display: none;
}
.upload-icon {
font-size: 3rem;
line-height: 1;
margin-bottom: 0.2rem;
}
.upload-text {
font-weight: 400;
background: #3a3a5a;
display: inline-block;
padding: 0.5rem 1.8rem;
border-radius: 40px;
margin: 0.5rem 0 0.2rem;
font-size: 1rem;
color: #d0d0f0;
border: 1px solid #6a6a96;
}
.hint {
font-size: 0.85rem;
color: #9d9dbc;
margin-top: 0.4rem;
}
.preview-panel {
flex: 3 1 400px;
background: #1c1c30;
border-radius: 2rem;
padding: 1rem 1.2rem;
border: 1px solid #404060;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 400;
letter-spacing: 0.3px;
margin-bottom: 0.8rem;
color: #c6c6e0;
}
.badge {
background: #3d3d62;
padding: 0.2rem 1rem;
border-radius: 40px;
font-size: 0.8rem;
color: #c8c8ff;
}
.preview-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
max-height: 180px;
overflow-y: auto;
padding: 4px 2px 8px;
scrollbar-width: thin;
scrollbar-color: #5a5a88 #2a2a40;
}
.preview-grid::-webkit-scrollbar {
width: 6px;
}
.preview-grid::-webkit-scrollbar-track {
background: #2a2a40;
border-radius: 20px;
}
.preview-grid::-webkit-scrollbar-thumb {
background: #5a5a88;
border-radius: 20px;
}
.img-thumb {
width: 60px;
height: 60px;
border-radius: 16px;
object-fit: cover;
border: 2px solid #4c4c72;
background: #25253b;
transition: 0.1s;
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
}
.img-thumb:hover {
border-color: #b0b0f0;
transform: scale(1.02);
}
.placeholder-thumb {
width: 60px;
height: 60px;
border-radius: 16px;
background: #2f2f4a;
display: flex;
align-items: center;
justify-content: center;
color: #6b6b99;
font-size: 1.6rem;
border: 2px dashed #4b4b71;
}
.action-bar {
margin-top: 1.8rem;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.8rem 1.2rem;
}
.size-options {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem 1.2rem;
background: #23233b;
padding: 0.5rem 1.2rem 0.5rem 1.5rem;
border-radius: 80px;
border: 1px solid #42426a;
}
.size-options label {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.95rem;
}
.size-options input[type="number"] {
width: 70px;
background: #1a1a2e;
border: 1px solid #5b5b8f;
color: white;
padding: 0.3rem 0.6rem;
border-radius: 40px;
font-weight: 400;
text-align: center;
}
.size-options input[type="number"]:focus {
outline: 2px solid #7b7bcf;
border-color: transparent;
}
.size-options span {
opacity: 0.7;
font-size: 0.9rem;
}
.hotspot-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
margin-left: 0.2rem;
}
.hotspot-group label {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.9rem;
}
.hotspot-group input[type="number"] {
width: 50px;
background: #1a1a2e;
border: 1px solid #5b5b8f;
color: white;
padding: 0.2rem 0.3rem;
border-radius: 30px;
text-align: center;
}
.btn-group {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
}
.btn {
background: #3f3f6b;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 40px;
font-weight: 500;
color: #f0f0ff;
font-size: 0.95rem;
border: 1px solid #6767a0;
transition: all 0.15s;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
letter-spacing: 0.3px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.btn-primary {
background: #5b5bb5;
border-color: #8a8ad4;
font-weight: 600;
}
.btn-primary:hover {
background: #6f6fd0;
border-color: #a8a8f0;
transform: scale(0.98);
box-shadow: 0 0 16px #6f6fd0aa;
}
.btn:active {
transform: scale(0.94);
}
.btn:disabled {
opacity: 0.5;
pointer-events: none;
filter: grayscale(0.4);
}
.footer-note {
margin-top: 1.6rem;
font-size: 0.78rem;
color: #7a7aa8;
text-align: center;
border-top: 1px solid #36365a;
padding-top: 1.2rem;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.4rem 1.6rem;
}
.footer-note a {
color: #a5a5e6;
text-decoration: none;
}
.log-area {
background: #1e1e34;
border-radius: 24px;
padding: 0.5rem 1rem;
margin-top: 0.8rem;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.8rem;
color: #b0b0da;
white-space: pre-wrap;
word-break: break-all;
max-height: 60px;
overflow-y: auto;
border: 1px solid #3d3d60;
}
@media (max-width: 720px) {
.card { padding: 1.5rem 1rem; }
.action-bar { flex-direction: column; align-items: stretch; }
.size-options { justify-content: center; }
.hotspot-group { justify-content: center; }
.btn-group { justify-content: center; }
}
</style>
</head>
<body>
<div class="card">
<h1>
🖱️ CUR 批量制作
<small>图片 → 光标</small>
</h1>
<div class="flex-row">
<!-- 上传区 -->
<div class="upload-area" id="dropZone">
<label for="fileInput">
<div class="upload-icon">🖼️📁</div>
<div class="upload-text">点击选择 或 拖拽图片</div>
<div class="hint">支持 PNG / BMP / JPG / GIF · 自动缩放</div>
</label>
<input type="file" id="fileInput" accept=".png,.bmp,.jpg,.jpeg,.gif,.webp" multiple>
</div>
<!-- 预览面板 -->
<div class="preview-panel">
<div class="preview-header">
<span>📸 图片预览</span>
<span class="badge" id="countBadge">0</span>
</div>
<div class="preview-grid" id="previewGrid">
<div class="placeholder-thumb">+</div>
<div class="placeholder-thumb">+</div>
</div>
</div>
</div>
<!-- 参数 & 操作 -->
<div class="action-bar">
<div class="size-options">
<label>
<span>↔️</span>
<input type="number" id="curWidth" value="32" min="8" max="128" step="1">
</label>
<span>×</span>
<label>
<span>↕️</span>
<input type="number" id="curHeight" value="32" min="8" max="128" step="1">
</label>
<span class="split">·</span>
<div class="hotspot-group">
<label>🎯<input type="number" id="hotX" value="0" min="0" max="128"></label>
<label>⬇️<input type="number" id="hotY" value="0" min="0" max="128"></label>
</div>
</div>
<div class="btn-group">
<button class="btn" id="clearBtn" title="清空列表">🗑️ 清空</button>
<button class="btn btn-primary" id="exportBtn">⬇️ 批量导出 CUR</button>
</div>
</div>
<!-- 简易日志 -->
<div class="log-area" id="logBox">📋 就绪 · 图片将按设定尺寸生成光标 (热点默认左上 0,0)</div>
<div class="footer-note">
<span>📌 生成 .cur (包含 PNG/BMP 数据) </span>
<span>🔹 热点: 可调 (超出自动裁剪) </span>
<span>⚡ 纯前端 · 图片不离开设备</span>
</div>
</div>
<script>
(function(){
const fileInput = document.getElementById('fileInput');
const previewGrid = document.getElementById('previewGrid');
const countBadge = document.getElementById('countBadge');
const logBox = document.getElementById('logBox');
const clearBtn = document.getElementById('clearBtn');
const exportBtn = document.getElementById('exportBtn');
const curWidthInp = document.getElementById('curWidth');
const curHeightInp = document.getElementById('curHeight');
const hotXInp = document.getElementById('hotX');
const hotYInp = document.getElementById('hotY');
// 存储文件列表 (File 对象)
let fileList = [];
// ---------- 辅助日志 ----------
function log(msg, isError = false) {
const prefix = isError ? '⚠️ ' : '✓ ';
logBox.textContent = prefix + msg;
console.log(msg);
}
// ---------- 更新预览网格 ----------
function updatePreview() {
previewGrid.innerHTML = '';
if (fileList.length === 0) {
// 占位
for (let i = 0; i < 3; i++) {
const placeholder = document.createElement('div');
placeholder.className = 'placeholder-thumb';
placeholder.textContent = '·';
previewGrid.appendChild(placeholder);
}
countBadge.textContent = '0';
return;
}
// 显示前24张 (太多会卡)
const showCount = Math.min(fileList.length, 24);
for (let i = 0; i < showCount; i++) {
const file = fileList[i];
const img = document.createElement('img');
img.className = 'img-thumb';
img.alt = file.name;
// 用objectURL预览
const url = URL.createObjectURL(file);
img.src = url;
// 释放内存 (但保留blob引用, 之后可能用到)
img.onload = () => URL.revokeObjectURL(url);
previewGrid.appendChild(img);
}
// 如果还有剩余显示 '+N'
const remain = fileList.length - showCount;
if (remain > 0) {
const extra = document.createElement('div');
extra.className = 'placeholder-thumb';
extra.textContent = `+${remain}`;
extra.style.fontSize = '1.2rem';
extra.style.fontWeight = 'bold';
previewGrid.appendChild(extra);
}
countBadge.textContent = fileList.length;
}
// ---------- 添加文件 (去重, 按名称+大小) ----------
function addFiles(files) {
let addedCount = 0;
for (const f of files) {
// 简单去重: 相同 filename + size
const exists = fileList.some(item => item.name === f.name && item.size === f.size);
if (!exists) {
// 检查是否为支持图像类型 (扩展名)
const type = f.type || '';
if (type.startsWith('image/') || /\.(png|bmp|jpg|jpeg|gif|webp)$/i.test(f.name)) {
fileList.push(f);
addedCount++;
} else {
log(`跳过非图像: ${f.name}`, true);
}
}
}
if (addedCount > 0) {
log(`➕ 新增 ${addedCount} 张图片 (共 ${fileList.length} 张)`);
updatePreview();
} else {
log('没有新图片添加 (可能已存在或格式不支持)', true);
}
}
// ---------- 文件输入事件 ----------
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files.length) {
addFiles(e.target.files);
}
fileInput.value = ''; // 允许重复选择相同文件 (但已去重)
});
// ---------- 拖拽支持 ----------
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.background = '#2f2f52';
dropZone.style.borderColor = '#b0b0f0';
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.background = '#22223b';
dropZone.style.borderColor = '#5b5b85';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.background = '#22223b';
dropZone.style.borderColor = '#5b5b85';
if (e.dataTransfer.files && e.dataTransfer.files.length) {
addFiles(e.dataTransfer.files);
}
});
// ---------- 清空 ----------
clearBtn.addEventListener('click', () => {
if (fileList.length === 0) return;
fileList = [];
updatePreview();
log('🗑️ 列表已清空');
});
// ---------- 核心: 生成 CUR (包含 PNG/RAW 数据) ----------
// 构建 ICO/CUR 结构 (只包含一个图像)
// 参考: https://en.wikipedia.org/wiki/ICO_(file_format)
function createCurFile(imageData, width, height, hotX, hotY) {
// imageData: 已缩放的 RGBA 字节 (Uint8ClampedArray)
const pixelCount = width * height;
const rawPixels = new Uint8Array(imageData); // RGBA 每个字节
// 使用 PNG 编码 (更通用, 所有浏览器都支持)
// 创建 canvas 获取 PNG buffer
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(width, height);
imgData.data.set(rawPixels);
ctx.putImageData(imgData, 0, 0);
// 转为 PNG blob (也可转为BMP, 但PNG压缩小)
const blob = canvas.toBlobSync('image/png');
// 同步方法? 但标准toBlob异步,使用较新的 canvas.toBlob 回调, 但这里需要同步。
// 改用 canvas.toBlob 回调 + 构造同步变通: 用 OffscreenCanvas 同步? 老兼容。
// 这里实现同步方法: 用 dataURI 转 blob (同步但稍慢)
const dataURI = canvas.toDataURL('image/png');
const blobSync = dataURItoBlob(dataURI);
// 读取 blob 为 ArrayBuffer (同步用FileReader同步? 不, 只能用异步,我们采用 async 方式)
// 为了代码简洁, 采用 promise 包装,调用处改用异步。
// 为了保持导出按钮点击逻辑, 构建一个方法返回 Promise<Blob>。
// 由于导出是批量, 我们将导出函数改为 async。
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
const pngBuffer = e.target.result; // ArrayBuffer
if (!pngBuffer) return reject('PNG buffer empty');
const pngBytes = new Uint8Array(pngBuffer);
const pngSize = pngBytes.length;
// ---- 构建 .cur 文件 ----
// ICO/CUR 头: 6 字节
const header = new Uint8Array(6);
// Reserved = 0
header[0] = 0; header[1] = 0;
// Type: 2 = CUR (光标)
header[2] = 2; header[3] = 0;
// Count: 1 (单图像)
header[4] = 1; header[5] = 0;
// 目录条目: 16 字节
const entry = new Uint8Array(16);
// 宽度/高度 (0 表示 256)
entry[0] = width === 256 ? 0 : width;
entry[1] = height === 256 ? 0 : height;
// 颜色数 (0 表示 >= 256)
entry[2] = 0;
// Reserved (对于光标必须为0)
entry[3] = 0;
// 热点 x, y (2字节 each, little-endian)
entry[4] = hotX & 0xFF;
entry[5] = (hotX >> 8) & 0xFF;
entry[6] = hotY & 0xFF;
entry[7] = (hotY >> 8) & 0xFF;
// 图像数据大小 (4字节)
const size = pngSize;
entry[8] = size & 0xFF;
entry[9] = (size >> 8) & 0xFF;
entry[10] = (size >> 16) & 0xFF;
entry[11] = (size >> 24) & 0xFF;
// 图像偏移: 6 + 16 = 22
const offset = 22;
entry[12] = offset & 0xFF;
entry[13] = (offset >> 8) & 0xFF;
entry[14] = (offset >> 16) & 0xFF;
entry[15] = (offset >> 24) & 0xFF;
// 合并: header + entry + png
const curBuffer = new Uint8Array(header.length + entry.length + pngSize);
curBuffer.set(header, 0);
curBuffer.set(entry, header.length);
curBuffer.set(pngBytes, header.length + entry.length);
resolve(new Blob([curBuffer], { type: 'image/x-cursor' }));
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(blobSync);
});
}
// 工具: dataURI to Blob
function dataURItoBlob(dataURI) {
const byteString = atob(dataURI.split(',')[1]);
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8 = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8[i] = byteString.charCodeAt(i);
}
return new Blob([arrayBuffer], { type: mimeString });
}
// 扩展 canvas.toBlob 同步 (某些环境)
if (!HTMLCanvasElement.prototype.toBlobSync) {
HTMLCanvasElement.prototype.toBlobSync = function(mime) {
const dataURI = this.toDataURL(mime);
return dataURItoBlob(dataURI);
};
}
// ---------- 批量导出 ----------
exportBtn.addEventListener('click', async () => {
if (fileList.length === 0) {
log('❌ 没有图片可导出', true);
return;
}
const width = parseInt(curWidthInp.value, 10) || 32;
const height = parseInt(curHeightInp.value, 10) || 32;
let hotX = parseInt(hotXInp.value, 10) || 0;
let hotY = parseInt(hotYInp.value, 10) || 0;
// 裁剪热点到图像内
hotX = Math.min(hotX, width - 1);
hotY = Math.min(hotY, height - 1);
if (hotX < 0) hotX = 0;
if (hotY < 0) hotY = 0;
// 使用临时 canvas 缩放图片
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
let successCount = 0;
let failCount = 0;
// 用于下载: 每生成一个触发下载 (批量)
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
try {
const img = await loadImage(file);
// 绘制到 canvas (缩放)
ctx.clearRect(0, 0, width, height);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, width, height);
// 提取 RGBA 数据
const imageData = ctx.getImageData(0, 0, width, height);
const rgba = imageData.data;
// 生成 cur blob
const curBlob = await createCurFile(rgba, width, height, hotX, hotY);
// 下载
const link = document.createElement('a');
link.href = URL.createObjectURL(curBlob);
// 更改后缀
let baseName = file.name.replace(/\.[^/.]+$/, '');
if (!baseName) baseName = `cursor_${i}`;
link.download = `${baseName}.cur`;
link.click();
URL.revokeObjectURL(link.href);
successCount++;
} catch (err) {
failCount++;
console.error(err);
log(`❌ 转换失败: ${file.name} - ${err.message}`, true);
}
}
log(`✅ 导出完成: 成功 ${successCount} 个, 失败 ${failCount} 个`);
if (failCount === 0 && successCount > 0) {
log(`🎉 成功导出 ${successCount} 个 .cur 文件 (热点 ${hotX},${hotY})`);
}
});
// 加载图片为 HTMLImageElement
function loadImage(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
resolve(img);
};
img.onerror = (e) => {
URL.revokeObjectURL(url);
reject(new Error(`图片加载失败: ${file.name}`));
};
img.src = url;
});
}
// 初始化预览
updatePreview();
log('📎 拖拽或选择图片 (PNG/BMP/JPG/GIF) 批量制作光标');
})();
</script>
</body>
</html>
工具功能与操作流程
您可以通过拖拽或点击上传图片,调整光标参数,并一键批量导出,其核心流程如下:
- 图片管理:支持批量上传 PNG、BMP、JPG 等格式图片,并在预览区清晰展示已添加的图片列表。
- 参数调整:您可以为生成的光标统一设定尺寸(宽度和高度)以及热点位置(即点击生效的坐标点)。
- 批量导出:点击“批量导出 CUR”按钮,工具会自动将所有图片转换为 Windows 光标格式,并为每张图片生成独立的
.cur文件供您下载。
标签: 图片批量光标工具CUR制作
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。