battery 项目图标编译器实战:将电池百分比 SVG 批量渲染为 Electron 托盘 PNG

原创2026-10-01 14:35:021,091 阅读
文章标签:桌面应用CLI

battery 项目图标编译器实战:将电池百分比 SVG 批量渲染为 Electron 托盘 PNG

本指南以仓库 assets 目录下的“Logo compiler”工具为主体,讲解其存在原因(Electron 托盘不支持直接使用 SVG 图片)、一行命令的运行方式(npm start)、以及配置入口 assets/modules/compile-images.js 中百分比步长、DPI 倍率、active/inactive 透明度等核心参数。读完本文,你将掌握该图标编译器从 SVG 模板生成、批量栅格化 PNG 到被 app/modules/theme.js 消费的完整链路,并能在修改“电量百分比显示粒度”这类需求时正确地重新生成全部图标。

背景:为什么 Electron 托盘图标需要一个“图标编译器”

原文档开宗明义:“Because Electron does not support SCG images”(原文此处为笔误,实指 SVG)。Electron 的 nativeImage API 并不支持直接加载 SVG,而本项目(一款用于 Apple silicon Mac 电池充电限制的托盘应用)需要在菜单栏托盘上按电量百分比动态显示不同的电池图标,因此必须先把矢量图源批量渲染成 PNG。

为了让托盘图标能覆盖“从 0% 到 100%”的每一个展示档位,同时兼顾 macOS 菜单栏在不同屏幕 DPI 下的清晰度,仓库在 assets 目录里维护了一个独立的小工具项目(asset-compiler):

  • 它读取“百分比”作为输入,调用 SVG 模板函数生成图标;
  • 通过 @resvg/resvg-js(Rust 实现的 SVG 渲染库)将 SVG 栅格化为 PNG;
  • 一次性输出数百张命名规范的图标文件,供 Electron 应用直接引用。

这套流程在 app/README.md 的开发者说明中也被印证:图标由脚本自动生成,应用侧无需再处理任何 SVG。

快速上手:安装依赖与运行

工具本身的工程信息定义在 assets/package.json 中,运行命令极其简单:

cd assets
npm install        # 安装 @resvg/resvg-js 等依赖
npm start          # 等价于 node index.js

npm start 对应 assets/package.json 中的 "start": "node index.js",入口文件 assets/index.js 只有寥寥几行——引入 assets/modules/compile-images.js 并调用其导出的 render_and_write_template_files():

const render_and_write_template_files = require( './modules/compile-images' )
render_and_write_template_files()

整个渲染过程是同步等待完成的,命令执行完毕后,所有 PNG 会写入 app/assets 目录(源码中 export_directory = ${ __dirname }/../../app/assets)。原文档特别强调:这个工具只在“图标百分比支持范围发生变化”时才需要重新运行,日常开发无需反复执行。

核心配置解析:百分比、倍率与命名规范

配置的真正“大脑”是 assets/modules/compile-images.js,它集中了全部渲染参数:

配置项 源码位置 取值 说明
百分比步长 compile-images.js 5 从 0 到 100 每 5% 生成一档,共 21 个百分比
DPI 倍率集合 compile-images.js ['1.25','1.33','1.4','1.5','1.8','2','2.5','3','4','5'] 对应 Electron nativeImage 支持的高分辨率表示(High-resolution image),注释中说明了依据
图标状态 compile-images.js active / inactive 每种百分比都同时生成激活态与未激活态
透明度 compile-images.js 与 L59 active 用 bake_logo(percentage, 1),inactive 用 bake_logo(percentage, .5) 与 app/README.md 中“100% vs 50% for active inactive”的说明一致
输出目录 compile-images.js ../../app/assets 相对 assets/modules,即仓库根下的 app/assets

渲染组合逻辑为:每个百分比生成 22 张图 = 1 张基础图(1x,无倍率后缀)+ 10 张 @Nx 倍率图,且 active/inactive 各一套。21 个百分比 × 22 张 = 共 462 张 PNG,全部通过 Promise.all 并发栅格化写入磁盘(见 compile-images.js)。

文件名遵循统一模板(见 compile-images.js):

battery-{active|inactive}-{percentage}-Template{@Nx}.png

例如:

  • battery-active-80-Template.png(1x 基础图,18×18)
  • battery-active-80-Template@2x.png(2 倍图,36×36)
  • battery-inactive-50-Template@1.5x.png

文件名的 Template 后缀并非装饰:macOS 会把名称带 Template 的图片当作模板图像处理,自动适配菜单栏的深色/浅色主题,这正是本项目图标能随系统外观切换的原因之一。

栅格化核心函数 render_and_write_png 使用 Resvg 的 fitTo.zoom 模式按倍率放大输出:

const resvg = new Resvg( svg_string, { fitTo: { mode: 'zoom', value: Number( size ) } } )
const png_data = resvg.render()
const png_buffer = png_data.asPng()
return fs.writeFile( `${ export_directory }/${ filename }.png`, png_buffer )

因此基础图 18×18 的 @2x 版本即为 36×36、@5x 即为 90×90,这与 app/assets 中实际存在的各倍率图片尺寸完全吻合。

SVG 模板的生成原理

SVG 模板由 assets/modules/logo.js 的 bake_logo(percentage, opacity) 函数生成,它是整个图标系统的“画布”。核心几何逻辑如下:

const max_height = 12
const min_y_position = 3.5

const percentage_as_decimal = percentage / 100
const current_height = max_height * percentage_as_decimal
const current_y_position = min_y_position + ( max_height - current_height )
  • 画布为 viewBox="0 0 18 18",即 18×18 的逻辑坐标系;
  • 电池内部的电量填充矩形(<rect id="content">)宽度固定为 7.25、圆角半径 1.5;
  • 填充高度 current_height 随百分比线性变化:0% 时高度为 0(只有空电池轮廓),100% 时高度为 12(充满);
  • 填充矩形纵坐标 current_y_position 随高度反向移动:电量越低越靠下,电量越高越靠上,从而让电量条始终“从底部开始生长”。

<svg> 根节点上施加的 opacity 参数则直接控制了 active(1.0)与 inactive(0.5)两种状态的视觉差异,这也是 app/README.md 所述“图标内部电量填充部分在激活/未激活时透明度不同”的实现来源。电池外壳则由一段预置的 path 路径绘制,配合两重 matrix 变换缩放到 18×18 画布内。

生成的图标如何被应用消费

编译产物最终被 Electron 应用侧读取。关键代码在 app/modules/theme.js 的 get_logo_template(percent, active):

const percentage_increment_to_render = 5
const display_percentage = Math.floor( percent / percentage_increment_to_render ) * percentage_increment_to_render
const image_path = path.join( asset_path, `/battery-${ active ? 'active' : 'inactive' }-${ display_percentage }-Template.png` )
return nativeImage.createFromPath( image_path )

这里有两个值得注意的联动点:

  1. 百分比取整对齐:display_percentage 会把任意实时电量(如 83%)向下取整到 5 的倍数(80%)。由于编译器恰好以 5 为步长生成图标,取整后总能命中已存在的文件;反过来,若想提高展示精度(例如改为 1% 一档),就必须同步修改 compile-images.js 中的 percentage_increment_to_render 并重新运行编译器。
  2. 资源路径切换:asset_path = app.isPackaged ? resourcesPath : './assets'——开发态直接读 app/assets,打包后则读取资源目录。

消费端 app/modules/interface.js 在生成托盘菜单时调用 tray.setImage( get_logo_template( percentage, limiter_on ) )(见 interface.js),并在启用/停用充电限制时分别用 active / inactive 强制刷新图标(见 interface.js 的 enable_limiter / disable_limiter)。整套流程形成了“实时电量 → 取整到 5 的倍数 → 查表选 PNG → 设置托盘图像”的完整数据链路。

打包与资源路径

打包配置位于 app/package.json 的 build.extraFiles:

"extraFiles": {
  "from": "./assets",
  "to": "resources",
  "filter": [ "**/*" ]
}

编译生成的 app/assets 下全部 PNG 会被原样打入应用包内的 resources 目录,运行时通过 resourcesPath 读取——这解释了为什么这 462 张图片必须作为静态资源随包发布,而不是在运行时动态生成。

何时需要重新运行编译器

原文档给出的原则非常明确:“This only needs to be done if icon percentage support changes.” 结合源码可以归纳出需要重新执行 npm start 的典型场景:

  • 修改了百分比步长(如把 5% 一档改为 1% 一档),需要同时改 compile-images.js 与 theme.js 中的 percentage_increment_to_render;
  • 调整了图标倍率集合(增删 DPI 档位,见 compile-images.js);
  • 修改了 logo.js 中的几何参数(填充条尺寸、坐标、透明度策略等),需要重新栅格化全部档位;
  • 调整了图标命名规则(前缀、后缀),此时还需同步更新 theme.js 中的文件名拼接逻辑。

日常使用、只改应用业务逻辑(如菜单文案、刷新频率)时,均无需触碰编译流程。

注意事项小结

  • 原文档中配置文件名写作 comile-images.js,为笔误,实际文件为 assets/modules/compile-images.js。
  • app/README.md 提到图标可通过在 app/assets 下运行 node index.js 生成;而编译器工程实际位于仓库根的 assets 目录,产物输出到 app/assets,运行入口以 npm start 为准。
  • 渲染依赖仅一个运行时包 @resvg/resvg-js(^2.4.1),整个工具非常轻量;npm test 目前仅输出占位提示(见 assets/package.json),功能验证可直接检查输出目录中的 PNG 数量与尺寸。
登录后查看全文
battery