这是为您生成的“图片批量光标工具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制作
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播