[ImageResizer] Fix JPEG quality setting ignored after WinUI3 migration (#47134)

<!-- 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
Restores honoring of the user-configured JPEG quality — via the Settings
UI slider, the CLI `--quality` flag, or the persisted
`imageresizer_jpegQualityLevel` — when resizing JPEG files. Since the
WinUI3 migration (#45288) any Q value from 1 to 100 produced
byte-identical output at WIC's internal default (~Q90) because the
transcode encoder silently ignored the setting. Only
`src/modules/imageresizer/ui/Models/ResizeOperation.cs` is changed.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #47135
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **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

Built `ImageResizerCLI.csproj` in `Release|x64` with
`/p:RuntimeIdentifier=win-x64`; no new warnings. Ran the resulting
`x64\Release\WinUI3Apps\PowerToys.ImageResizerCLI.exe` against a
synthetic test JPEG (`a.jpg` 2373×905, ~240 KB) and both in-tree EXIF
assets:

- `src/modules/imageresizer/tests/TestMetadataIssue1928.jpg` (42 EXIF
properties)
- `src/modules/imageresizer/tests/TestMetadataIssue2447.jpg` (44 EXIF
properties)

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
moooyo
2026-04-22 14:46:41 +08:00
committed by GitHub
parent fcfbf83b55
commit c8ffcb73c3

View File

@@ -102,11 +102,14 @@ namespace ImageResizer.Models
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.ReadWrite))
{
var winrtOutputStream = outputStream.AsRandomAccessStream();
bool forceFresh = encoderGuid == BitmapEncoder.JpegEncoderId && !noTransformNeeded;
await EncodeToStreamAsync(
decoder,
winrtInputStream,
winrtOutputStream,
encoderGuid,
forceFresh,
async (encoder, isTranscode) =>
{
if (isTranscode)
@@ -121,14 +124,6 @@ namespace ImageResizer.Models
{
encoder.BitmapTransform.Bounds = cropBounds.Value;
}
// Apply codec-specific properties (e.g., JPEG quality).
// Must be set after transforms since re-encoding will occur.
var encoderProps = GetEncoderPropertySet(encoderGuid);
if (encoderProps != null)
{
await encoder.BitmapProperties.SetPropertiesAsync(encoderProps);
}
}
}
else
@@ -188,11 +183,15 @@ namespace ImageResizer.Models
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.ReadWrite))
{
var winrtOutputStream = outputStream.AsRandomAccessStream();
// SetSoftwareBitmap requires a fresh encoder; the transcode encoder only
// accepts BitmapTransform. Also forces quality to apply for JPEG output.
await EncodeToStreamAsync(
decoder,
winrtInputStream,
winrtOutputStream,
encoderGuid,
forceFresh: true,
(encoder, _) =>
{
encoder.SetSoftwareBitmap(aiResult);
@@ -214,10 +213,12 @@ namespace ImageResizer.Models
IRandomAccessStream inputStream,
IRandomAccessStream outputStream,
Guid encoderGuid,
bool forceFresh,
Func<BitmapEncoder, bool, Task> writeContent)
{
var decoderEncoderId = CodecHelper.GetEncoderIdForDecoder(decoder);
bool canTranscode = !_settings.RemoveMetadata
bool canTranscode = !forceFresh
&& !_settings.RemoveMetadata
&& decoderEncoderId.HasValue
&& decoderEncoderId.Value == encoderGuid;
@@ -257,7 +258,8 @@ namespace ImageResizer.Models
/// <summary>
/// Fresh encoder path: creates a blank encoder and manually writes pixel data.
/// Used when metadata must be stripped (RemoveMetadata) or format doesn't match (ICO→PNG).
/// Used when codec options (e.g. JPEG quality) must apply, when metadata must be
/// stripped (RemoveMetadata), or when the format doesn't match (ICO→PNG).
/// The <paramref name="writeContent"/> callback receives isTranscode=false and should
/// call <see cref="EncodeFramesAsync"/> or <see cref="BitmapEncoder.SetSoftwareBitmap"/>.
/// </summary>
@@ -267,22 +269,18 @@ namespace ImageResizer.Models
Guid encoderGuid,
Func<BitmapEncoder, bool, Task> writeContent)
{
// Read rendering-critical metadata before encoding so we can restore it on
// the blank encoder. Only needed for RemoveMetadata; format-mismatch files
// (e.g. ICO) rarely carry meaningful EXIF data.
BitmapPropertySet renderingMetadata = null;
if (_settings.RemoveMetadata)
{
renderingMetadata = await ReadMetadataAsync(decoder, RenderingMetadataProperties);
}
// The blank encoder inherits nothing from the source, so we have to carry
// metadata over ourselves. RemoveMetadata keeps only the rendering-critical
// properties (orientation/colorspace); otherwise mirror the transcode path's
// best-effort EXIF preservation.
var propsToPreserve = _settings.RemoveMetadata
? RenderingMetadataProperties
: KnownMetadataProperties;
var preservedMetadata = await ReadMetadataAsync(decoder, propsToPreserve);
var encoder = await CreateFreshEncoderAsync(encoderGuid, outputStream);
await writeContent(encoder, false);
if (renderingMetadata != null)
{
await WriteMetadataAsync(encoder, renderingMetadata);
}
await WriteMetadataAsync(encoder, preservedMetadata);
await encoder.FlushAsync();
}