<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request With this skills, we can easily enable AI to complete most of the tasks involved in migrating from WPF to WinUI3. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46464 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Yu Leng <yuleng@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Imaging API Migration
Migrating from WPF (System.Windows.Media.Imaging / PresentationCore.dll) to WinRT (Windows.Graphics.Imaging). Based on the ImageResizer migration.
Why This Migration Is Required
WinUI 3 apps deployed as self-contained do NOT include PresentationCore.dll. Any code using System.Windows.Media.Imaging will throw FileNotFoundException at runtime. ALL imaging code must use WinRT APIs.
| Purpose | Namespace |
|---|---|
UI display (Image.Source) |
Microsoft.UI.Xaml.Media.Imaging |
| Image processing (encode/decode/transform) | Windows.Graphics.Imaging |
Architecture Change: Pipeline vs Declarative
The fundamental architecture differs:
WPF: In-memory pipeline of bitmap objects. Decode → transform → encode synchronously.
var decoder = BitmapDecoder.Create(stream, ...);
var transform = new TransformedBitmap(decoder.Frames[0], new ScaleTransform(...));
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(transform, ...));
encoder.Save(outputStream);
WinRT: Declarative transform model. Configure transforms on the encoder, which handles pixel manipulation internally. All async.
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
encoder.BitmapTransform.ScaledWidth = newWidth;
encoder.BitmapTransform.ScaledHeight = newHeight;
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
await encoder.FlushAsync();
Core Type Mapping
Decoders
| WPF | WinRT | Notes |
|---|---|---|
BitmapDecoder.Create(stream, options, cache) |
BitmapDecoder.CreateAsync(stream) |
Async, auto-detects format |
JpegBitmapDecoder / PngBitmapDecoder / etc. |
BitmapDecoder.CreateAsync(stream) |
Single unified decoder |
decoder.Frames[0] |
await decoder.GetFrameAsync(0) |
Async frame access |
decoder.Frames.Count |
decoder.FrameCount (uint) |
int → uint |
decoder.CodecInfo.ContainerFormat |
decoder.DecoderInformation.CodecId |
Different property path |
decoder.Frames[0].PixelWidth (int) |
decoder.PixelWidth (uint) |
int → uint |
WmpBitmapDecoder |
Not available | WMP/HDP not supported |
Encoders
| WPF | WinRT | Notes |
|---|---|---|
new JpegBitmapEncoder() |
BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream) |
Async factory |
new PngBitmapEncoder() |
BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream) |
No interlace control |
encoder.Frames.Add(frame) |
encoder.SetSoftwareBitmap(bitmap) |
Different API |
encoder.Save(stream) |
await encoder.FlushAsync() |
Async |
Encoder Properties (Strongly-Typed → BitmapPropertySet)
WPF had type-specific encoder subclasses. WinRT uses a generic property set:
// WPF
case JpegBitmapEncoder jpeg: jpeg.QualityLevel = 85; // int 1-100
case PngBitmapEncoder png: png.Interlace = PngInterlaceOption.On;
case TiffBitmapEncoder tiff: tiff.Compression = TiffCompressOption.Lzw;
// WinRT — JPEG quality (float 0.0-1.0)
await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
{
{ "ImageQuality", new BitmapTypedValue(0.85f, PropertyType.Single) }
});
// WinRT — TIFF compression (via BitmapPropertySet at creation time)
var props = new BitmapPropertySet
{
{ "TiffCompressionMethod", new BitmapTypedValue((byte)2, PropertyType.UInt8) }
};
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.TiffEncoderId, stream, props);
JPEG quality scale change: WPF int 1-100 → WinRT float 0.0-1.0. Divide by 100.
Bitmap Types
| WPF | WinRT | Notes |
|---|---|---|
BitmapSource |
SoftwareBitmap |
Central pixel-data type |
BitmapImage |
BitmapImage (in Microsoft.UI.Xaml.Media.Imaging) |
UI display only |
FormatConvertedBitmap |
SoftwareBitmap.Convert() |
|
TransformedBitmap + ScaleTransform |
BitmapTransform via encoder |
Declarative |
CroppedBitmap |
BitmapTransform.Bounds |
Metadata
| WPF | WinRT | Notes |
|---|---|---|
BitmapMetadata |
BitmapProperties |
Different API surface |
BitmapMetadata.Clone() |
No equivalent | Cannot selectively clone |
| Selective metadata removal | Not supported | All-or-nothing only |
Two encoder creation strategies for metadata:
CreateForTranscodingAsync()— preserves ALL metadata from sourceCreateAsync()— creates fresh encoder with NO metadata
This eliminated ~258 lines of manual metadata manipulation code (BitmapMetadataExtension.cs) in ImageResizer.
Interpolation Modes
WPF BitmapScalingMode |
WinRT BitmapInterpolationMode |
|---|---|
HighQuality / Fant |
Fant |
Linear |
Linear |
NearestNeighbor |
NearestNeighbor |
Unspecified / LowQuality |
Linear |
Stream Interop
WinRT imaging requires IRandomAccessStream instead of System.IO.Stream:
using var stream = File.OpenRead(path);
var winrtStream = stream.AsRandomAccessStream(); // Extension method
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
Critical: For transcode, seek the input stream back to 0 before creating the encoder:
winrtStream.Seek(0);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
CodecHelper Pattern (from ImageResizer)
WPF stored container format GUIDs in settings.json. WinRT uses different codec IDs. Create a CodecHelper to bridge them:
internal static class CodecHelper
{
// Maps WPF container format GUIDs (stored in settings JSON) to WinRT encoder IDs
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
{
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
};
// Maps decoder IDs to corresponding encoder IDs
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
{
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
// ...
};
public static Guid GetEncoderIdFromLegacyGuid(Guid legacyGuid)
=> LegacyGuidToEncoderId.GetValueOrDefault(legacyGuid, Guid.Empty);
public static Guid GetEncoderIdForDecoder(BitmapDecoder decoder)
=> DecoderIdToEncoderId.GetValueOrDefault(decoder.DecoderInformation.CodecId, Guid.Empty);
}
This preserves backward compatibility with existing settings.json files that contain WPF-era GUIDs.
ImagingEnums Pattern (from ImageResizer)
WPF-specific enums (PngInterlaceOption, TiffCompressOption) from System.Windows.Media.Imaging are used in settings JSON. Create custom enums with identical integer values for backward-compatible deserialization:
// Replace System.Windows.Media.Imaging.PngInterlaceOption
public enum PngInterlaceOption { Default = 0, On = 1, Off = 2 }
// Replace System.Windows.Media.Imaging.TiffCompressOption
public enum TiffCompressOption { Default = 0, None = 1, Ccitt3 = 2, Ccitt4 = 3, Lzw = 4, Rle = 5, Zip = 6 }
Async Migration Patterns
Method Signatures
All imaging operations become async:
| Before | After |
|---|---|
void Execute(file, settings) |
async Task ExecuteAsync(file, settings) |
IEnumerable<Error> Process() |
async Task<IEnumerable<Error>> ProcessAsync() |
Parallel Processing
// WPF (synchronous)
Parallel.ForEach(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
(file, state, i) => { Execute(file, settings); });
// WinRT (async)
await Parallel.ForEachAsync(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
async (file, ct) => { await ExecuteAsync(file, settings); });
CLI Async Bridge
CLI entry points must bridge async to sync:
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
Task.Factory.StartNew → Task.Run
// WPF
_ = Task.Factory.StartNew(StartExecutingWork, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
// WinUI 3
_ = Task.Run(() => StartExecutingWorkAsync());
SoftwareBitmap as Interface Type
When modules expose imaging interfaces (e.g., AI super-resolution), change parameter/return types:
// WPF
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
// WinRT
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
This eliminates manual BitmapSource ↔ SoftwareBitmap conversion code (unsafe IMemoryBufferByteAccess COM interop).
MultiFrame Image Handling
// WinRT multi-frame encode (e.g., multi-page TIFF, animated GIF)
for (uint i = 0; i < decoder.FrameCount; i++)
{
if (i > 0)
await encoder.GoToNextFrameAsync();
var frame = await decoder.GetFrameAsync(i);
var bitmap = await frame.GetSoftwareBitmapAsync(
frame.BitmapPixelFormat,
BitmapAlphaMode.Premultiplied,
transform,
ExifOrientationMode.IgnoreExifOrientation,
ColorManagementMode.DoNotColorManage);
encoder.SetSoftwareBitmap(bitmap);
}
await encoder.FlushAsync();
int → uint for Pixel Dimensions
WinRT uses uint for all pixel dimensions. This affects:
decoder.PixelWidth/decoder.PixelHeight—uintBitmapTransform.ScaledWidth/ScaledHeight—uintSoftwareBitmapconstructor —uintparameters- Test assertions:
Assert.AreEqual(96, ...)→Assert.AreEqual(96u, ...)
Display SoftwareBitmap in UI
var source = new SoftwareBitmapSource();
// Must convert to Bgra8/Premultiplied for display
if (bitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
bitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
{
bitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
await source.SetBitmapAsync(bitmap);
myImage.Source = source;
Known Limitations
| Feature | WPF | WinRT | Impact |
|---|---|---|---|
| PNG interlace | PngBitmapEncoder.Interlace |
Not available | Always non-interlaced |
| Metadata stripping | Selective via BitmapMetadata.Clone() |
All-or-nothing | Orientation EXIF also removed |
| Pixel formats | Many (Pbgra32, Bgr24, Indexed8, ...) |
Primarily Bgra8, Rgba8, Gray8/16 |
Convert to Bgra8 |
| WMP/HDP format | WmpBitmapDecoder |
Not available | Not supported |
| Pixel differences | WPF scaler | BitmapInterpolationMode.Fant |
Not bit-identical |