返回博客

macOS Dock 图标大白底:Squircle 和留白必须烘焙进素材

给桌面应用打包完,一打开 Dock 发现图标长这样:蓝色 logo 端端正正地坐在一个白色正方形上,四个角还带着明显的白色三角。更难看的是,整个图形顶边到底边无缝铺满,完全没有 macOS 图标该有的透气感——一眼看出是从 iOS 移过来的。

找了半天,源 PNG 的四个角明明是透明的,为什么生成出来的 icon.icns 却是白的?

现象

  • Dock / Finder / Cmd-Tab 里,图标四角出现白色不透明区域
  • 图标形状是 iOS 风格的全出血圆角矩形,填满整个图标格,没有边距
  • 用 Pillow 验证角像素:
from PIL import Image
Image.open("icon.png").convert("RGBA").getpixel((0, 0))
# 输出 (255, 255, 255, 255)  ← 白色不透明,bug 就在这里

根因

有两个互相叠加的根因:

第一:macOS 不会自动遮罩 Dock 图标。

iOS 在显示时会把图标裁剪成圆角矩形;macOS 完全不做这件事,.icns 里是什么形状,Dock 就渲染什么形状。Squircle(超椭圆)、透明留白、软阴影——这三样全都必须烘焙进图标素材本身

第二:图标生成工具悄悄把透明通道合并到了白底上。

不管是 ImageMagick 的 -flatten / -background white,还是 sips,还是某些旧版图标生成脚本,都会在写出图标时把带透明度的源文件与白色背景做一次 composite,结果:源文件角是透明的,生成物角是白色不透明的。这个过程不报任何警告。

坑:macOS 图标的圆角不是运行时剪裁的

iOS 会在展示时把方形图标裁成圆角,所以你可以交一张全出血的方图。macOS 完全不做这步——.icns 里是方的,Dock 里就是方的。留白、Squircle、阴影,一个都不能省,得写进文件。

正解

为 macOS 单独制作一张 1024×1024 的主图,规格:

  • 内容区 824×824 居中(四边各留 100px 透明边距)
  • 圆角形状:超椭圆 / Squircle,指数 n≈5(Apple 原生规格)
  • 底部叠一层 18% 不透明度的软阴影,控制在下边距内
  • 四角保持完全透明(alpha=0)

用 Pillow + iconutil 生成,绝对不用 -flatten 或任何会合并透明通道的工具:

from PIL import Image, ImageFilter
 
BLUE = (59, 91, 219, 255)
CANVAS = 1024
SS = 4          # 4× 超采样,LANCZOS 缩小后边缘抗锯齿
C = CANVAS * SS
CONTENT = int(824 * SS)
off = (C - CONTENT) // 2
 
def squircle(size, n=5.0):
    m = Image.new("L", (size, size), 0)
    px = m.load()
    h = size / 2
    for y in range(size):
        v = abs((y + 0.5 - h) / h) ** n
        if v > 1:
            continue
        for x in range(size):
            if abs((x + 0.5 - h) / h) ** n + v <= 1:
                px[x, y] = 255
    return m
 
mask = squircle(CONTENT)
 
# 超椭圆底色
shape = Image.new("RGBA", (C, C), (0, 0, 0, 0))
shape.paste(Image.new("RGBA", (CONTENT, CONTENT), BLUE), (off, off), mask)
 
# logo 叠上去
src = Image.open("source-1024.png").convert("RGBA").resize((CONTENT, CONTENT), Image.LANCZOS)
g = Image.new("RGBA", (C, C), (0, 0, 0, 0))
g.paste(src, (off, off), src)
shape = Image.alpha_composite(shape, g)
 
# 裁回超椭圆轮廓
clip = Image.new("L", (C, C), 0)
clip.paste(mask, (off, off))
shape.putalpha(Image.composite(shape.getchannel("A"), Image.new("L", (C, C), 0), clip))
 
# 软阴影(向下偏 8px,18% 不透明度)
sa = Image.new("L", (C, C), 0)
sa.paste(mask, (off, off + 8 * SS))
sa = sa.filter(ImageFilter.GaussianBlur(16 * SS)).point(lambda a: int(a * 0.18))
sh = Image.new("RGBA", (C, C), (0, 0, 0, 0))
sh.putalpha(sa)
 
# 合成并缩回 1024
result = Image.alpha_composite(sh, shape)
result.resize((CANVAS, CANVAS), Image.LANCZOS).save("source-macos-1024.png")

然后用这张 macOS 专属主图降采样为各尺寸,再交给 iconutil 打包:

iconutil -c icns macos.iconset -o icon.icns

验证每个输出文件的角

Image.open("icon.png").convert("RGBA").getpixel((0, 0))
# 必须是 (0, 0, 0, 0)
正解:macOS 专属主图 + iconutil,不用 flatten 工具

源文件分平台:source-1024.png(全出血,给 iOS/Android/Windows 用)和 source-macos-1024.png(Squircle + 100px 留白 + 阴影,只给 macOS 用)。macOS 的 .icns 和各尺寸 PNG 全部从后者降采样,保留 alpha 通道。

排查补充

  • 改完图标文件后必须重新构建应用(Tauri tauri build / Electron electron-builder),.icns 是打包进 .app/Contents/Resources/ 的,直接替换源文件对 Dock 没有任何效果。
  • 重新打包后 Dock 依然显示旧图标,大概率是 macOS 图标缓存:
sudo rm -rf /Library/Caches/com.apple.iconservices.store && killall Dock

一句话外卖

macOS 图标的 Squircle、留白、阴影不是运行时效果,而是你交给系统的文件本身就得长这样;任何会合并 alpha 通道的生成步骤都会把透明角变成白角。