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

288 lines
11 KiB
Markdown

# 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.
```csharp
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.
```csharp
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:
```csharp
// 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`:
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
// 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
```csharp
// 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:
```csharp
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
```
### Task.Factory.StartNew → Task.Run
```csharp
// 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:
```csharp
// 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
```csharp
// 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``uint`
- `BitmapTransform.ScaledWidth` / `ScaledHeight``uint`
- `SoftwareBitmap` constructor — `uint` parameters
- Test assertions: `Assert.AreEqual(96, ...)``Assert.AreEqual(96u, ...)`
## Display SoftwareBitmap in UI
```csharp
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 |