DEV Community

코딩나우(하늘아래)
코딩나우(하늘아래)

Posted on Originally published at coding-now.com

Converting WebP to JPG on Windows with no dependencies (and the alpha channel gotcha)

Someone sends you a folder of .webp files and the upload form only takes JPG. You don't need ImageMagick for this: Windows has shipped a WebP decoder since 1809 (as the Store-delivered Microsoft.WebpImageExtension), and WIC exposes it to PowerShell. But the built-in path has two sharp edges I only found by testing them.

First: renaming is not converting

$ head -c 16 photo.webp | xxd
00000000: 5249 4646 4e0a 0000 5745 4250 5650 3820  RIFF....WEBPVP8
Enter fullscreen mode Exit fullscreen mode

Rename it to photo.jpg and those bytes don't change. Most viewers sniff the content and open it anyway, which is exactly why people think the rename worked - until something that actually validates the format rejects it. Python agrees about what it really is:

>>> Image.open("renamed.jpg").format
'WEBP'
Enter fullscreen mode Exit fullscreen mode

The no-install route: WIC via PowerShell

Add-Type -AssemblyName PresentationCore

$src   = "C:\Pictures\image.webp"
$frame = [System.Windows.Media.Imaging.BitmapDecoder]::Create([Uri]$src, 'None', 'OnLoad').Frames[0]
$enc   = New-Object System.Windows.Media.Imaging.JpegBitmapEncoder
$enc.QualityLevel = 90
$enc.Frames.Add($frame)
$out = [System.IO.File]::Create([IO.Path]::ChangeExtension($src, '.jpg'))
$enc.Save($out); $out.Close()
Enter fullscreen mode Exit fullscreen mode

BitmapDecoder.Create picks the codec by content, and on a machine with the WebP extension you can confirm which one it chose:

$dec = [System.Windows.Media.Imaging.BitmapDecoder]::Create([Uri]$src, 'None', 'OnLoad')
$dec.CodecInfo.FriendlyName   # -> Microsoft Webp Decoder
Enter fullscreen mode Exit fullscreen mode

A whole folder is the same thing in a loop:

Add-Type -AssemblyName PresentationCore

Get-ChildItem "C:\Pictures\*.webp" | ForEach-Object {
    $f = [System.Windows.Media.Imaging.BitmapDecoder]::Create([Uri]$_.FullName, 'None', 'OnLoad').Frames[0]
    $e = New-Object System.Windows.Media.Imaging.JpegBitmapEncoder
    $e.QualityLevel = 90
    $e.Frames.Add($f)
    $o = [System.IO.File]::Create([IO.Path]::ChangeExtension($_.FullName, '.jpg'))
    $e.Save($o); $o.Close()
}
Enter fullscreen mode Exit fullscreen mode

Gotcha 1: the alpha channel is gone

This is the part worth knowing before you run it over a folder of logos. The frame that comes back from the Microsoft WebP decoder is Bgr32 - no alpha. Transparent pixels arrive black, and because the data is already flattened, switching to PngBitmapEncoder doesn't save you:

$f.Format              # -> Bgr32
# transparent WebP -> PNG, then inspect the corner pixel:
#   (0, 0, 0, 255)     # opaque black, not transparent
Enter fullscreen mode Exit fullscreen mode

Pillow reads the same file as RGBA, so if transparency matters, do it in Python and decide what goes behind it:

from PIL import Image
from pathlib import Path

for src in Path(".").glob("*.webp"):
    im = Image.open(src)
    if im.mode in ("RGBA", "LA", "P"):       # composite onto white instead of black
        im = im.convert("RGBA")
        bg = Image.new("RGB", im.size, (255, 255, 255))
        bg.paste(im, mask=im.split()[3])
        im = bg
    im.convert("RGB").save(src.with_suffix(".jpg"), quality=90)
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: animated WebP silently loses everything after frame 1

The decoder does expose the frames - $dec.Frames.Count returned 4 for my test file, and Pillow reported n_frames = 4 for the same one. But Frames[0] is what the snippet above encodes, and JPEG has nowhere to put the rest. If the motion matters, GIF is two lines:

Image.open("anim.webp").save("anim.gif", save_all=True, loop=0)
Enter fullscreen mode Exit fullscreen mode

And it gets bigger

WebP -> JPG is a re-encode of already-lossy data, so you pay twice: a little more generation loss, and usually a larger file. My 2.6 KB test WebP became 7.4 KB as a quality-90 JPEG. That tracks with Google's own numbers - lossy WebP is 25-34% smaller than JPEG at equivalent SSIM, lossless is 26% smaller than PNG - so converting "to save space" is backwards. Keep the original, and if size is the goal, resize instead of re-encoding.

Quick reference

Need Use
One or two files, no install Any browser-based converter that runs client-side
A folder, nothing installed PowerShell + WIC (above)
Transparency preserved or flattened your way Pillow
Animation kept Pillow -> GIF

Everything above was run on Windows 11 with Microsoft.WebpImageExtension 1.2.31: the codec name, the Bgr32 frame format, the black corner pixel, the frame counts and the file sizes are measured, not assumed.


Longer version with the non-developer paths (Paint, Photos, a drag-and-drop converter that doesn't upload): https://www.coding-now.com/en/guides/webp-to-jpg?utm_source=devto

Has the alpha-drop bitten anyone else, or is there a WIC flag I missed?

Top comments (0)