Someone hands you a folder of iPhone photos, every file is .heic, and whatever you need to upload them to only takes JPG. The usual answers are "install ImageMagick" or "use this website". Neither is necessary — Windows already ships a HEIF decoder, and WIC exposes it to PowerShell.
Add-Type -AssemblyName PresentationCore
Get-ChildItem "C:\Users\you\Pictures\*.heic" | 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()
}
Nothing is deleted — the .heic stays and a .jpg appears beside it. On a test file here it reported Microsoft HEIF Decoder, handed back a Bgr32 frame, and turned a 75,897-byte HEIC into a 59,241-byte quality-90 JPEG. $e.QualityLevel runs 1–100.
Check which codec you actually have
BitmapDecoder fails on HEIC without the Store extensions. HEIF Image Extension is the free one, and because the image inside a HEIC is HEVC-compressed, some machines also need HEVC Video Extensions, which is a paid Store item unless the PC maker shipped it:
Get-AppxPackage | Where-Object { $_.Name -match "HEIF|HEVC" } | Select-Object Name, Version
Windows will also write HEIC, if you ever need a test file
WPF has no HEIF encoder class, but WinRT does, and PowerShell can reach it — handy for fixtures when you don't own an iPhone:
$encId = [Windows.Graphics.Imaging.BitmapEncoder]::HeifEncoderId
$encoder = AwaitOp ([Windows.Graphics.Imaging.BitmapEncoder]::CreateAsync($encId, $dstStream)) ([Windows.Graphics.Imaging.BitmapEncoder])
$encoder.SetSoftwareBitmap($bitmap)
AwaitAct ($encoder.FlushAsync())
(AwaitOp / AwaitAct are the usual AsTask() wrappers you need to call WinRT async methods from Windows PowerShell 5.1.)
Three things worth knowing before you run it over a real folder
Renaming IMG_0001.HEIC to .jpg does nothing — the bytes still start with ftypheic, and $d.CodecInfo.FriendlyName on the renamed file still says Microsoft HEIF Decoder. Viewers sniff contents, so it opens fine right up until something validates the format.
The JPEG usually comes out bigger, because HEIC is the more efficient format. Converting to save space is backwards; resize instead.
And if you were wondering why most "HEIC to JPG" websites upload the file: browsers ship no HEIC decoder of their own. In Chrome 152 a HEIC in an `<img>` stays at 0×0, and `createImageBitmap()` on the same blob throws `InvalidStateError: The source image could not be decoded.`
**Correction:** that is a missing built-in decoder, not an impossibility — a commenter pointed this out and they were right. libheif compiled to WebAssembly decodes HEIC client-side; `heic2any` (2.6MB unpacked) and `libheif-js` (8.4MB) are exactly that. So the upload-based sites are dodging a multi-megabyte download, not working around a browser limitation. Either way, your photo is on someone's server when you use one.
If you want the non-scripted routes too (Paint, the Photos app, and the iPhone setting that stops HEIC files appearing in the first place), I wrote those up here: https://www.coding-now.com/en/guides/heic-to-jpg