From 34a8e07c7eb9a9ba629922509a9e329909cf7c8a Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 26 Feb 2026 10:57:49 -0800 Subject: [PATCH 1/2] Add SVG support for asset generation --- src/winapp-CLI/Directory.Packages.props | 1 + .../ManifestUpdateAssetsCommandTests.cs | 79 ++++++++++++++++++- src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs | 10 +++ .../WinApp.Cli/Services/ImageAssetService.cs | 57 ++++++++----- src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj | 1 + 5 files changed, 126 insertions(+), 22 deletions(-) diff --git a/src/winapp-CLI/Directory.Packages.props b/src/winapp-CLI/Directory.Packages.props index 9949ec17..c6bf2e3f 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..7bd20e18 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; +using Svg; using WinApp.Cli.ConsoleTasks; using WinApp.Cli.Helpers; @@ -47,15 +48,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 +116,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 +199,42 @@ 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 svgDoc = SvgDocument.Open(sourceImagePath.FullName); + + // Ensure SVG renders at a reasonable minimum size for quality when scaling down + const float minRenderDimension = 1024f; + var currentWidth = svgDoc.Width.Value; + var currentHeight = svgDoc.Height.Value; + + if (currentWidth > 0 && currentHeight > 0 && (currentWidth < minRenderDimension || currentHeight < minRenderDimension)) + { + var scaleFactor = Math.Max(minRenderDimension / currentWidth, minRenderDimension / currentHeight); + svgDoc.Width = new SvgUnit(currentWidth * scaleFactor); + svgDoc.Height = new SvgUnit(currentHeight * scaleFactor); + } + + var bitmap = svgDoc.Draw(); + if (bitmap == null || bitmap.Width == 0 || bitmap.Height == 0) + { + throw new InvalidOperationException("SVG rendered to an empty bitmap."); + } + + 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..c517978b 100644 --- a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj +++ b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj @@ -54,6 +54,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + From f65171e669fd4d77b23aa7d39d0bf21deca8cc70 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 26 Feb 2026 14:47:12 -0800 Subject: [PATCH 2/2] Switch to Svg.Skia for SVG rendering. --- src/winapp-CLI/Directory.Packages.props | 2 +- .../WinApp.Cli/Services/ImageAssetService.cs | 45 ++++++++++++++----- src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj | 6 +-- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/winapp-CLI/Directory.Packages.props b/src/winapp-CLI/Directory.Packages.props index c6bf2e3f..e12da34b 100644 --- a/src/winapp-CLI/Directory.Packages.props +++ b/src/winapp-CLI/Directory.Packages.props @@ -8,7 +8,7 @@ - + diff --git a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs index 7bd20e18..3cd9e15f 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs @@ -1,10 +1,11 @@ // 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; -using Svg; using WinApp.Cli.ConsoleTasks; using WinApp.Cli.Helpers; @@ -209,26 +210,46 @@ private static Bitmap LoadSourceImage(FileInfo sourceImagePath) if (sourceImagePath.Extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - var svgDoc = SvgDocument.Open(sourceImagePath.FullName); + 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; - var currentWidth = svgDoc.Width.Value; - var currentHeight = svgDoc.Height.Value; - - if (currentWidth > 0 && currentHeight > 0 && (currentWidth < minRenderDimension || currentHeight < minRenderDimension)) + float scaleFactor = 1f; + if (width > 0 && height > 0 && (width < minRenderDimension || height < minRenderDimension)) { - var scaleFactor = Math.Max(minRenderDimension / currentWidth, minRenderDimension / currentHeight); - svgDoc.Width = new SvgUnit(currentWidth * scaleFactor); - svgDoc.Height = new SvgUnit(currentHeight * scaleFactor); + scaleFactor = Math.Max(minRenderDimension / width, minRenderDimension / height); + width = (int)Math.Ceiling(bounds.Width * scaleFactor); + height = (int)Math.Ceiling(bounds.Height * scaleFactor); } - var bitmap = svgDoc.Draw(); - if (bitmap == null || bitmap.Width == 0 || bitmap.Height == 0) + // Render to SKBitmap + using var skBitmap = new SKBitmap(width, height); + using (var canvas = new SKCanvas(skBitmap)) { - throw new InvalidOperationException("SVG rendered to an empty bitmap."); + 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; } diff --git a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj index c517978b..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,7 +54,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - +