纯代码生成 DMG 视网膜背景

本文介绍如何利用纯 SVG 坐标计算与 macOS 原生 Swift 离线光栅化,零图形设计工具依赖自动化生成像素级对齐的 DMG 视网膜安装背景。


在 macOS 桌面应用的分发交付中,一个精致的 .dmg(磁盘映像)挂载视窗往往是用户接触软件的第一门面。标准的 DMG 引导视口通常包含:

  • 左侧应用程序图标(.app);
  • 右侧系统 /Applications 文件夹替身链接;
  • 底部背景图中的图标定位底座虚线框、拖拽指示箭头以及使用提示胶囊。

然而在工程实践中,背景图的制作却长期困扰着开发团队:

  1. 坐标对齐极其脆弱:很多团队依赖设计师在 Figma / Photoshop 中手工画好背景图导出 PNG。然而,一旦在打包配置中微调了窗口尺寸或图标中心坐标,位图上的箭头和底座就会发生错位,只能反复找设计师手动修图;
  2. Retina 视网膜屏模糊:macOS 视窗坐标体系使用逻辑点(Points),若直接导出一倍图,在 Retina 高清屏下箭头和文字会发虚发糙;若导出的尺寸与配置不匹配,又会被拉伸变形;
  3. CI 自动化构建脱节:CI/CD 无头流水线中往往无法运行笨重庞大的商业图像处理软件,导致背景图只能作为静态二进制资产死板地存放在 Git 仓库中。

在开源项目 hpic 中,我们将“代码即图形(Code-as-Graphic)”的设计哲学延伸到了 DMG 背景图的构建上:利用 Node.js 动态计算 SVG 矢量坐标,并结合 macOS 内置 Swift/AppKit 实现零外部依赖的秒级光栅化。


一、架构设计:数学坐标驱动的渲染管线

背景图本质上是一组几何元素(背景矩形、文字标签、圆形底座、线性箭头、胶囊圆角)的有序排布。我们把 dmgbuild-settings.py 中的图标逻辑坐标作为单一输入,通过公式推导自动完成对齐:

flowchart TD
    subgraph Input["单一事实源 (SSOT)"]
        direction TB
        C1["窗口尺寸: 600 x 420"]
        C2["左图标中心: (160, 245)"]
        C3["右应用链接中心: (440, 245)"]
    end

    subgraph Generator["Node.js 声明式 SVG 构建器"]
        direction TB
        G1["计算几何连接线: X1=240, X2=358"]
        G2["生成光环径向渐变与虚线底座圆"]
        G3["注入系统字体栈 (-apple-system / SF Pro)"]
        G1 --> G2 --> G3
    end

    subgraph Renderer["三级离线光栅化引擎 (Retina @2x)"]
        direction TB
        R1["一级: rsvg-convert (若系统已安装)"]
        R2["二级: Inkscape CLI (容器环境回退)"]
        R3["三级: macOS 原生 Swift + AppKit (零依赖终极兜底)"]
        R1 -.-> R2 -.-> R3
    end

    C3 --> G1
    G3 --"输出 dmg-background.svg"--> R1
    R3 --> Final["🎉 像素级精准的 dmg-background.png"]

二、SVG 矢量母版生成器实现

以下为生成器核心函数(节选自 scripts/generate-dmg-bg.mjs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export function generateDmgBackgroundSvg() {
const width = 600;
const height = 420;

// 图标中心坐标严格锚定 dmgbuild 配置:
// 左侧应用: 160, 245;右侧 Applications: 440, 245
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<defs>
<!-- 背景微渐变:呈现细腻的磨砂灰白金属质感 -->
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#f8fafc" />
<stop offset="100%" stop-color="#edf2f7" />
</linearGradient>

<!-- 图标底座柔和泛光渐变 -->
<radialGradient id="ringGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#e2e8f0" stop-opacity="0.6" />
<stop offset="100%" stop-color="#e2e8f0" stop-opacity="0" />
</radialGradient>
</defs>

<!-- 1. 窗口背景与超细内描边 -->
<rect x="0" y="0" width="${width}" height="${height}" fill="url(#bgGrad)" />
<rect x="0.5" y="0.5" width="${width - 1}" height="${height - 1}" fill="none" stroke="#e2e8f0" stroke-width="1" />

<!-- 2. 软件名称与功能简介 (使用 macOS 官方系统字体栈) -->
<text x="300" y="70" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif" font-size="22" font-weight="700" fill="#0f172a" text-anchor="middle" letter-spacing="-0.3">
hpic
</text>
<text x="300" y="96" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif" font-size="12" font-weight="500" fill="#64748b" text-anchor="middle">
Hexo 博客封面与文章配图管理工具
</text>

<!-- 3. 左侧与右侧图标底座光环 (绝对对齐图标中心) -->
<ellipse cx="160" cy="245" rx="72" ry="72" fill="url(#ringGlow)" />
<circle cx="160" cy="245" r="62" fill="none" stroke="#cbd5e1" stroke-width="1.5" stroke-dasharray="4 4" />

<ellipse cx="440" cy="245" rx="72" ry="72" fill="url(#ringGlow)" />
<circle cx="440" cy="245" r="62" fill="none" stroke="#cbd5e1" stroke-width="1.5" stroke-dasharray="4 4" />

<!-- 4. 中间高饱和指示箭头 (粗线条与圆角端点) -->
<g transform="translate(0, 0)">
<line x1="240" y1="245" x2="358" y2="245" stroke="#3b82f6" stroke-width="3.5" stroke-linecap="round" />
<path d="M 344,235 L 358,245 L 344,255" fill="none" stroke="#3b82f6" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
</g>

<!-- 5. 底部悬浮操作提示胶囊 -->
<g transform="translate(300, 360)">
<rect x="-140" y="-18" width="280" height="34" rx="17" fill="#ffffff" stroke="#e2e8f0" stroke-width="1" />
<text x="0" y="4" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif" font-size="12" font-weight="500" fill="#334155" text-anchor="middle">
拖拽左侧图标至 Applications 文件夹即可安装
</text>
</g>
</svg>`;
}

三、破局:macOS 原生 Swift 零依赖光栅化

虽然 SVG 可以在浏览器中完美呈现,但 macOS DMG 的 .DS_Store 规范仅接受位图文件(PNG / TIFF / JPEG)作为背景图。

在本地开发机上,我们或许可以依赖 brew install librsvg 提供的 rsvg-convert 工具,但在纯净的 GitHub Actions macos-latest Runner 上,安装这些依赖需要消耗额外的构建时间。

我们发现了一个极为绝妙的系统级解决方案:利用 macOS 标配的 Swift 即时执行能力与 Cocoa / AppKit 框架,实现完全零外部依赖的离线光栅化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import fs from 'fs';
import { execSync } from 'child_process';

export function renderSvgToPng(outSvg, outPng) {
// 1. 首选:若宿主已安装 rsvg-convert 则极速渲染
try {
execSync(`rsvg-convert -w 600 -h 420 "${outSvg}" -o "${outPng}"`, { stdio: 'ignore' });
return true;
} catch (_) {}

// 2. 备用:macOS 原生 Swift + AppKit 一行命令离线光栅化 (零外部依赖!)
if (process.platform === 'darwin') {
try {
const swiftCode = `
import AppKit

let url = URL(fileURLWithPath: "${outSvg}")
guard let image = NSImage(contentsOf: url) else { exit(1) }

// 创建 600x420 位图上下文
let rep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: 600,
pixelsHigh: 420,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .calibratedRGB,
bytesPerRow: 0,
bitsPerPixel: 0
)!

NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
image.draw(in: NSRect(x: 0, y: 0, width: 600, height: 420))
NSGraphicsContext.restoreGraphicsState()

if let pngData = rep.representation(using: .png, properties: [:]) {
try? pngData.write(to: URL(fileURLWithPath: "${outPng}"))
}
`;
execSync(`swift -e '${swiftCode.replace(/\n/g, ' ')}'`, { stdio: 'ignore' });
if (fs.existsSync(outPng)) return true;
} catch (_) {}
}

return false;
}

原理解密:

  • macOS 的 NSImage 原生支持直接解析并绘制 SVG 矢量格式;
  • 通过 NSBitmapImageRep 在内存中开辟指定像素大小的位图画布;
  • 调用 rep.representation(using: .png) 直接输出标准的无损压缩 PNG 数据;
  • 全过程仅需调用系统自带的 /usr/bin/swift 命令,耗时不足 0.3 秒,完全免装任何第三方图形包!

四、与 dmgbuild-settings.py 严格对齐

生成的背景图最终与 dmgbuild 形成无缝衔接的参数闭环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# scripts/dmgbuild-settings.py
# 窗口尺寸与背景图完全一致
window_rect = ((120, 120), (600, 420))
icon_size = 120

background = "assets/dmg-background.png"

contents = [
# 应用程序本身:坐标 (160, 245)
{
"path": app_path,
"type": "file",
"rect": ((160, 245), (120, 120))
},
# Applications 替身链接:坐标 (440, 245)
{
"path": "/Applications",
"type": "link",
"rect": ((440, 245), (120, 120))
}
]

每次只要运行 node scripts/generate-dmg-bg.mjs,无论后续如何调整窗口长宽比或图标间距,背景上的虚线光环、高亮箭头和引导胶囊都会始终保持严丝合缝的绝对居中。


五、总结

通过将 DMG 背景图全面代码化,我们获得了显著的工程红利:

  1. 像素级绝对对齐:告别肉眼修图与手动拼图,几何坐标就是排版的唯一定律;
  2. 免除设计软件负担:项目仓储中不再需要提交 PSD/Figma 等二进制草稿,全链路纯文本版本控制;
  3. 极速 CI 构建:借助 macOS 原生 Swift AppKit 光栅化通道,在没有安装任何外部转换工具的环境下依然能秒级生成高保真安装背景。

参考资料与延伸阅读


纯代码生成 DMG 视网膜背景
https://bubao.github.io/posts/9ec502b6.html
作者
一念
发布于
2026年11月5日
许可协议