不装任何工具,用 PowerShell 给 logo 抠掉白底(含防白边处理)
做本站 header 的时候碰到一个典型小问题:品牌 logo 只有不透明白底的 PNG(24bpp,没有 alpha 通道),放到暗色模式的页面上就是一块刺眼的白色方块。
机器上没有 Photoshop,也不想为这一件事装 ImageMagick —— 其实 Windows 自带的 .NET System.Drawing 就够了。

左:原图直接放暗色页面的惨状;右:脚本处理后的效果
思路
- 把图片重绘到一张 32bpp(带 alpha)的画布上;
- 遍历像素:接近白色的设为全透明;
- 关键:边缘像素做渐变透明,否则抠出来一圈白色毛边(halo)。
判断「接近白色」用 RGB 三通道的最小值 m:
m ≥ 238→ 完全透明(背景);210 ≤ m < 238→ 按比例给部分透明度(抗锯齿边缘);- 其余 → 保持不动(logo 本体)。
代码
性能要点:不要用 GetPixel/SetPixel 逐像素调用(一张图几十万次方法调用,PowerShell 里能慢到分钟级),用 LockBits 把整块像素数据拷出来在字节数组上操作:
Add-Type -AssemblyName System.Drawing
$src = New-Object System.Drawing.Bitmap "logo.png"
$bmp = New-Object System.Drawing.Bitmap $src.Width, $src.Height, `
([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.DrawImage($src, 0, 0, $src.Width, $src.Height)
$g.Dispose(); $src.Dispose()
$rect = New-Object System.Drawing.Rectangle 0, 0, $bmp.Width, $bmp.Height
$data = $bmp.LockBits($rect, 'ReadWrite', $bmp.PixelFormat)
$bytes = New-Object byte[] ($data.Stride * $data.Height)
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $bytes.Length)
for ($i = 0; $i -lt $bytes.Length; $i += 4) {
# BGRA 顺序
$m = [Math]::Min([Math]::Min([int]$bytes[$i], [int]$bytes[$i+1]), [int]$bytes[$i+2])
if ($m -ge 238) { $bytes[$i+3] = 0 }
elseif ($m -ge 210) { $bytes[$i+3] = [byte][Math]::Round(255 * (238 - $m) / 28) }
}
[System.Runtime.InteropServices.Marshal]::Copy($bytes, 0, $data.Scan0, $bytes.Length)
$bmp.UnlockBits($data)
$bmp.Save("logo-transparent.png", [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
两个踩过的点
- 像素是 BGRA 不是 RGBA:
LockBits出来的字节顺序是 Blue、Green、Red、Alpha,通道搞反了阈值判断就全错; - 背景不是纯白:真实图片的”白底”往往是
#fefefc、#f8f9fb这种近白渐变(特别是 AI 生成或扫描的图),阈值要留余量,别写死255。
暗色模式的后续处理
抠完透明底还有一步:logo 里深色的文字部分在暗色背景上对比度不够。我的做法是把图标和文字分开裁剪,暗色模式下只对文字部分套一层 CSS filter 转成近白色,图标保持品牌原色:
.dark .wordmark img.word {
filter: brightness(0) invert(0.92);
}
一套下来零安装、全程命令行,本站 header 上的效果就是这个脚本的产物。