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;