图片批量光标工具CUR制作

联启 电脑工具 18

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

图片批量光标工具CUR制作-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

<!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>

工具功能与操作流程

您可以通过拖拽或点击上传图片,调整光标参数,并一键批量导出,其核心流程如下:

  1. 图片管理:支持批量上传 PNG、BMP、JPG 等格式图片,并在预览区清晰展示已添加的图片列表。
  2. 参数调整:您可以为生成的光标统一设定尺寸(宽度和高度)以及热点位置(即点击生效的坐标点)。
  3. 批量导出:点击“批量导出 CUR”按钮,工具会自动将所有图片转换为 Windows 光标格式,并为每张图片生成独立的 .cur 文件供您下载。

标签: 图片批量光标工具CUR制作

抱歉,评论功能暂时关闭!