文档管理中心
我的

如何批量生成元服务的睫毛图

问题现象

在元服务的开发与上架过程中,必须提供符合规范的图标。DevEco Studio支持Image Asset功能,帮助开发者生成统一的元服务图标样式,具体步骤请参考生成元服务图标。虽然该功能可以便捷地生成元服务图标,但当需要处理的图标数量较多时,在IDE中逐一操作效率过低。因此,需要寻求一种能够批量、快速生成整套元服务图标的解决方案。

背景知识

睫毛图生成工具:为了满足服务商使用场景,ASCF提供了SDK版本:@atomicservice/as-icon-generator,帮助服务商集成睫毛图生成能力。

解决方案

鉴于SDK对浏览器环境的强依赖,以及文件系统写入权限的限制,本文采用“本地轻量化Web容器+JSZip内存打包”的方案。

方案架构如下:

  1. 运行环境:使用Vite快速搭建一个一次性的本地开发服务器,提供SDK运行所需的浏览器环境。
  2. 批量机制:利用HTML5的<input type="file" multiple>实现多图导入。
  3. 全量生成:编写双重循环逻辑,遍历每一张输入图片,并针对每一张图片提取出的所有推荐色自动执行生成任务。
  4. 文件导出:引入JSZip库,在浏览器内存中创建虚拟文件夹,将生成的Base64图片流转为二进制文件,最后打包为.zip触发浏览器下载。

构建流程如下:

  1. 环境搭建:在任意临时目录通过命令行初始化一个纯JS项目:npm create vite@latest icon-batch-tool -- --template vanilla,接着cd icon-batch-tool进入项目icon-batch-tool,输入npm install安装npm。
  2. 安装核心依赖:安装元服务图标SDK及ZIP打包工具:npm install @atomicservice/as-icon-generator jszip。
  3. 核心逻辑实现:修改icon-batch-tool项目根目录下的src/main.js,注入批量处理逻辑,完整的代码如下:
    import { AsIconGenerator } from '@atomicservice/as-icon-generator';
    import JSZip from 'jszip';
    
    // 动态构建界面
    document.querySelector('#app').innerHTML = `
      <div style="padding: 20px; font-family: sans-serif; max-width: 800px; margin: 0 auto;">
        <h2>批量睫毛图生成器 (All Colors版)</h2>
    
        <div style="background: #eef; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
          <h3>使用说明:</h3>
          <ol>
            <li>点击下方按钮,可以<strong>框选多张图片</strong>。</li>
            <li>程序会自动分析每张图,并为<strong>每一个识别出的颜色</strong>生成一张图。</li>
            <li>全部完成后,会自动下载一个 <strong>images.zip</strong> 压缩包。</li>
            <li>解压该压缩包到你的项目文件夹即可。</li>
          </ol>
        </div>
    
        <div style="margin-bottom: 20px;">
          <input type="file" id="fileInput" accept="image/*" multiple 
                 style="padding: 10px; border: 2px dashed #ccc; width: 100%; box-sizing: border-box;">
        </div>
    
    
        <div id="logArea" style="
          background: #222; color: #0f0; 
          padding: 15px; height: 300px; overflow-y: auto; 
          font-family: monospace; font-size: 12px; border-radius: 4px;
          display: none;
        "></div>
      </div>
    `;
    
    const fileInput = document.getElementById('fileInput');
    const logArea = document.getElementById('logArea');
    
    // 简单的日志工具
    function log(msg, type = 'info') {
      logArea.style.display = 'block';
      const time = new Date().toLocaleTimeString();
      const color = type === 'error' ? '#ff4444' : type === 'success' ? '#00ff00' : '#ddd';
      logArea.innerHTML += `<div style="color:${color}; margin-bottom: 4px;">[${time}] ${msg}</div>`;
      logArea.scrollTop = logArea.scrollHeight; // 自动滚动到底部
    }
    
    fileInput.addEventListener('change', async (e) => {
      const files = Array.from(e.target.files);
      if (files.length === 0) return;
    
      log(` 开始处理,共选择了 ${files.length} 张图片...`);
    
      // 创建 ZIP 对象
      const zip = new JSZip();
      // 创建一个文件夹存放结果
      const folder = zip.folder("generated_icons");
    
      let successCount = 0;
    
      // --- 第一层循环:遍历所有图片 ---
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        const fileName = file.name.substring(0, file.name.lastIndexOf('.')); // 去掉后缀
    
        log(`\n----------------------------------------`);
        log(` [${i + 1}/${files.length}] 正在处理图片: ${file.name}`);
    
        try {
          // 1. 加载图片
          const objectUrl = URL.createObjectURL(file);
    
          // 2. 初始化 SDK
          const generator = new AsIconGenerator({ srcImg: objectUrl });
    
          // 3. 提取颜色 (设置超时防止卡死)
          const colors = await Promise.race([
            generator.getColors(),
            new Promise((_, r) => setTimeout(() => r(new Error('提取颜色超时')), 5000))
          ]);
    
          if (!colors || colors.length === 0) {
            log(`⚠️ 图片 ${file.name} 未提取到有效颜色,跳过。`, 'error');
            continue;
          }
    
          log(` 识别到 ${colors.length} 种颜色: ${JSON.stringify(colors)}`);
    
          // --- 第二层循环:遍历该图片的所有颜色 ---
          for (const color of colors) {
            try {
              // 设置颜色
              generator.setColor(color);
    
              // 生成图片
              const result = await generator.generate({ agc: true });
    
              if (result && result.outputImage) {
                // 处理 Base64 数据
                const base64Data = result.outputImage.split(',')[1];
    
                // 构造文件名:原名_颜色.png (例如: icon_#ff0000.png)
                // 注意:#在文件名中可能有些系统不喜欢,替换为 _
                const safeColorName = color.replace('#', '');
                const outputName = `${fileName}_${safeColorName}.png`;
    
                // 添加到压缩包
                folder.file(outputName, base64Data, { base64: true });
    
                log(`   ✅ 生成成功: ${outputName}`);
                successCount++;
              }
            } catch (innerErr) {
              log(`   ❌ 颜色 ${color} 生成失败: ${innerErr.message}`, 'error');
            }
          }
    
        } catch (err) {
          log(`❌ 图片 ${file.name} 处理出错: ${err.message}`, 'error');
        }
      }
    
      // --- 结束:生成并下载 ZIP ---
      if (successCount > 0) {
        log(`\n 全部处理完成!共生成 ${successCount} 张图片。`);
        log(` 正在打包压缩,请稍候...`);
    
        try {
          const content = await zip.generateAsync({ type: "blob" });
    
          // 触发下载
          const link = document.createElement('a');
          link.href = URL.createObjectURL(content);
          link.download = "batch_eyelash_icons.zip";
          document.body.appendChild(link);
          link.click();
          document.body.removeChild(link);
    
          log(`⬇️ 已触发下载,请查看浏览器下载栏。`, 'success');
        } catch (zipErr) {
          log(`❌ 压缩包生成失败: ${zipErr.message}`, 'error');
        }
      } else {
        log(`⚠️ 没有生成任何有效图片。`, 'error');
      }
    });
  4. 执行与输出:
    1. 运行npm run dev启动服务,在浏览器打开localhost:5173(具体端口请参考执行命令后的Local地址):

    2. 4.2 点击选择文件框选所有待处理的图片文件:

    3. 脚本自动执行分析与生成,最终自动下载batch_eyelash_icons.zip: