Remove a logo's white background with pure PowerShell (halo-free)
Building this site’s header surfaced a classic small problem: the only brand logo available was an opaque white-background PNG (24bpp, no alpha channel). Drop it on a dark-mode page and you get a glaring white rectangle.
No Photoshop on this machine, and installing ImageMagick for one job felt silly — Windows’ built-in .NET System.Drawing is enough.

Left: the original dropped straight onto a dark page. Right: the script’s output.
The approach
- Redraw the image onto a 32bpp (alpha-capable) canvas;
- Walk the pixels: near-white becomes fully transparent;
- The crucial part: edge pixels get graduated transparency, otherwise you cut out a white halo around everything.
“Near-white” is judged by the minimum of the RGB channels, m:
m ≥ 238→ fully transparent (background);210 ≤ m < 238→ proportional partial alpha (anti-aliased edges);- everything else → untouched (the logo itself).
The code
Performance matters: don’t loop with GetPixel/SetPixel (hundreds of thousands of method calls — minutes in PowerShell). Use LockBits and work on the raw byte array:
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 order
$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()
Two things that bit me
- Pixels are BGRA, not RGBA:
LockBitshands you Blue, Green, Red, Alpha. Mix up the channels and every threshold decision is wrong; - The background isn’t pure white: real “white” backgrounds are gradients like
#fefefcand#f8f9fb(especially AI-generated or scanned images). Leave headroom in the threshold — don’t hardcode255.
The dark-mode follow-up
Transparency isn’t the whole story: dark text in the logo has poor contrast on a dark background. My solution was to crop the icon and the wordmark separately, and in dark mode run a CSS filter only on the text portion, keeping the icon in brand colors:
.dark .wordmark img.word {
filter: brightness(0) invert(0.92);
}
Zero installs, all command line — the header of this very site is this script’s output.