Files
PowerToys/.github/skills/wpf-to-winui3-migration/references/imaging-migration.md
moooyo 7051b8939b [Skills] Add WPF to WinUI 3 migration agent skill (#46462)
<!-- 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>
2026-03-24 09:40:33 +00:00

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) intuint
decoder.CodecInfo.ContainerFormat decoder.DecoderInformation.CodecId Different property path
decoder.Frames[0].PixelWidth (int) decoder.PixelWidth (uint) intuint
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 source
  • CreateAsync() — 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.PixelHeightuint
  • BitmapTransform.ScaledWidth / ScaledHeightuint
  • SoftwareBitmap constructor — uint parameters
  • 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