diff --git a/src/winapp-CLI/Directory.Packages.props b/src/winapp-CLI/Directory.Packages.props index 9949ec17..e12da34b 100644 --- a/src/winapp-CLI/Directory.Packages.props +++ b/src/winapp-CLI/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs index 52a286e3..f6c3868b 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs @@ -255,7 +255,78 @@ public async Task ManifestUpdateAssetsCommandShouldInferManifestFromCurrentDirec Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully when manifest is inferred"); // Verify Assets directory was created - var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created"); - } -} + var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); + Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created"); + } + + [TestMethod] + public async Task ManifestUpdateAssetsCommandShouldGenerateAssetsFromSvg() + { + // Arrange + var svgImagePath = Path.Combine(_tempDirectory.FullName, "testlogo.svg"); + PngHelper.CreateTestSvgImage(svgImagePath); + + var updateAssetsCommand = GetRequiredService(); + var args = new[] + { + svgImagePath, + "--manifest", _testManifestPath + }; + + // Act + var parseResult = updateAssetsCommand.Parse(args); + var exitCode = await parseResult.InvokeAsync(); + + // Assert + Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully with SVG source"); + + // Verify Assets directory was created + var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); + Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created"); + + // Verify assets referenced in manifest were generated + var expectedAssets = new[] + { + "Square44x44Logo.png", + "Square150x150Logo.png", + "Wide310x150Logo.png", + "StoreLogo.png" + }; + + foreach (var asset in expectedAssets) + { + var assetPath = Path.Combine(assetsDir, asset); + Assert.IsTrue(File.Exists(assetPath), $"Asset {asset} should be generated from SVG source"); + } + } + + [TestMethod] + public async Task ManifestUpdateAssetsCommandShouldGenerateCorrectSizesFromSvg() + { + // Arrange + var svgImagePath = Path.Combine(_tempDirectory.FullName, "testlogo.svg"); + PngHelper.CreateTestSvgImage(svgImagePath); + + var updateAssetsCommand = GetRequiredService(); + var args = new[] + { + svgImagePath, + "--manifest", _testManifestPath + }; + + // Act + var parseResult = updateAssetsCommand.Parse(args); + var exitCode = await parseResult.InvokeAsync(); + + // Assert + Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully with SVG source"); + + var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); + + // Check that scale-200 assets exist (which should be 2x the base size) + Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "Square44x44Logo.scale-200.png")), + "Square44x44Logo.scale-200.png should exist when generated from SVG"); + Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "Square150x150Logo.scale-200.png")), + "Square150x150Logo.scale-200.png should exist when generated from SVG"); + } + } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs b/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs index e2edbd2b..8b2f8742 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs @@ -25,6 +25,16 @@ internal static void CreateTestImage(string path) File.WriteAllBytes(path, pngData); } + internal static void CreateTestSvgImage(string path) + { + var svgContent = """ + + + + """; + File.WriteAllText(path, svgContent); + } + /// /// Verifies that all pixels in the image are fully transparent (alpha = 0). /// diff --git a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs index ca23ba26..3cd9e15f 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation and Contributors. All rights reserved. // Licensed under the MIT License. +using SkiaSharp; +using Svg.Skia; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; @@ -47,15 +49,7 @@ public async Task GenerateAssetsAsync(FileInfo sourceImagePath, DirectoryInfo ou Bitmap sourceImage; try { - if (sourceImagePath.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) - { - using var icon = new Icon(sourceImagePath.FullName); - sourceImage = icon.ToBitmap(); - } - else - { - sourceImage = new Bitmap(sourceImagePath.FullName); - } + sourceImage = LoadSourceImage(sourceImagePath); } catch (Exception ex) { @@ -123,15 +117,7 @@ public async Task GenerateAssetsFromManifestAsync( Bitmap sourceImage; try { - if (sourceImagePath.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) - { - using var icon = new Icon(sourceImagePath.FullName); - sourceImage = icon.ToBitmap(); - } - else - { - sourceImage = new Bitmap(sourceImagePath.FullName); - } + sourceImage = LoadSourceImage(sourceImagePath); } catch (Exception ex) { @@ -214,6 +200,62 @@ public async Task GenerateAssetsFromManifestAsync( } } + private static Bitmap LoadSourceImage(FileInfo sourceImagePath) + { + if (sourceImagePath.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) + { + using var icon = new Icon(sourceImagePath.FullName); + return icon.ToBitmap(); + } + + if (sourceImagePath.Extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) + { + var svg = new SKSvg(); + using var stream = File.OpenRead(sourceImagePath.FullName); + svg.Load(stream); + + var picture = svg.Picture ?? throw new InvalidOperationException($"Failed to render SVG image: {sourceImagePath.FullName}. The file may be corrupted or contain unsupported SVG features."); + var bounds = picture.CullRect; + + int width = (int)Math.Ceiling(bounds.Width); + int height = (int)Math.Ceiling(bounds.Height); + + // Ensure SVG renders at a reasonable minimum size for quality when scaling down + const float minRenderDimension = 1024f; + float scaleFactor = 1f; + if (width > 0 && height > 0 && (width < minRenderDimension || height < minRenderDimension)) + { + scaleFactor = Math.Max(minRenderDimension / width, minRenderDimension / height); + width = (int)Math.Ceiling(bounds.Width * scaleFactor); + height = (int)Math.Ceiling(bounds.Height * scaleFactor); + } + + // Render to SKBitmap + using var skBitmap = new SKBitmap(width, height); + using (var canvas = new SKCanvas(skBitmap)) + { + canvas.Clear(SKColors.Transparent); + if (scaleFactor > 1f) + { + canvas.Scale(scaleFactor); + } + canvas.DrawPicture(picture); + canvas.Flush(); + } + + // Convert SKBitmap → System.Drawing.Bitmap + using var image = SKImage.FromBitmap(skBitmap); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + using var ms = new MemoryStream(data.ToArray()); + + var bitmap = new Bitmap(ms); + + return bitmap; + } + + return new Bitmap(sourceImagePath.FullName); + } + private static async Task GenerateAssetAsync(Bitmap sourceImage, string outputPath, int targetWidth, int targetHeight, CancellationToken cancellationToken) { await Task.Run(() => diff --git a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj index bc98d6d3..8ecb1e98 100644 --- a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj +++ b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj @@ -1,4 +1,4 @@ - + Exe @@ -26,7 +26,7 @@ full true true - + false false @@ -54,6 +54,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive +