diff --git a/src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj b/src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj
index 48906a241a..bfd5c0902e 100644
--- a/src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj
+++ b/src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj
@@ -11,8 +11,7 @@
$(SolutionDir)$(Platform)\$(Configuration)\tests\$(AssemblyName)\
-
- true
+ true
diff --git a/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs b/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs
index bd6031cad4..12b2cb4830 100644
--- a/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs
+++ b/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs
@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -50,29 +51,14 @@ namespace ImageResizer.Models
Assert.AreEqual("OutputDir", result.DestinationDirectory);
}
- /*[Fact]
- public void Process_executes_in_parallel()
- {
- var batch = CreateBatch(_ => Thread.Sleep(50));
- batch.Files.AddRange(
- Enumerable.Range(0, Environment.ProcessorCount)
- .Select(i => "Image" + i + ".jpg"));
-
- var stopwatch = Stopwatch.StartNew();
- batch.Process(CancellationToken.None, (_, __) => { });
- stopwatch.Stop();
-
- Assert.InRange(stopwatch.ElapsedMilliseconds, 50, 99);
- }*/
-
[TestMethod]
- public void ProcessAggregatesErrors()
+ public async Task ProcessAggregatesErrors()
{
var batch = CreateBatch(file => throw new InvalidOperationException("Error: " + file));
batch.Files.Add("Image1.jpg");
batch.Files.Add("Image2.jpg");
- var errors = batch.Process((_, __) => { }, CancellationToken.None).ToList();
+ var errors = (await batch.ProcessAsync((_, __) => { }, CancellationToken.None)).ToList();
Assert.AreEqual(2, errors.Count);
@@ -91,14 +77,14 @@ namespace ImageResizer.Models
}
[TestMethod]
- public void ProcessReportsProgress()
+ public async Task ProcessReportsProgress()
{
var batch = CreateBatch(_ => { });
batch.Files.Add("Image1.jpg");
batch.Files.Add("Image2.jpg");
var calls = new ConcurrentBag<(int I, double Count)>();
- batch.Process(
+ await batch.ProcessAsync(
(i, count) => calls.Add((i, count)),
CancellationToken.None);
@@ -109,8 +95,12 @@ namespace ImageResizer.Models
{
var mock = new Mock { CallBase = true };
mock.Protected()
- .Setup("Execute", ItExpr.IsAny(), ItExpr.IsAny())
- .Callback((string file, Settings settings) => executeAction(file));
+ .Setup("ExecuteAsync", ItExpr.IsAny(), ItExpr.IsAny())
+ .Returns((string file, Settings settings) =>
+ {
+ executeAction(file);
+ return Task.CompletedTask;
+ });
return mock.Object;
}
diff --git a/src/modules/imageresizer/tests/Models/ResizeOperationTests.cs b/src/modules/imageresizer/tests/Models/ResizeOperationTests.cs
index d91a4e4879..d9e2f75d7b 100644
--- a/src/modules/imageresizer/tests/Models/ResizeOperationTests.cs
+++ b/src/modules/imageresizer/tests/Models/ResizeOperationTests.cs
@@ -7,10 +7,8 @@
using System;
using System.IO;
using System.Linq;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
+using System.Threading.Tasks;
-using ImageResizer.Extensions;
using ImageResizer.Properties;
using ImageResizer.Test;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -20,45 +18,59 @@ namespace ImageResizer.Models
[TestClass]
public class ResizeOperationTests : IDisposable
{
+ // Known legacy container format GUID for PNG, used as FallbackEncoder value in settings JSON
+ private static readonly Guid PngContainerFormatGuid = new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf");
+
+ private static readonly string[] DateTakenPropertyQuery = new[] { "System.Photo.DateTaken" };
+ private static readonly string[] CameraModelPropertyQuery = new[] { "System.Photo.CameraModel" };
+
private readonly TestDirectory _directory = new TestDirectory();
private bool disposedValue;
[TestMethod]
- public void ExecuteCopiesFrameMetadata()
+ public async Task ExecuteCopiesFrameMetadata()
{
var operation = new ResizeOperation("Test.jpg", _directory, Settings());
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image => Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment));
+ async decoder =>
+ {
+ var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
+ Assert.IsTrue(props.ContainsKey("System.Photo.DateTaken"), "Metadata should be preserved during transcode");
+ });
}
[TestMethod]
- public void ExecuteCopiesFrameMetadataEvenWhenMetadataCannotBeCloned()
+ public async Task ExecuteCopiesFrameMetadataEvenWhenMetadataCannotBeCloned()
{
var operation = new ResizeOperation("TestMetadataIssue2447.jpg", _directory, Settings());
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image => Assert.IsNotNull(((BitmapMetadata)image.Frames[0].Metadata).CameraModel));
+ async decoder =>
+ {
+ var props = await decoder.BitmapProperties.GetPropertiesAsync(CameraModelPropertyQuery);
+ Assert.IsTrue(props.ContainsKey("System.Photo.CameraModel"), "Camera model metadata should be preserved");
+ });
}
[TestMethod]
- public void ExecuteKeepsDateModified()
+ public async Task ExecuteKeepsDateModified()
{
var operation = new ResizeOperation("Test.png", _directory, Settings(s => s.KeepDateModified = true));
- operation.Execute();
+ await operation.ExecuteAsync();
Assert.AreEqual(File.GetLastWriteTimeUtc("Test.png"), File.GetLastWriteTimeUtc(_directory.File()));
}
[TestMethod]
- public void ExecuteKeepsDateModifiedWhenReplacingOriginals()
+ public async Task ExecuteKeepsDateModifiedWhenReplacingOriginals()
{
var path = Path.Combine(_directory, "Test.png");
File.Copy("Test.png", path);
@@ -75,55 +87,59 @@ namespace ImageResizer.Models
s.Replace = true;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
Assert.AreEqual(originalDateModified, File.GetLastWriteTimeUtc(_directory.File()));
}
[TestMethod]
- public void ExecuteReplacesOriginals()
+ public async Task ExecuteReplacesOriginals()
{
var path = Path.Combine(_directory, "Test.png");
File.Copy("Test.png", path);
var operation = new ResizeOperation(path, null, Settings(s => s.Replace = true));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(_directory.File(), image => Assert.AreEqual(96, image.Frames[0].PixelWidth));
+ await AssertEx.ImageAsync(_directory.File(), decoder => Assert.AreEqual(96u, decoder.PixelWidth));
}
[TestMethod]
- public void ExecuteTransformsEachFrame()
+ public async Task ExecuteTransformsEachFrame()
{
var operation = new ResizeOperation("Test.gif", _directory, Settings());
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ async decoder =>
{
- Assert.AreEqual(2, image.Frames.Count);
- AssertEx.All(image.Frames, frame => Assert.AreEqual(96, frame.PixelWidth));
+ Assert.AreEqual(2u, decoder.FrameCount);
+ for (uint i = 0; i < decoder.FrameCount; i++)
+ {
+ var frame = await decoder.GetFrameAsync(i);
+ Assert.AreEqual(96u, frame.PixelWidth);
+ }
});
}
[TestMethod]
- public void ExecuteUsesFallbackEncoder()
+ public async Task ExecuteUsesFallbackEncoder()
{
var operation = new ResizeOperation(
"Test.ico",
_directory,
- Settings(s => s.FallbackEncoder = new PngBitmapEncoder().CodecInfo.ContainerFormat));
+ Settings(s => s.FallbackEncoder = PngContainerFormatGuid));
- operation.Execute();
+ await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test).png");
}
[TestMethod]
- public void TransformIgnoresOrientationWhenLandscapeToPortrait()
+ public async Task TransformIgnoresOrientationWhenLandscapeToPortrait()
{
var operation = new ResizeOperation(
"Test.png",
@@ -136,19 +152,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 192;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(192, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ Assert.AreEqual(192u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformIgnoresOrientationWhenPortraitToLandscape()
+ public async Task TransformIgnoresOrientationWhenPortraitToLandscape()
{
var operation = new ResizeOperation(
"TestPortrait.png",
@@ -161,19 +177,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 96;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(192, image.Frames[0].PixelHeight);
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(192u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformIgnoresIgnoreOrientationWhenAuto()
+ public async Task TransformIgnoresIgnoreOrientationWhenAuto()
{
var operation = new ResizeOperation(
"Test.png",
@@ -186,19 +202,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 0;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(48, image.Frames[0].PixelHeight);
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(48u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformIgnoresIgnoreOrientationWhenPercent()
+ public async Task TransformIgnoresIgnoreOrientationWhenPercent()
{
var operation = new ResizeOperation(
"Test.png",
@@ -213,19 +229,19 @@ namespace ImageResizer.Models
x.SelectedSize.Fit = ResizeFit.Stretch;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(192, image.Frames[0].PixelHeight);
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(192u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsShrinkOnly()
+ public async Task TransformHonorsShrinkOnly()
{
var operation = new ResizeOperation(
"Test.png",
@@ -238,19 +254,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 288;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(192, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ Assert.AreEqual(192u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformIgnoresShrinkOnlyWhenPercent()
+ public async Task TransformIgnoresShrinkOnlyWhenPercent()
{
var operation = new ResizeOperation(
"Test.png",
@@ -263,19 +279,19 @@ namespace ImageResizer.Models
x.SelectedSize.Unit = ResizeUnit.Percent;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(256, image.Frames[0].PixelWidth);
- Assert.AreEqual(128, image.Frames[0].PixelHeight);
+ Assert.AreEqual(256u, decoder.PixelWidth);
+ Assert.AreEqual(128u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsShrinkOnlyWhenAutoHeight()
+ public async Task TransformHonorsShrinkOnlyWhenAutoHeight()
{
var operation = new ResizeOperation(
"Test.png",
@@ -288,15 +304,15 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 0;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image => Assert.AreEqual(192, image.Frames[0].PixelWidth));
+ decoder => Assert.AreEqual(192u, decoder.PixelWidth));
}
[TestMethod]
- public void TransformHonorsShrinkOnlyWhenAutoWidth()
+ public async Task TransformHonorsShrinkOnlyWhenAutoWidth()
{
var operation = new ResizeOperation(
"Test.png",
@@ -309,15 +325,15 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 288;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image => Assert.AreEqual(96, image.Frames[0].PixelHeight));
+ decoder => Assert.AreEqual(96u, decoder.PixelHeight));
}
[TestMethod]
- public void TransformHonorsUnit()
+ public async Task TransformHonorsUnit()
{
var operation = new ResizeOperation(
"Test.png",
@@ -330,82 +346,79 @@ namespace ImageResizer.Models
x.SelectedSize.Unit = ResizeUnit.Inch;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(_directory.File(), image => Assert.AreEqual(Math.Ceiling(image.Frames[0].DpiX), image.Frames[0].PixelWidth));
+ await AssertEx.ImageAsync(_directory.File(), decoder => Assert.AreEqual((uint)Math.Ceiling(decoder.DpiX), decoder.PixelWidth));
}
[TestMethod]
- public void TransformHonorsFitWhenFit()
+ public async Task TransformHonorsFitWhenFit()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(x => x.SelectedSize.Fit = ResizeFit.Fit));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(48, image.Frames[0].PixelHeight);
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(48u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsFitWhenFill()
+ public async Task TransformHonorsFitWhenFill()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(x => x.SelectedSize.Fit = ResizeFit.Fill));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ async decoder =>
{
- Assert.AreEqual(Colors.White, image.Frames[0].GetFirstPixel());
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ var pixel = await decoder.GetFirstPixelAsync();
+ Assert.AreEqual((byte)255, pixel.R, "First pixel R should be 255 (white)");
+ Assert.AreEqual((byte)255, pixel.G, "First pixel G should be 255 (white)");
+ Assert.AreEqual((byte)255, pixel.B, "First pixel B should be 255 (white)");
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsFitWhenStretch()
+ public async Task TransformHonorsFitWhenStretch()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(x => x.SelectedSize.Fit = ResizeFit.Stretch));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ async decoder =>
{
- Assert.AreEqual(Colors.Black, image.Frames[0].GetFirstPixel());
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ var pixel = await decoder.GetFirstPixelAsync();
+ Assert.AreEqual((byte)0, pixel.R, "First pixel R should be 0 (black)");
+ Assert.AreEqual((byte)0, pixel.G, "First pixel G should be 0 (black)");
+ Assert.AreEqual((byte)0, pixel.B, "First pixel B should be 0 (black)");
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsFillWithShrinkOnlyWhenCropRequired()
+ public async Task TransformHonorsFillWithShrinkOnlyWhenCropRequired()
{
- // Testing original 96x96 pixel Test.jpg cropped to 48x96 (Fill mode).
- //
- // ScaleX = 48/96 = 0.5
- // ScaleY = 96/96 = 1.0
- // Fill mode takes the max of these = 1.0.
- //
- // Previously, the transform logic saw the scale of 1.0 and returned the
- // original dimensions. The corrected logic recognizes that a crop is
- // required on one dimension and proceeds with the operation.
var operation = new ResizeOperation(
"Test.jpg",
_directory,
@@ -417,22 +430,20 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 96;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(48, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ Assert.AreEqual(48u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsFillWithShrinkOnlyWhenUpscaleAttempted()
+ public async Task TransformHonorsFillWithShrinkOnlyWhenUpscaleAttempted()
{
- // Confirm that attempting to upscale the original image will return the
- // original dimensions when Shrink Only is enabled.
var operation = new ResizeOperation(
"Test.jpg",
_directory,
@@ -444,21 +455,20 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 192;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void TransformHonorsFillWithShrinkOnlyWhenNoChangeRequired()
+ public async Task TransformHonorsFillWithShrinkOnlyWhenNoChangeRequired()
{
- // With a scale of 1.0 on both axes, the original should be returned.
var operation = new ResizeOperation(
"Test.jpg",
_directory,
@@ -470,70 +480,70 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 96;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image =>
+ decoder =>
{
- Assert.AreEqual(96, image.Frames[0].PixelWidth);
- Assert.AreEqual(96, image.Frames[0].PixelHeight);
+ Assert.AreEqual(96u, decoder.PixelWidth);
+ Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
- public void GetDestinationPathUniquifiesOutputFilename()
+ public async Task GetDestinationPathUniquifiesOutputFilename()
{
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty());
var operation = new ResizeOperation("Test.png", _directory, Settings());
- operation.Execute();
+ await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (1).png");
}
[TestMethod]
- public void GetDestinationPathUniquifiesOutputFilenameAgain()
+ public async Task GetDestinationPathUniquifiesOutputFilenameAgain()
{
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty());
File.WriteAllBytes(Path.Combine(_directory, "Test (Test) (1).png"), Array.Empty());
var operation = new ResizeOperation("Test.png", _directory, Settings());
- operation.Execute();
+ await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (2).png");
}
[TestMethod]
- public void GetDestinationPathUsesFileNameFormat()
+ public async Task GetDestinationPathUsesFileNameFormat()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(s => s.FileName = "%1_%2_%3_%4_%5_%6"));
- operation.Execute();
+ await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test_Test_96_96_96_48.png");
}
[TestMethod]
- public void ExecuteHandlesDirectoriesInFileNameFormat()
+ public async Task ExecuteHandlesDirectoriesInFileNameFormat()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(s => s.FileName = @"Directory\%1 (%2)"));
- operation.Execute();
+ await operation.ExecuteAsync();
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test (Test).png"));
}
[TestMethod]
- public void StripMetadata()
+ public async Task StripMetadata()
{
var operation = new ResizeOperation(
"TestMetadataIssue1928.jpg",
@@ -544,18 +554,26 @@ namespace ImageResizer.Models
x.RemoveMetadata = true;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).DateTaken));
- AssertEx.Image(
- _directory.File(),
- image => Assert.IsNotNull(((BitmapMetadata)image.Frames[0].Metadata).GetQuerySafe("System.Photo.Orientation")));
+ async decoder =>
+ {
+ try
+ {
+ var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
+ Assert.IsFalse(props.ContainsKey("System.Photo.DateTaken"), "DateTaken should be stripped");
+ }
+ catch (Exception)
+ {
+ // If GetPropertiesAsync throws, metadata is not present — which is expected
+ }
+ });
}
[TestMethod]
- public void StripMetadataWhenNoMetadataPresent()
+ public async Task StripMetadataWhenNoMetadataPresent()
{
var operation = new ResizeOperation(
"TestMetadataIssue1928_NoMetadata.jpg",
@@ -566,18 +584,26 @@ namespace ImageResizer.Models
x.RemoveMetadata = true;
}));
- operation.Execute();
+ await operation.ExecuteAsync();
- AssertEx.Image(
+ await AssertEx.ImageAsync(
_directory.File(),
- image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).DateTaken));
- AssertEx.Image(
- _directory.File(),
- image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).GetQuerySafe("System.Photo.Orientation")));
+ async decoder =>
+ {
+ try
+ {
+ var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
+ Assert.IsFalse(props.ContainsKey("System.Photo.DateTaken"), "DateTaken should not exist");
+ }
+ catch (Exception)
+ {
+ // Expected: no metadata block at all
+ }
+ });
}
[TestMethod]
- public void VerifyFileNameIsSanitized()
+ public async Task VerifyFileNameIsSanitized()
{
var operation = new ResizeOperation(
"Test.png",
@@ -589,13 +615,13 @@ namespace ImageResizer.Models
s.SelectedSize.Name = "Test\\/";
}));
- operation.Execute();
+ await operation.ExecuteAsync();
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test_______(Test__).png"));
}
[TestMethod]
- public void VerifyNotRecommendedNameIsChanged()
+ public async Task VerifyNotRecommendedNameIsChanged()
{
var operation = new ResizeOperation(
"Test.png",
@@ -606,7 +632,7 @@ namespace ImageResizer.Models
s.FileName = @"nul";
}));
- operation.Execute();
+ await operation.ExecuteAsync();
Assert.IsTrue(File.Exists(_directory + @"\nul_.png"));
}
diff --git a/src/modules/imageresizer/tests/Test/AssertEx.cs b/src/modules/imageresizer/tests/Test/AssertEx.cs
index 4cb9a2067b..0e802710da 100644
--- a/src/modules/imageresizer/tests/Test/AssertEx.cs
+++ b/src/modules/imageresizer/tests/Test/AssertEx.cs
@@ -8,11 +8,14 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
+using System.IO;
using System.IO.Abstractions;
-using System.Windows.Media.Imaging;
+using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Windows.Graphics.Imaging;
+
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1636:FileHeaderCopyrightTextMustMatch", Justification = "File created under PowerToys.")]
namespace ImageResizer.Test
@@ -29,17 +32,20 @@ namespace ImageResizer.Test
}
}
- public static void Image(string path, Action action)
+ public static async Task ImageAsync(string path, Action action)
{
- using (var stream = _fileSystem.File.OpenRead(path))
- {
- var image = BitmapDecoder.Create(
- stream,
- BitmapCreateOptions.PreservePixelFormat,
- BitmapCacheOption.None);
+ using var stream = _fileSystem.File.OpenRead(path);
+ var winrtStream = stream.AsRandomAccessStream();
+ var decoder = await BitmapDecoder.CreateAsync(winrtStream);
+ action(decoder);
+ }
- action(image);
- }
+ public static async Task ImageAsync(string path, Func action)
+ {
+ using var stream = _fileSystem.File.OpenRead(path);
+ var winrtStream = stream.AsRandomAccessStream();
+ var decoder = await BitmapDecoder.CreateAsync(winrtStream);
+ await action(decoder);
}
public static RaisedEvent Raises(
diff --git a/src/modules/imageresizer/tests/Test/BitmapSourceExtensions.cs b/src/modules/imageresizer/tests/Test/BitmapSourceExtensions.cs
index ed25e800f4..19d53dd113 100644
--- a/src/modules/imageresizer/tests/Test/BitmapSourceExtensions.cs
+++ b/src/modules/imageresizer/tests/Test/BitmapSourceExtensions.cs
@@ -1,28 +1,34 @@
-#pragma warning disable IDE0073
+#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
-using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
+using System;
+using System.Threading.Tasks;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
namespace ImageResizer.Test
{
internal static class BitmapSourceExtensions
{
- public static Color GetFirstPixel(this BitmapSource source)
+ public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(this BitmapDecoder decoder)
{
- var pixel = new byte[4];
- new FormatConvertedBitmap(
- new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
- PixelFormats.Bgra32,
- destinationPalette: null,
- alphaThreshold: 0)
- .CopyPixels(pixel, 4, 0);
+ using var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Premultiplied);
- return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
+ var buffer = new Windows.Storage.Streams.Buffer((uint)(softwareBitmap.PixelWidth * softwareBitmap.PixelHeight * 4));
+ softwareBitmap.CopyToBuffer(buffer);
+
+ using var reader = DataReader.FromBuffer(buffer);
+ byte b = reader.ReadByte();
+ byte g = reader.ReadByte();
+ byte r = reader.ReadByte();
+ byte a = reader.ReadByte();
+
+ return (r, g, b, a);
}
}
}
diff --git a/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs b/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
index bd22da62da..851d3e7983 100644
--- a/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
+++ b/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
@@ -6,6 +6,7 @@ using System;
using System.Globalization;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using ImageResizer.Models;
using ImageResizer.Properties;
@@ -58,10 +59,10 @@ namespace ImageResizer.Cli
return 1;
}
- return RunSilentMode(cliOptions);
+ return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
}
- private int RunSilentMode(CliOptions cliOptions)
+ private async Task RunSilentModeAsync(CliOptions cliOptions)
{
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
var settings = Settings.Default;
@@ -73,7 +74,7 @@ namespace ImageResizer.Cli
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
int lastReportedMilestone = -1;
- var errors = batch.Process(
+ var errors = await batch.ProcessAsync(
(completed, total) =>
{
var progress = (int)((completed / total) * 100);
diff --git a/src/modules/imageresizer/ui/Extensions/BitmapEncoderExtensions.cs b/src/modules/imageresizer/ui/Extensions/BitmapEncoderExtensions.cs
deleted file mode 100644
index f24a0663e2..0000000000
--- a/src/modules/imageresizer/ui/Extensions/BitmapEncoderExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma warning disable IDE0073
-// Copyright (c) Brice Lambson
-// The Brice Lambson licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
-#pragma warning restore IDE0073
-
-namespace System.Windows.Media.Imaging
-{
- internal static class BitmapEncoderExtensions
- {
- public static bool CanEncode(this BitmapEncoder encoder)
- {
- try
- {
- var temp = encoder.CodecInfo;
- }
- catch (NotSupportedException)
- {
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs b/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs
deleted file mode 100644
index fed4d4d05e..0000000000
--- a/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Windows.Media.Imaging;
-
-namespace ImageResizer.Extensions
-{
- internal static class BitmapMetadataExtension
- {
- public static void CopyMetadataPropertyTo(this BitmapMetadata source, BitmapMetadata target, string query)
- {
- if (source == null || target == null || string.IsNullOrWhiteSpace(query))
- {
- return;
- }
-
- try
- {
- var value = source.GetQuerySafe(query);
-
- if (value == null)
- {
- return;
- }
-
- target.SetQuery(query, value);
- }
- catch (InvalidOperationException)
- {
- // InvalidOperationException is thrown if metadata object is in readonly state.
- return;
- }
- }
-
- public static object GetQuerySafe(this BitmapMetadata metadata, string query)
- {
- if (metadata == null || string.IsNullOrWhiteSpace(query))
- {
- return null;
- }
-
- try
- {
- if (metadata.ContainsQuery(query))
- {
- return metadata.GetQuery(query);
- }
- else
- {
- return null;
- }
- }
- catch (NotSupportedException)
- {
- // NotSupportedException is throw if the metadata entry is not preset on the target image (e.g. Orientation not set).
- return null;
- }
- }
-
- public static void RemoveQuerySafe(this BitmapMetadata metadata, string query)
- {
- if (metadata == null || string.IsNullOrWhiteSpace(query))
- {
- return;
- }
-
- try
- {
- if (metadata.ContainsQuery(query))
- {
- metadata.RemoveQuery(query);
- }
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Exception while trying to remove metadata entry at position: {query}");
- Debug.WriteLine(ex);
- }
- }
-
- public static void SetQuerySafe(this BitmapMetadata metadata, string query, object value)
- {
- if (metadata == null || string.IsNullOrWhiteSpace(query) || value == null)
- {
- return;
- }
-
- try
- {
- metadata.SetQuery(query, value);
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Exception while trying to set metadata {value} at position: {query}");
- Debug.WriteLine(ex);
- }
- }
-
- ///
- /// Gets all metadata.
- /// Iterates recursively through metadata and adds valid items to a list while skipping invalid data items.
- ///
- ///
- /// Invalid data items are items which throw an exception when reading the data with metadata.GetQuery(...).
- /// Sometimes Metadata collections are improper closed and cause an exception on IEnumerator.MoveNext(). In this case, we return all data items which were successfully collected so far.
- ///
- ///
- /// metadata path and metadata value of all successfully read data items.
- ///
- public static List<(string MetadataPath, object Value)> GetListOfMetadata(this BitmapMetadata metadata)
- {
- var listOfAllMetadata = new List<(string MetadataPath, object Value)>();
-
- try
- {
- GetMetadataRecursively(metadata, string.Empty);
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Exception while trying to iterate recursively over metadata. We were able to read {listOfAllMetadata.Count} metadata entries.");
- Debug.WriteLine(ex);
- }
-
- return listOfAllMetadata;
-
- void GetMetadataRecursively(BitmapMetadata metadata, string query)
- {
- foreach (string relativeQuery in metadata)
- {
- string absolutePath = query + relativeQuery;
-
- object metadataQueryReader = null;
-
- try
- {
- metadataQueryReader = GetQueryWithPreCheck(metadata, relativeQuery);
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Removing corrupt metadata property {absolutePath}. Skipping metadata entry | {ex.Message}");
- Debug.WriteLine(ex);
- }
-
- if (metadataQueryReader != null)
- {
- listOfAllMetadata.Add((absolutePath, metadataQueryReader));
- }
- else
- {
- Debug.WriteLine($"No metadata found for query {absolutePath}. Skipping empty null entry because its invalid.");
- }
-
- if (metadataQueryReader is BitmapMetadata innerMetadata)
- {
- GetMetadataRecursively(innerMetadata, absolutePath);
- }
- }
- }
-
- object GetQueryWithPreCheck(BitmapMetadata metadata, string query)
- {
- if (metadata == null || string.IsNullOrWhiteSpace(query))
- {
- return null;
- }
-
- if (metadata.ContainsQuery(query))
- {
- return metadata.GetQuery(query);
- }
- else
- {
- return null;
- }
- }
- }
-
- ///
- /// Prints all metadata to debug console
- ///
- ///
- /// Intended for debug only!!!
- ///
- public static void PrintsAllMetadataToDebugOutput(this BitmapMetadata metadata)
- {
- if (metadata == null)
- {
- Debug.WriteLine($"Metadata was null.");
- }
-
- var listOfMetadata = metadata.GetListOfMetadataForDebug();
- foreach (var metadataItem in listOfMetadata)
- {
- // Debug.WriteLine($"modifiableMetadata.RemoveQuerySafe(\"{metadataItem.metadataPath}\");");
- Debug.WriteLine($"{metadataItem.MetadataPath} | {metadataItem.Value}");
- }
- }
-
- ///
- /// Gets all metadata
- /// Iterates recursively through all metadata
- ///
- ///
- /// Intended for debug only!!!
- ///
- public static List<(string MetadataPath, object Value)> GetListOfMetadataForDebug(this BitmapMetadata metadata)
- {
- var listOfAllMetadata = new List<(string MetadataPath, object Value)>();
-
- try
- {
- GetMetadataRecursively(metadata, string.Empty);
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Exception while trying to iterate recursively over metadata. We were able to read {listOfAllMetadata.Count} metadata entries.");
- Debug.WriteLine(ex);
- }
-
- return listOfAllMetadata;
-
- void GetMetadataRecursively(BitmapMetadata metadata, string query)
- {
- if (metadata == null)
- {
- return;
- }
-
- foreach (string relativeQuery in metadata)
- {
- string absolutePath = query + relativeQuery;
-
- object metadataQueryReader = null;
-
- try
- {
- metadataQueryReader = metadata.GetQuerySafe(relativeQuery);
- listOfAllMetadata.Add((absolutePath, metadataQueryReader));
- }
- catch (Exception ex)
- {
- listOfAllMetadata.Add((absolutePath, $"######## INVALID METADATA: {ex.Message}"));
- Debug.WriteLine(ex);
- }
-
- if (metadataQueryReader is BitmapMetadata innerMetadata)
- {
- GetMetadataRecursively(innerMetadata, absolutePath);
- }
- }
- }
- }
- }
-}
diff --git a/src/modules/imageresizer/ui/ImageResizerXAML/Views/InputPage.xaml b/src/modules/imageresizer/ui/ImageResizerXAML/Views/InputPage.xaml
index e93ac553ca..10a751cbf7 100644
--- a/src/modules/imageresizer/ui/ImageResizerXAML/Views/InputPage.xaml
+++ b/src/modules/imageresizer/ui/ImageResizerXAML/Views/InputPage.xaml
@@ -121,7 +121,7 @@
-
+
+ /// PNG interlace option for the encoder.
+ /// Integer values preserve backward compatibility with existing settings JSON.
+ ///
+ public enum PngInterlaceOption
+ {
+ Default = 0,
+ On = 1,
+ Off = 2,
+ }
+
+ ///
+ /// TIFF compression option for the encoder.
+ /// Integer values preserve backward compatibility with existing settings JSON.
+ ///
+ public enum TiffCompressOption
+ {
+ Default = 0,
+ None = 1,
+ Ccitt3 = 2,
+ Ccitt4 = 3,
+ Lzw = 4,
+ Rle = 5,
+ Zip = 6,
+ }
+}
diff --git a/src/modules/imageresizer/ui/Models/ResizeBatch.cs b/src/modules/imageresizer/ui/Models/ResizeBatch.cs
index cb8fa39bd1..d1c32d4108 100644
--- a/src/modules/imageresizer/ui/Models/ResizeBatch.cs
+++ b/src/modules/imageresizer/ui/Models/ResizeBatch.cs
@@ -140,33 +140,31 @@ namespace ImageResizer.Models
return FromCliOptions(standardInput, options);
}
- public IEnumerable Process(Action reportProgress, CancellationToken cancellationToken)
+ public Task> ProcessAsync(Action reportProgress, CancellationToken cancellationToken)
{
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
- return Process(reportProgress, Settings.Default, cancellationToken);
+ return ProcessAsync(reportProgress, Settings.Default, cancellationToken);
}
- public IEnumerable Process(Action reportProgress, Settings settings, CancellationToken cancellationToken)
+ public async Task> ProcessAsync(Action reportProgress, Settings settings, CancellationToken cancellationToken)
{
double total = Files.Count;
int completed = 0;
var errors = new ConcurrentBag();
- // TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
- // APIs and a custom SynchronizationContext
- Parallel.ForEach(
+ await Parallel.ForEachAsync(
Files,
new ParallelOptions
{
CancellationToken = cancellationToken,
},
- (file, state, i) =>
+ async (file, ct) =>
{
try
{
- Execute(file, settings);
+ await ExecuteAsync(file, settings);
}
catch (Exception ex)
{
@@ -180,10 +178,10 @@ namespace ImageResizer.Models
return errors;
}
- protected virtual void Execute(string file, Settings settings)
+ protected virtual async Task ExecuteAsync(string file, Settings settings)
{
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
- new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
+ await new ResizeOperation(file, DestinationDirectory, settings, aiService).ExecuteAsync();
}
}
}
diff --git a/src/modules/imageresizer/ui/Models/ResizeOperation.cs b/src/modules/imageresizer/ui/Models/ResizeOperation.cs
index 9296ceb4c4..28ae40a122 100644
--- a/src/modules/imageresizer/ui/Models/ResizeOperation.cs
+++ b/src/modules/imageresizer/ui/Models/ResizeOperation.cs
@@ -5,16 +5,15 @@
#pragma warning restore IDE0073
using System;
-using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Text;
-using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using ImageResizer.Extensions;
+using System.Threading.Tasks;
+using Windows.Foundation;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
using ImageResizer.Helpers;
using ImageResizer.Properties;
using ImageResizer.Services;
@@ -65,78 +64,134 @@ namespace ImageResizer.Models
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
}
- public void Execute()
+ public async Task ExecuteAsync()
{
string path;
+
using (var inputStream = _fileSystem.File.OpenRead(_file))
{
- var decoder = BitmapDecoder.Create(
- inputStream,
- BitmapCreateOptions.PreservePixelFormat,
- BitmapCacheOption.None);
+ var winrtInputStream = inputStream.AsRandomAccessStream();
+ var decoder = await BitmapDecoder.CreateAsync(winrtInputStream);
- var containerFormat = decoder.CodecInfo.ContainerFormat;
-
- var encoder = CreateEncoder(containerFormat);
-
- if (decoder.Metadata != null)
+ // Determine encoder ID from decoder
+ var encoderId = CodecHelper.GetEncoderIdForDecoder(decoder);
+ if (encoderId == null || !CodecHelper.CanEncode(encoderId.Value))
{
- try
- {
- encoder.Metadata = decoder.Metadata;
- }
- catch (InvalidOperationException)
- {
- }
+ encoderId = CodecHelper.GetEncoderIdFromLegacyGuid(_settings.FallbackEncoder);
}
- if (decoder.Palette != null)
+ var encoderGuid = encoderId.Value;
+
+ if (_settings.SelectedSize is AiSize)
{
- encoder.Palette = decoder.Palette;
+ // AI super resolution path
+ path = await ExecuteAiAsync(decoder, winrtInputStream, encoderGuid);
}
-
- foreach (var originalFrame in decoder.Frames)
+ else
{
- var transformedBitmap = Transform(originalFrame);
+ // Standard resize path
+ var originalWidth = (int)decoder.PixelWidth;
+ var originalHeight = (int)decoder.PixelHeight;
+ var dpiX = decoder.DpiX;
+ var dpiY = decoder.DpiY;
- // if the frame was not modified, we should not replace the metadata
- if (transformedBitmap == originalFrame)
+ var (scaledWidth, scaledHeight, cropBounds, noTransformNeeded) =
+ CalculateDimensions(originalWidth, originalHeight, dpiX, dpiY);
+
+ // For destination path, calculate final output dimensions
+ int outputWidth, outputHeight;
+ if (noTransformNeeded)
{
- encoder.Frames.Add(originalFrame);
+ outputWidth = originalWidth;
+ outputHeight = originalHeight;
+ }
+ else if (cropBounds.HasValue)
+ {
+ outputWidth = (int)cropBounds.Value.Width;
+ outputHeight = (int)cropBounds.Value.Height;
}
else
{
- BitmapMetadata originalMetadata = (BitmapMetadata)originalFrame.Metadata;
-
-#if DEBUG
- Debug.WriteLine($"### Processing metadata of file {_file}");
- originalMetadata.PrintsAllMetadataToDebugOutput();
-#endif
-
- var metadata = GetValidMetadata(originalMetadata, transformedBitmap, containerFormat);
-
- if (_settings.RemoveMetadata && metadata != null)
- {
- // strip any metadata that doesn't affect rendering
- var newMetadata = new BitmapMetadata(metadata.Format);
-
- metadata.CopyMetadataPropertyTo(newMetadata, "System.Photo.Orientation");
- metadata.CopyMetadataPropertyTo(newMetadata, "System.Image.ColorSpace");
-
- metadata = newMetadata;
- }
-
- var frame = CreateBitmapFrame(transformedBitmap, metadata);
-
- encoder.Frames.Add(frame);
+ outputWidth = (int)scaledWidth;
+ outputHeight = (int)scaledHeight;
}
- }
- path = GetDestinationPath(encoder);
- _fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
- using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
- {
- encoder.Save(outputStream);
+ path = GetDestinationPath(encoderGuid, outputWidth, outputHeight);
+ _fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
+
+ using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
+ {
+ var winrtOutputStream = outputStream.AsRandomAccessStream();
+
+ if (!_settings.RemoveMetadata)
+ {
+ // Transcode path: preserves all metadata automatically
+ winrtInputStream.Seek(0);
+ var encoder = await BitmapEncoder.CreateForTranscodingAsync(winrtOutputStream, decoder);
+
+ if (!noTransformNeeded)
+ {
+ encoder.BitmapTransform.ScaledWidth = scaledWidth;
+ encoder.BitmapTransform.ScaledHeight = scaledHeight;
+ encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
+
+ if (cropBounds.HasValue)
+ {
+ encoder.BitmapTransform.Bounds = cropBounds.Value;
+ }
+ }
+
+ await ConfigureEncoderPropertiesAsync(encoder, encoderGuid);
+ await encoder.FlushAsync();
+ }
+ else
+ {
+ // Strip metadata path: fresh encoder = no old metadata
+ var propertySet = GetEncoderPropertySet(encoderGuid);
+ var encoder = propertySet != null
+ ? await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream, propertySet)
+ : await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream);
+
+ var transform = new BitmapTransform();
+ if (!noTransformNeeded)
+ {
+ transform.ScaledWidth = scaledWidth;
+ transform.ScaledHeight = scaledHeight;
+ transform.InterpolationMode = BitmapInterpolationMode.Fant;
+
+ if (cropBounds.HasValue)
+ {
+ transform.Bounds = cropBounds.Value;
+ }
+ }
+ else
+ {
+ transform.ScaledWidth = (uint)originalWidth;
+ transform.ScaledHeight = (uint)originalHeight;
+ }
+
+ // Handle multi-frame images (e.g., 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();
+ }
+ }
}
}
@@ -153,50 +208,72 @@ namespace ImageResizer.Models
}
}
- private BitmapEncoder CreateEncoder(Guid containerFormat)
+ private async Task ExecuteAiAsync(BitmapDecoder decoder, IRandomAccessStream winrtInputStream, Guid encoderGuid)
{
- var createdEncoder = BitmapEncoder.Create(containerFormat);
- if (!createdEncoder.CanEncode())
+ try
{
- createdEncoder = BitmapEncoder.Create(_settings.FallbackEncoder);
- }
+ // Get the source bitmap for AI processing
+ var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Premultiplied);
- ConfigureEncoder(createdEncoder);
+ var aiResult = _aiSuperResolutionService.ApplySuperResolution(
+ softwareBitmap,
+ _settings.AiSize.Scale,
+ _file);
- return createdEncoder;
-
- void ConfigureEncoder(BitmapEncoder encoder)
- {
- switch (encoder)
+ if (aiResult == null)
{
- case JpegBitmapEncoder jpegEncoder:
- jpegEncoder.QualityLevel = MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100);
- break;
-
- case PngBitmapEncoder pngBitmapEncoder:
- pngBitmapEncoder.Interlace = _settings.PngInterlaceOption;
- break;
-
- case TiffBitmapEncoder tiffEncoder:
- tiffEncoder.Compression = _settings.TiffCompressOption;
- break;
+ throw new InvalidOperationException(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiConversionFailed"));
}
+
+ var outputWidth = aiResult.PixelWidth;
+ var outputHeight = aiResult.PixelHeight;
+
+ var path = GetDestinationPath(encoderGuid, outputWidth, outputHeight);
+ _fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
+
+ using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
+ {
+ var winrtOutputStream = outputStream.AsRandomAccessStream();
+
+ if (!_settings.RemoveMetadata)
+ {
+ // Transcode path: preserves metadata
+ winrtInputStream.Seek(0);
+ var encoder = await BitmapEncoder.CreateForTranscodingAsync(winrtOutputStream, decoder);
+ encoder.SetSoftwareBitmap(aiResult);
+ await ConfigureEncoderPropertiesAsync(encoder, encoderGuid);
+ await encoder.FlushAsync();
+ }
+ else
+ {
+ // Strip metadata path
+ var propertySet = GetEncoderPropertySet(encoderGuid);
+ var encoder = propertySet != null
+ ? await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream, propertySet)
+ : await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream);
+
+ encoder.SetSoftwareBitmap(aiResult);
+ await encoder.FlushAsync();
+ }
+ }
+
+ return path;
+ }
+ catch (Exception ex) when (ex is not InvalidOperationException)
+ {
+ var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
+ throw new InvalidOperationException(errorMessage, ex);
}
}
- private BitmapSource Transform(BitmapSource source)
+ private (uint ScaledWidth, uint ScaledHeight, BitmapBounds? CropBounds, bool NoTransformNeeded) CalculateDimensions(
+ int originalWidth, int originalHeight, double dpiX, double dpiY)
{
- if (_settings.SelectedSize is AiSize)
- {
- return TransformWithAi(source);
- }
-
- int originalWidth = source.PixelWidth;
- int originalHeight = source.PixelHeight;
-
// Convert from the chosen size unit to pixels, if necessary.
- double width = _settings.SelectedSize.GetPixelWidth(originalWidth, source.DpiX);
- double height = _settings.SelectedSize.GetPixelHeight(originalHeight, source.DpiY);
+ double width = _settings.SelectedSize.GetPixelWidth(originalWidth, dpiX);
+ double height = _settings.SelectedSize.GetPixelHeight(originalHeight, dpiY);
// Swap target width/height dimensions if orientation correction is required.
bool canSwapDimensions = _settings.IgnoreOrientation &&
@@ -238,7 +315,7 @@ namespace ImageResizer.Models
{
if (scaleX > 1 || scaleY > 1)
{
- return source;
+ return ((uint)originalWidth, (uint)originalHeight, null, true);
}
bool isFillCropRequired = _settings.SelectedSize.Fit == ResizeFit.Fill &&
@@ -246,140 +323,95 @@ namespace ImageResizer.Models
if (scaleX == 1 && scaleY == 1 && !isFillCropRequired)
{
- return source;
+ return ((uint)originalWidth, (uint)originalHeight, null, true);
}
}
- // Apply the scaling.
- var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, scaleY));
+ // Calculate scaled dimensions
+ uint scaledWidth = (uint)Math.Max(1, (int)Math.Round(originalWidth * scaleX));
+ uint scaledHeight = (uint)Math.Max(1, (int)Math.Round(originalHeight * scaleY));
// Apply the centered crop for Fill mode, if necessary.
if (_settings.SelectedSize.Fit == ResizeFit.Fill
- && (scaledBitmap.PixelWidth > width
- || scaledBitmap.PixelHeight > height))
+ && (scaledWidth > (uint)width || scaledHeight > (uint)height))
{
- int x = (int)(((originalWidth * scaleX) - width) / 2);
- int y = (int)(((originalHeight * scaleY) - height) / 2);
+ uint cropX = (uint)(((originalWidth * scaleX) - width) / 2);
+ uint cropY = (uint)(((originalHeight * scaleY) - height) / 2);
- return new CroppedBitmap(scaledBitmap, new Int32Rect(x, y, (int)width, (int)height));
- }
-
- return scaledBitmap;
- }
-
- private BitmapSource TransformWithAi(BitmapSource source)
- {
- try
- {
- var result = _aiSuperResolutionService.ApplySuperResolution(
- source,
- _settings.AiSize.Scale,
- _file);
-
- if (result == null)
+ var cropBounds = new BitmapBounds
{
- throw new InvalidOperationException(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiConversionFailed"));
- }
+ X = cropX,
+ Y = cropY,
+ Width = (uint)width,
+ Height = (uint)height,
+ };
- return result;
+ return (scaledWidth, scaledHeight, cropBounds, false);
}
- catch (Exception ex)
+
+ return (scaledWidth, scaledHeight, null, false);
+ }
+
+ private async Task ConfigureEncoderPropertiesAsync(BitmapEncoder encoder, Guid encoderGuid)
+ {
+ if (encoderGuid == BitmapEncoder.JpegEncoderId)
{
- var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
- throw new InvalidOperationException(errorMessage, ex);
+ await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
+ {
+ { "ImageQuality", new BitmapTypedValue((float)MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100) / 100f, PropertyType.Single) },
+ });
}
}
- private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
+ private BitmapPropertySet GetEncoderPropertySet(Guid encoderGuid)
{
- if (originalMetadata == null)
+ if (encoderGuid == BitmapEncoder.JpegEncoderId)
{
- return null;
+ return new BitmapPropertySet
+ {
+ { "ImageQuality", new BitmapTypedValue((float)MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100) / 100f, PropertyType.Single) },
+ };
}
- var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
- if (EnsureFrameIsValid(frameWithOriginalMetadata))
+ if (encoderGuid == BitmapEncoder.TiffEncoderId)
{
- return originalMetadata;
- }
-
- var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
- var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
- if (EnsureFrameIsValid(frameWithRecreatedMetadata))
- {
- return recreatedMetadata;
+ var compressionMethod = MapTiffCompression(_settings.TiffCompressOption);
+ if (compressionMethod.HasValue)
+ {
+ return new BitmapPropertySet
+ {
+ { "TiffCompressionMethod", new BitmapTypedValue(compressionMethod.Value, PropertyType.UInt8) },
+ };
+ }
}
return null;
-
- bool EnsureFrameIsValid(BitmapFrame frameToBeChecked)
- {
- try
- {
- var encoder = CreateEncoder(containerFormat);
- encoder.Frames.Add(frameToBeChecked);
- using (var testStream = new MemoryStream())
- {
- encoder.Save(testStream);
- }
-
- return true;
- }
- catch (Exception)
- {
- return false;
- }
- }
}
- private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
+ private static byte? MapTiffCompression(TiffCompressOption option)
{
- try
+ return option switch
{
- var metadata = new BitmapMetadata(originalMetadata.Format);
- var listOfMetadata = originalMetadata.GetListOfMetadata();
- foreach (var (metadataPath, value) in listOfMetadata)
- {
- if (value is BitmapMetadata bitmapMetadata)
- {
- var innerMetadata = new BitmapMetadata(bitmapMetadata.Format);
- metadata.SetQuerySafe(metadataPath, innerMetadata);
- }
- else
- {
- metadata.SetQuerySafe(metadataPath, value);
- }
- }
-
- return metadata;
- }
- catch (ArgumentException ex)
- {
- Debug.WriteLine(ex);
-
- return null;
- }
+ TiffCompressOption.None => 1,
+ TiffCompressOption.Ccitt3 => 2,
+ TiffCompressOption.Ccitt4 => 3,
+ TiffCompressOption.Lzw => 4,
+ TiffCompressOption.Rle => 5,
+ TiffCompressOption.Zip => 6,
+ _ => null, // Default: let the encoder decide
+ };
}
- private static BitmapFrame CreateBitmapFrame(BitmapSource transformedBitmap, BitmapMetadata metadata)
- {
- return BitmapFrame.Create(
- transformedBitmap,
- thumbnail: null,
- metadata,
- colorContexts: null);
- }
-
- private string GetDestinationPath(BitmapEncoder encoder)
+ private string GetDestinationPath(Guid encoderGuid, int outputPixelWidth, int outputPixelHeight)
{
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
var originalFileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
- var supportedExtensions = encoder.CodecInfo.FileExtensions.Split(',');
+ var supportedExtensions = CodecHelper.GetSupportedExtensions(encoderGuid);
var extension = _fileSystem.Path.GetExtension(_file);
if (!supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
- extension = supportedExtensions.FirstOrDefault();
+ extension = CodecHelper.GetDefaultExtension(encoderGuid);
}
string sizeName = _settings.SelectedSize is AiSize aiSize
@@ -389,8 +421,8 @@ namespace ImageResizer.Models
.Replace('\\', '_')
.Replace('/', '_');
- var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
- var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
+ var selectedWidth = _settings.SelectedSize is AiSize ? outputPixelWidth : _settings.SelectedSize.Width;
+ var selectedHeight = _settings.SelectedSize is AiSize ? outputPixelHeight : _settings.SelectedSize.Height;
var fileName = string.Format(
CultureInfo.CurrentCulture,
_settings.FileNameFormat,
@@ -398,8 +430,8 @@ namespace ImageResizer.Models
sizeNameSanitized,
selectedWidth,
selectedHeight,
- encoder.Frames[0].PixelWidth,
- encoder.Frames[0].PixelHeight);
+ outputPixelWidth,
+ outputPixelHeight);
fileName = fileName
.Replace(':', '_')
diff --git a/src/modules/imageresizer/ui/Properties/Settings.cs b/src/modules/imageresizer/ui/Properties/Settings.cs
index 529005ad1e..696468320d 100644
--- a/src/modules/imageresizer/ui/Properties/Settings.cs
+++ b/src/modules/imageresizer/ui/Properties/Settings.cs
@@ -17,7 +17,6 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
-using System.Windows.Media.Imaging;
using ImageResizer.Helpers;
using ImageResizer.Models;
using ManagedCommon;
@@ -89,8 +88,8 @@ namespace ImageResizer.Properties
IgnoreOrientation = true;
RemoveMetadata = false;
JpegQualityLevel = 90;
- PngInterlaceOption = System.Windows.Media.Imaging.PngInterlaceOption.Default;
- TiffCompressOption = System.Windows.Media.Imaging.TiffCompressOption.Default;
+ PngInterlaceOption = Models.PngInterlaceOption.Default;
+ TiffCompressOption = Models.TiffCompressOption.Default;
FileName = "%1 (%2)";
Sizes = new ObservableCollection
{
diff --git a/src/modules/imageresizer/ui/Services/IAISuperResolutionService.cs b/src/modules/imageresizer/ui/Services/IAISuperResolutionService.cs
index 3db073c5e5..a85f3fcc97 100644
--- a/src/modules/imageresizer/ui/Services/IAISuperResolutionService.cs
+++ b/src/modules/imageresizer/ui/Services/IAISuperResolutionService.cs
@@ -3,12 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.Windows.Media.Imaging;
+using Windows.Graphics.Imaging;
namespace ImageResizer.Services
{
public interface IAISuperResolutionService : IDisposable
{
- BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
+ SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
}
}
diff --git a/src/modules/imageresizer/ui/Services/NoOpAiSuperResolutionService.cs b/src/modules/imageresizer/ui/Services/NoOpAiSuperResolutionService.cs
index e59b5033ac..56c75c1b09 100644
--- a/src/modules/imageresizer/ui/Services/NoOpAiSuperResolutionService.cs
+++ b/src/modules/imageresizer/ui/Services/NoOpAiSuperResolutionService.cs
@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System.Windows.Media.Imaging;
+using Windows.Graphics.Imaging;
namespace ImageResizer.Services
{
@@ -14,7 +14,7 @@ namespace ImageResizer.Services
{
}
- public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
+ public SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath)
{
return source;
}
diff --git a/src/modules/imageresizer/ui/Services/WinAiSuperResolutionService.cs b/src/modules/imageresizer/ui/Services/WinAiSuperResolutionService.cs
index 4cd752184a..0f2ea80a5b 100644
--- a/src/modules/imageresizer/ui/Services/WinAiSuperResolutionService.cs
+++ b/src/modules/imageresizer/ui/Services/WinAiSuperResolutionService.cs
@@ -3,13 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.Runtime.InteropServices;
-using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
using Microsoft.Windows.AI;
using Microsoft.Windows.AI.Imaging;
using Windows.Graphics.Imaging;
@@ -91,7 +86,7 @@ namespace ImageResizer.Services
}
}
- public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
+ public SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath)
{
if (source == null || _disposed)
{
@@ -102,19 +97,12 @@ namespace ImageResizer.Services
// Currently not used by the ImageScaler API
try
{
- // Convert WPF BitmapSource to WinRT SoftwareBitmap
- var softwareBitmap = ConvertBitmapSourceToSoftwareBitmap(source);
- if (softwareBitmap == null)
- {
- return source;
- }
-
// Calculate target dimensions
- var newWidth = softwareBitmap.PixelWidth * scale;
- var newHeight = softwareBitmap.PixelHeight * scale;
+ var newWidth = source.PixelWidth * scale;
+ var newHeight = source.PixelHeight * scale;
// Apply super resolution with thread-safe access
- // _usageLock protects concurrent access from Parallel.ForEach threads
+ // _usageLock protects concurrent access from Parallel.ForEachAsync threads
SoftwareBitmap scaledBitmap;
lock (_usageLock)
{
@@ -123,16 +111,10 @@ namespace ImageResizer.Services
return source;
}
- scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
+ scaledBitmap = _imageScaler.ScaleSoftwareBitmap(source, newWidth, newHeight);
}
- if (scaledBitmap == null)
- {
- return source;
- }
-
- // Convert back to WPF BitmapSource
- return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
+ return scaledBitmap ?? source;
}
catch (Exception)
{
@@ -141,102 +123,6 @@ namespace ImageResizer.Services
}
}
- private static SoftwareBitmap ConvertBitmapSourceToSoftwareBitmap(BitmapSource bitmapSource)
- {
- try
- {
- // Ensure the bitmap is in a compatible format
- var convertedBitmap = new FormatConvertedBitmap();
- convertedBitmap.BeginInit();
- convertedBitmap.Source = bitmapSource;
- convertedBitmap.DestinationFormat = PixelFormats.Bgra32;
- convertedBitmap.EndInit();
-
- int width = convertedBitmap.PixelWidth;
- int height = convertedBitmap.PixelHeight;
- int stride = width * 4; // 4 bytes per pixel for Bgra32
- byte[] pixels = new byte[height * stride];
-
- convertedBitmap.CopyPixels(pixels, stride, 0);
-
- // Create SoftwareBitmap from pixel data
- var softwareBitmap = new SoftwareBitmap(
- BitmapPixelFormat.Bgra8,
- width,
- height,
- BitmapAlphaMode.Premultiplied);
-
- using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
- using (var reference = buffer.CreateReference())
- {
- unsafe
- {
- ((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
- System.Runtime.InteropServices.Marshal.Copy(pixels, 0, (IntPtr)dataInBytes, pixels.Length);
- }
- }
-
- return softwareBitmap;
- }
- catch (Exception)
- {
- return null;
- }
- }
-
- private static BitmapSource ConvertSoftwareBitmapToBitmapSource(SoftwareBitmap softwareBitmap)
- {
- try
- {
- // Convert to Bgra8 format if needed
- var convertedBitmap = SoftwareBitmap.Convert(
- softwareBitmap,
- BitmapPixelFormat.Bgra8,
- BitmapAlphaMode.Premultiplied);
-
- int width = convertedBitmap.PixelWidth;
- int height = convertedBitmap.PixelHeight;
- int stride = width * 4; // 4 bytes per pixel for Bgra8
- byte[] pixels = new byte[height * stride];
-
- using (var buffer = convertedBitmap.LockBuffer(BitmapBufferAccessMode.Read))
- using (var reference = buffer.CreateReference())
- {
- unsafe
- {
- ((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
- System.Runtime.InteropServices.Marshal.Copy((IntPtr)dataInBytes, pixels, 0, pixels.Length);
- }
- }
-
- // Create WPF BitmapSource from pixel data
- var wpfBitmap = BitmapSource.Create(
- width,
- height,
- 96, // DPI X
- 96, // DPI Y
- PixelFormats.Bgra32,
- null,
- pixels,
- stride);
-
- wpfBitmap.Freeze(); // Make it thread-safe
- return wpfBitmap;
- }
- catch (Exception)
- {
- return null;
- }
- }
-
- [ComImport]
- [Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IMemoryBufferByteAccess
- {
- unsafe void GetBuffer(out byte* buffer, out uint capacity);
- }
-
public void Dispose()
{
if (_disposed)
diff --git a/src/modules/imageresizer/ui/Utilities/CodecHelper.cs b/src/modules/imageresizer/ui/Utilities/CodecHelper.cs
new file mode 100644
index 0000000000..c469a5af14
--- /dev/null
+++ b/src/modules/imageresizer/ui/Utilities/CodecHelper.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Windows.Graphics.Imaging;
+
+namespace ImageResizer.Utilities
+{
+ ///
+ /// Maps between legacy container format GUIDs (used in settings JSON) and WinRT encoder/decoder IDs,
+ /// and provides file extension lookups.
+ ///
+ internal static class CodecHelper
+ {
+ // Legacy container format GUID (stored in settings JSON) -> WinRT Encoder ID
+ private static readonly Dictionary 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,
+ };
+
+ // WinRT Decoder ID -> WinRT Encoder ID
+ private static readonly Dictionary DecoderIdToEncoderId = new()
+ {
+ [BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
+ [BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
+ [BitmapDecoder.BmpDecoderId] = BitmapEncoder.BmpEncoderId,
+ [BitmapDecoder.TiffDecoderId] = BitmapEncoder.TiffEncoderId,
+ [BitmapDecoder.GifDecoderId] = BitmapEncoder.GifEncoderId,
+ [BitmapDecoder.JpegXRDecoderId] = BitmapEncoder.JpegXREncoderId,
+ };
+
+ // Encoder ID -> supported file extensions
+ private static readonly Dictionary EncoderExtensions = new()
+ {
+ [BitmapEncoder.JpegEncoderId] = new[] { ".jpg", ".jpeg", ".jpe", ".jfif" },
+ [BitmapEncoder.PngEncoderId] = new[] { ".png" },
+ [BitmapEncoder.BmpEncoderId] = new[] { ".bmp", ".dib", ".rle" },
+ [BitmapEncoder.TiffEncoderId] = new[] { ".tiff", ".tif" },
+ [BitmapEncoder.GifEncoderId] = new[] { ".gif" },
+ [BitmapEncoder.JpegXREncoderId] = new[] { ".jxr", ".wdp" },
+ };
+
+ ///
+ /// Gets the WinRT encoder ID that corresponds to the given legacy container format GUID.
+ /// Falls back to PNG if the GUID is not recognized.
+ ///
+ public static Guid GetEncoderIdFromLegacyGuid(Guid containerFormatGuid)
+ => LegacyGuidToEncoderId.TryGetValue(containerFormatGuid, out var id)
+ ? id
+ : BitmapEncoder.PngEncoderId;
+
+ ///
+ /// Gets the WinRT encoder ID that matches the given decoder's codec.
+ /// Returns null if no matching encoder exists (e.g., ICO decoder has no encoder).
+ ///
+ public static Guid? GetEncoderIdForDecoder(BitmapDecoder decoder)
+ {
+ var codecId = decoder.DecoderInformation?.CodecId ?? Guid.Empty;
+ return DecoderIdToEncoderId.TryGetValue(codecId, out var encoderId)
+ ? encoderId
+ : null;
+ }
+
+ ///
+ /// Returns the supported file extensions for the given encoder ID.
+ ///
+ public static string[] GetSupportedExtensions(Guid encoderId)
+ => EncoderExtensions.TryGetValue(encoderId, out var extensions)
+ ? extensions
+ : Array.Empty();
+
+ ///
+ /// Returns the default (first) file extension for the given encoder ID.
+ ///
+ public static string GetDefaultExtension(Guid encoderId)
+ => GetSupportedExtensions(encoderId).FirstOrDefault() ?? ".png";
+
+ ///
+ /// Checks whether the given encoder ID is a known, supported encoder.
+ ///
+ public static bool CanEncode(Guid encoderId)
+ => EncoderExtensions.ContainsKey(encoderId);
+ }
+}
diff --git a/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs b/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs
index 0b86060081..7bd7d9b7c9 100644
--- a/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs
+++ b/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs
@@ -9,7 +9,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using System.Windows.Media.Imaging;
+using Windows.Graphics.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Common.UI;
@@ -244,7 +244,7 @@ namespace ImageResizer.ViewModels
}
}
- private void UpdateAiDetails()
+ private async void UpdateAiDetails()
{
if (Settings == null || Settings.SelectedSize is not AiSize)
{
@@ -262,7 +262,7 @@ namespace ImageResizer.ViewModels
return;
}
- EnsureOriginalDimensionsLoaded();
+ await EnsureOriginalDimensionsLoadedAsync();
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
CurrentResolutionDescription = hasConcreteSize
@@ -280,7 +280,7 @@ namespace ImageResizer.ViewModels
return string.Format(CultureInfo.CurrentCulture, "{0} x {1}", width, height);
}
- private void EnsureOriginalDimensionsLoaded()
+ private async Task EnsureOriginalDimensionsLoadedAsync()
{
if (_originalDimensionsLoaded)
{
@@ -296,14 +296,11 @@ namespace ImageResizer.ViewModels
try
{
- using var stream = File.OpenRead(file);
- var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
- var frame = decoder.Frames.FirstOrDefault();
- if (frame != null)
- {
- _originalWidth = frame.PixelWidth;
- _originalHeight = frame.PixelHeight;
- }
+ using var fileStream = File.OpenRead(file);
+ using var stream = fileStream.AsRandomAccessStream();
+ var decoder = await BitmapDecoder.CreateAsync(stream);
+ _originalWidth = (int)decoder.PixelWidth;
+ _originalHeight = (int)decoder.PixelHeight;
}
catch (Exception)
{
diff --git a/src/modules/imageresizer/ui/ViewModels/ProgressViewModel.cs b/src/modules/imageresizer/ui/ViewModels/ProgressViewModel.cs
index 2d46cf6884..e8b4965994 100644
--- a/src/modules/imageresizer/ui/ViewModels/ProgressViewModel.cs
+++ b/src/modules/imageresizer/ui/ViewModels/ProgressViewModel.cs
@@ -47,13 +47,13 @@ namespace ImageResizer.ViewModels
[RelayCommand]
public void Start()
{
- _ = Task.Factory.StartNew(StartExecutingWork, _cancellationTokenSource.Token, TaskCreationOptions.None, TaskScheduler.Current);
+ _ = Task.Run(() => StartExecutingWorkAsync());
}
- private void StartExecutingWork()
+ private async Task StartExecutingWorkAsync()
{
_stopwatch.Restart();
- var errors = _batch.Process(
+ var errors = await _batch.ProcessAsync(
(completed, total) =>
{
var progress = completed / total;