From c77b40f8606e33b3b679df255db89c519aaf765d Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Tue, 2 Jun 2026 20:00:21 +0200 Subject: [PATCH 01/11] Add themed splash screen support Adds light/dark splash screen metadata support for Android and Apple targets, including Android night-qualified resources and iOS/Mac Catalyst asset catalog launch screens. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CreatePartialInfoPlistTask.cs | 25 +- src/SingleProject/Resizetizer/src/DpiPath.cs | 26 ++ .../src/GenerateSplashAndroidResources.cs | 88 ++++-- .../src/GenerateSplashAssetCatalog.cs | 261 ++++++++++++++++++ .../src/GenerateSplashStoryboard.cs | 9 + .../Resizetizer/src/ResizeImageInfo.cs | 52 ++++ .../Microsoft.Maui.Resizetizer.After.targets | 79 +++++- .../GenerateSplashAndroidResourcesTests.cs | 133 ++++++++- .../GenerateSplashAssetCatalogTests.cs | 162 +++++++++++ .../GenerateSplashStoryboardTests.cs | 20 ++ .../test/UnitTests/ResizeImageInfoTests.cs | 56 ++++ 11 files changed, 882 insertions(+), 29 deletions(-) create mode 100644 src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs create mode 100644 src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs diff --git a/src/SingleProject/Resizetizer/src/CreatePartialInfoPlistTask.cs b/src/SingleProject/Resizetizer/src/CreatePartialInfoPlistTask.cs index 956630fea7e3..e50bfb48aa3c 100644 --- a/src/SingleProject/Resizetizer/src/CreatePartialInfoPlistTask.cs +++ b/src/SingleProject/Resizetizer/src/CreatePartialInfoPlistTask.cs @@ -16,6 +16,10 @@ public class CreatePartialInfoPlistTask : Task public string Storyboard { get; set; } + public string LaunchScreenImage { get; set; } + + public string LaunchScreenColor { get; set; } + const string plistHeader = @" @@ -52,7 +56,26 @@ public override bool Execute() f.WriteLine(" "); } - if (!string.IsNullOrEmpty(Storyboard)) + if (!string.IsNullOrEmpty(LaunchScreenImage) || !string.IsNullOrEmpty(LaunchScreenColor)) + { + f.WriteLine(" UILaunchScreen"); + f.WriteLine(" "); + + if (!string.IsNullOrEmpty(LaunchScreenImage)) + { + f.WriteLine(" UIImageName"); + f.WriteLine($" {LaunchScreenImage}"); + } + + if (!string.IsNullOrEmpty(LaunchScreenColor)) + { + f.WriteLine(" UIColorName"); + f.WriteLine($" {LaunchScreenColor}"); + } + + f.WriteLine(" "); + } + else if (!string.IsNullOrEmpty(Storyboard)) { f.WriteLine(" UILaunchStoryboardName"); f.WriteLine($" {Path.GetFileNameWithoutExtension(Storyboard)}"); diff --git a/src/SingleProject/Resizetizer/src/DpiPath.cs b/src/SingleProject/Resizetizer/src/DpiPath.cs index 69af600f1a81..318401b59c91 100644 --- a/src/SingleProject/Resizetizer/src/DpiPath.cs +++ b/src/SingleProject/Resizetizer/src/DpiPath.cs @@ -48,6 +48,9 @@ public static class Android public static DpiPath Original => new DpiPath("drawable", 1.0m); + public static DpiPath OriginalNight => + new DpiPath("drawable-night", 1.0m); + public static DpiPath[] Image => new[] { @@ -58,6 +61,16 @@ public static DpiPath[] Image new DpiPath("drawable-xxxhdpi", 4.0m), }; + public static DpiPath[] ImageNight + => new[] + { + new DpiPath("drawable-night-mdpi", 1.0m), + new DpiPath("drawable-night-hdpi", 1.5m), + new DpiPath("drawable-night-xhdpi", 2.0m), + new DpiPath("drawable-night-xxhdpi", 3.0m), + new DpiPath("drawable-night-xxxhdpi", 4.0m), + }; + public static DpiPath[] AppIcon => new[] { @@ -89,6 +102,11 @@ public static DpiPath[] AppIconParts public static class Ios { public const string AppIconPath = "Assets.xcassets/{name}.appiconset"; + public const string SplashImageName = "MauiSplashImage"; + public const string SplashImageDarkName = "MauiSplashImageDark"; + public const string SplashColorName = "MauiSplashColor"; + public const string SplashImageSetPath = "Assets.xcassets/MauiSplashImage.imageset"; + public const string SplashColorSetPath = "Assets.xcassets/MauiSplashColor.colorset"; public static DpiPath Original => new DpiPath("Resources", 1.0m); @@ -101,6 +119,14 @@ public static DpiPath[] Image new DpiPath("", 3.0m, null,"@3x"), }; + public static DpiPath[] SplashImageAsset + => new[] + { + new DpiPath(SplashImageSetPath, 1.0m), + new DpiPath(SplashImageSetPath, 2.0m, null, "@2x"), + new DpiPath(SplashImageSetPath, 3.0m, null, "@3x"), + }; + public static DpiPath[] AppIcon => new[] { diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs index 0e0fd63fd880..8418af4e1a6a 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs @@ -43,11 +43,30 @@ public override bool Execute() var info = ResizeImageInfo.Parse(splash); var resizer = new Resizer(info, IntermediateOutputPath, this); + Resizer darkResizer = null; + if (info.HasDarkMode) + { + var darkInfo = info.CreateDarkVariant(info.OutputName + info.OutputExtension); + if (darkInfo.BaseSize is null && !string.IsNullOrWhiteSpace(info.DarkFilename)) + darkInfo.BaseSize = resizer.BaseSize ?? resizer.GetOriginalSize(); + darkResizer = new Resizer(darkInfo, IntermediateOutputPath, this); + } WriteImages(resizer); - WriteColors(resizer); - WriteDrawable(resizer); - WriteDrawable_v31(resizer); + if (darkResizer is not null) + WriteImages(darkResizer, dark: true); + else + CleanNightResources(); + + WriteColors(resizer, darkResizer); + WriteDrawable(resizer, dark: false); + WriteDrawable_v31(resizer, dark: false); + + if (darkResizer is not null) + { + WriteDrawable(darkResizer, dark: true); + WriteDrawable_v31(darkResizer, dark: true); + } return !Log.HasLoggedErrors; } @@ -63,11 +82,11 @@ public override bool Execute() const string Comment = "This file was auto-generated by .NET MAUI."; const float PreferredImageSize = 108f; - private void WriteImages(Resizer resizer) + private void WriteImages(Resizer resizer, bool dark = false) { if (resizer.Info.Resize) { - foreach (var dpi in DpiPath.Android.Image) + foreach (var dpi in dark ? DpiPath.Android.ImageNight : DpiPath.Android.Image) { Log.LogMessage(MessageImportance.Low, $"Splash Screen Resize: " + dpi); resizer.Resize(dpi, InputsFile); @@ -75,16 +94,26 @@ private void WriteImages(Resizer resizer) } else { - var dpi = DpiPath.Android.Original; + var dpi = dark ? DpiPath.Android.OriginalNight : DpiPath.Android.Original; Log.LogMessage(MessageImportance.Low, $"Splash Screen Copy: " + dpi); resizer.CopyFile(dpi, InputsFile); } } - void WriteColors(Resizer resizer) + void WriteColors(Resizer resizer, Resizer darkResizer) + { + WriteColors(resizer.Info.Color, "values"); + + if (darkResizer?.Info.Color is not null) + WriteColors(darkResizer.Info.Color, "values-night"); + else if (darkResizer is not null) + DeleteNightColors(); + } + + void WriteColors(SKColor? color, string directory) { - var dir = Path.Combine(IntermediateOutputPath, "values"); + var dir = Path.Combine(IntermediateOutputPath, directory); Directory.CreateDirectory(dir); var colorsFile = Path.Combine(dir, "maui_colors.xml"); @@ -95,25 +124,25 @@ void WriteColors(Resizer resizer) writer.WriteComment(Comment); writer.WriteStartElement("resources"); - if (resizer.Info.Color is not null) + if (color is not null) { writer.WriteStartElement("color"); writer.WriteAttributeString("name", "maui_splash_color"); - writer.WriteString(resizer.Info.Color.ToString()); + writer.WriteString(color.ToString()); writer.WriteEndElement(); } writer.WriteEndDocument(); } - void WriteDrawable(Resizer resizer) + void WriteDrawable(Resizer resizer, bool dark) { - var dir = Path.Combine(IntermediateOutputPath, "drawable"); + var dir = Path.Combine(IntermediateOutputPath, dark ? "drawable-night" : "drawable"); Directory.CreateDirectory(dir); var drawableFile = Path.Combine(dir, "maui_splash_image.xml"); - Log.LogMessage(MessageImportance.Low, $"Splash Screen Drawable: " + drawableFile); + Log.LogMessage(MessageImportance.Low, $"Splash Screen Drawable{(dark ? " (night)" : "")}: " + drawableFile); using var writer = XmlWriter.Create(drawableFile, Settings); writer.WriteComment(Comment); @@ -130,16 +159,16 @@ void WriteDrawable(Resizer resizer) writer.WriteEndDocument(); } - void WriteDrawable_v31(Resizer resizer) + void WriteDrawable_v31(Resizer resizer, bool dark) { var size = CalculateScaledSize(resizer); - var dir = Path.Combine(IntermediateOutputPath, "drawable-v31"); + var dir = Path.Combine(IntermediateOutputPath, dark ? "drawable-night-v31" : "drawable-v31"); Directory.CreateDirectory(dir); var drawableFile = Path.Combine(dir, "maui_splash_image.xml"); - Log.LogMessage(MessageImportance.Low, $"Splash Screen Drawable (v31): " + drawableFile); + Log.LogMessage(MessageImportance.Low, $"Splash Screen Drawable ({(dark ? "night " : "")}v31): " + drawableFile); using var writer = XmlWriter.Create(drawableFile, Settings); writer.WriteComment(Comment); @@ -164,6 +193,33 @@ void ILogger.Log(string message) Log?.LogMessage(message); } + void CleanNightResources() + { + foreach (var directory in new[] + { + "values-night", + "drawable-night", + "drawable-night-v31", + "drawable-night-mdpi", + "drawable-night-hdpi", + "drawable-night-xhdpi", + "drawable-night-xxhdpi", + "drawable-night-xxxhdpi", + }) + { + var path = Path.Combine(IntermediateOutputPath, directory); + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + } + + void DeleteNightColors() + { + var colorsFile = Path.Combine(IntermediateOutputPath, "values-night", "maui_colors.xml"); + if (File.Exists(colorsFile)) + File.Delete(colorsFile); + } + static SKSize CalculateScaledSize(Resizer resizer) { var size = resizer.BaseSize ?? resizer.GetOriginalSize(); diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs new file mode 100644 index 000000000000..2ed40b1db8f3 --- /dev/null +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -0,0 +1,261 @@ +#nullable enable +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using SkiaSharp; + +namespace Microsoft.Maui.Resizetizer +{ + /// + /// Generates iOS asset catalog resources for themed launch screens. + /// + public class GenerateSplashAssetCatalog : Task, ILogger + { + [Required] + public string IntermediateOutputPath { get; set; } = null!; + + public ITaskItem[]? MauiSplashScreen { get; set; } + + public string? InputsFile { get; set; } + + public override bool Execute() + { + Log.LogMessage(MessageImportance.Low, $"Splash Screen Asset Catalog: Intermediate Path " + IntermediateOutputPath); + + try + { + var splash = MauiSplashScreen?.FirstOrDefault(); + if (splash is null) + return true; + + CleanStoryboard(); + + var info = ResizeImageInfo.Parse(splash); + var lightInfo = CloneForAsset(info, DpiPath.Ios.SplashImageName); + var lightResizer = new Resizer(lightInfo, IntermediateOutputPath, this); + var darkInfo = CloneForAsset(info.CreateDarkVariant(), DpiPath.Ios.SplashImageDarkName); + if (darkInfo.BaseSize is null && !string.IsNullOrWhiteSpace(info.DarkFilename)) + darkInfo.BaseSize = lightResizer.BaseSize ?? lightResizer.GetOriginalSize(); + + WriteImages(lightResizer); + WriteImages(new Resizer(darkInfo, IntermediateOutputPath, this)); + WriteImageSet(); + if (info.Color is not null || info.DarkColor is not null) + WriteColorSet(info.Color ?? SKColors.White, info.DarkColor ?? info.Color ?? SKColors.White); + + return !Log.HasLoggedErrors; + } + catch (Exception ex) + { + Log.LogError(Resources.ErrorMessages.AppleResourceProcessing, ErrorCodes.AppleResourceProcessingCode, null, null, 0, 0, 0, 0, string.Format(Resources.ErrorMessages.AppleResourceProcessingError, ex.ToString())); + return false; + } + } + + private static ResizeImageInfo CloneForAsset(ResizeImageInfo info, string alias) => + new ResizeImageInfo + { + ItemSpec = info.ItemSpec, + Alias = alias + Resizer.RasterFileExtension, + Filename = info.Filename, + BaseSize = info.BaseSize, + Resize = info.Resize, + TintColor = info.TintColor, + Color = info.Color, + }; + + private void WriteImages(Resizer resizer) + { + foreach (var dpi in DpiPath.Ios.SplashImageAsset) + { + Log.LogMessage(MessageImportance.Low, $"Splash Screen Asset Catalog Resize: " + dpi); + resizer.Resize(dpi, InputsFile); + } + } + + private void WriteImageSet() + { + var imageSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashImageSetPath); + Directory.CreateDirectory(imageSetPath); + + var contentsFile = Path.Combine(imageSetPath, "Contents.json"); + File.WriteAllText(contentsFile, + $$""" + { + "images": [ + { + "idiom": "universal", + "filename": "MauiSplashImage.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "MauiSplashImage@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "MauiSplashImage@3x.png", + "scale": "3x" + }, + { + "idiom": "universal", + "filename": "MauiSplashImage.png", + "scale": "1x", + "appearances": [ + { + "appearance": "luminosity", + "value": "light" + } + ] + }, + { + "idiom": "universal", + "filename": "MauiSplashImage@2x.png", + "scale": "2x", + "appearances": [ + { + "appearance": "luminosity", + "value": "light" + } + ] + }, + { + "idiom": "universal", + "filename": "MauiSplashImage@3x.png", + "scale": "3x", + "appearances": [ + { + "appearance": "luminosity", + "value": "light" + } + ] + }, + { + "idiom": "universal", + "filename": "MauiSplashImageDark.png", + "scale": "1x", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "idiom": "universal", + "filename": "MauiSplashImageDark@2x.png", + "scale": "2x", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "idiom": "universal", + "filename": "MauiSplashImageDark@3x.png", + "scale": "3x", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + } + ], + "info": { + "version": 1, + "author": "xcode" + } + } + """); + } + + private void WriteColorSet(SKColor lightColor, SKColor darkColor) + { + var colorSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashColorSetPath); + Directory.CreateDirectory(colorSetPath); + + var contentsFile = Path.Combine(colorSetPath, "Contents.json"); + File.WriteAllText(contentsFile, + $$""" + { + "colors": [ + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "{{ToComponent(lightColor.Red)}}", + "green": "{{ToComponent(lightColor.Green)}}", + "blue": "{{ToComponent(lightColor.Blue)}}", + "alpha": "{{ToComponent(lightColor.Alpha)}}" + } + } + }, + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "{{ToComponent(lightColor.Red)}}", + "green": "{{ToComponent(lightColor.Green)}}", + "blue": "{{ToComponent(lightColor.Blue)}}", + "alpha": "{{ToComponent(lightColor.Alpha)}}" + } + }, + "appearances": [ + { + "appearance": "luminosity", + "value": "light" + } + ] + }, + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "{{ToComponent(darkColor.Red)}}", + "green": "{{ToComponent(darkColor.Green)}}", + "blue": "{{ToComponent(darkColor.Blue)}}", + "alpha": "{{ToComponent(darkColor.Alpha)}}" + } + }, + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + } + ], + "info": { + "version": 1, + "author": "xcode" + } + } + """); + } + + private static string ToComponent(byte value) => + (value / (float)byte.MaxValue).ToString("0.#######", CultureInfo.InvariantCulture); + + private void CleanStoryboard() + { + var storyboard = Path.Combine(IntermediateOutputPath, "MauiSplash.storyboard"); + if (File.Exists(storyboard)) + File.Delete(storyboard); + } + + void ILogger.Log(string message) + { + Log?.LogMessage(message); + } + } +} diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashStoryboard.cs b/src/SingleProject/Resizetizer/src/GenerateSplashStoryboard.cs index 472495b83154..b9d676826de6 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashStoryboard.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashStoryboard.cs @@ -28,6 +28,8 @@ public override bool Execute() try { + CleanThemedAssets(); + var splash = MauiSplashScreen?.FirstOrDefault(); if (splash is null) { @@ -145,5 +147,12 @@ void ILogger.Log(string message) { Log?.LogMessage(message); } + + void CleanThemedAssets() + { + var assetsPath = Path.Combine(IntermediateOutputPath, "Assets.xcassets"); + if (Directory.Exists(assetsPath)) + Directory.Delete(assetsPath, recursive: true); + } } } diff --git a/src/SingleProject/Resizetizer/src/ResizeImageInfo.cs b/src/SingleProject/Resizetizer/src/ResizeImageInfo.cs index 021857a49401..321c58f93292 100644 --- a/src/SingleProject/Resizetizer/src/ResizeImageInfo.cs +++ b/src/SingleProject/Resizetizer/src/ResizeImageInfo.cs @@ -40,8 +40,21 @@ internal class ResizeImageInfo public SKColor? Color { get; set; } + public SKColor? DarkTintColor { get; set; } + + public SKColor? DarkColor { get; set; } + public bool IsVector => IsVectorFilename(Filename); + public string? DarkFilename { get; set; } + + public bool DarkIsVector => IsVectorFilename(DarkFilename); + + public bool HasDarkMode => + DarkColor is not null || + DarkTintColor is not null || + !string.IsNullOrWhiteSpace(DarkFilename); + public bool IsAppIcon { get; set; } public string? ForegroundFilename { get; set; } @@ -108,6 +121,16 @@ public static ResizeImageInfo Parse(ITaskItem image) if (info.Color is null && !string.IsNullOrEmpty(color)) throw new InvalidDataException($"Unable to parse color value '{color}' for '{info.Filename}'."); + var darkTintColor = image.GetMetadata("DarkTintColor"); + info.DarkTintColor = Utils.ParseColorString(darkTintColor); + if (info.DarkTintColor is null && !string.IsNullOrEmpty(darkTintColor)) + throw new InvalidDataException($"Unable to parse color value '{darkTintColor}' for '{info.Filename}'."); + + var darkColor = image.GetMetadata("DarkColor"); + info.DarkColor = Utils.ParseColorString(darkColor); + if (info.DarkColor is null && !string.IsNullOrEmpty(darkColor)) + throw new InvalidDataException($"Unable to parse color value '{darkColor}' for '{info.Filename}'."); + if (bool.TryParse(image.GetMetadata("IsAppIcon"), out var iai)) info.IsAppIcon = iai; @@ -124,6 +147,16 @@ public static ResizeImageInfo Parse(ITaskItem image) info.ForegroundFilename = fgFileInfo.FullName; } + var darkFile = image.GetMetadata("DarkFile"); + if (!string.IsNullOrEmpty(darkFile)) + { + var darkFileInfo = new FileInfo(darkFile); + if (!darkFileInfo.Exists) + throw new FileNotFoundException("Unable to find dark file: " + darkFileInfo.FullName, darkFileInfo.FullName); + + info.DarkFilename = darkFileInfo.FullName; + } + // make sure the image is a foreground if this is an icon if (info.IsAppIcon && string.IsNullOrEmpty(info.ForegroundFilename)) { @@ -136,5 +169,24 @@ public static ResizeImageInfo Parse(ITaskItem image) return info; } + + public ResizeImageInfo CreateDarkVariant(string? alias = null) + { + var hasDarkFile = !string.IsNullOrWhiteSpace(DarkFilename); + + return new ResizeImageInfo + { + ItemSpec = ItemSpec, + Alias = alias ?? Alias, + Filename = hasDarkFile ? DarkFilename : Filename, + BaseSize = BaseSize, + Resize = Resize, + TintColor = DarkTintColor ?? (hasDarkFile ? null : TintColor), + Color = DarkColor ?? Color, + IsAppIcon = IsAppIcon, + ForegroundFilename = ForegroundFilename, + ForegroundScale = ForegroundScale, + }; + } } } diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index feb26d3d5946..9459cfe0eba5 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -33,6 +33,10 @@ AssemblyFile="$(_ResizetizerTaskAssemblyName)" TaskName="Microsoft.Maui.Resizetizer.GenerateSplashStoryboard" /> + + @@ -241,7 +245,8 @@ - + + @@ -321,6 +326,14 @@ + + <_MauiSplashScreenDarkFile Include="%(MauiSplashScreen.DarkFile)" Condition="'%(MauiSplashScreen.DarkFile)' != ''" /> + + + + + + @@ -406,8 +419,29 @@ <_MauiHasSplashScreens Condition="'@(MauiSplashScreen->Count())' != '0'">true <_MauiShouldGenerateSplashScreen Condition="'$(_MauiHasSplashScreens)' == 'true'">true <_MauiShouldGenerateSplashScreen Condition="'$(_ResizetizerIsiOSSpecificApp)' == 'True' and '$(_MauiHasSplashScreens)' != 'true' and '$(EnableBlankMauiSplashScreen)' == 'true'">true + <_MauiAppleThemedSplashSupported Condition="'$(SupportedOSPlatformVersion)' != '' and $([MSBuild]::VersionGreaterThanOrEquals('$(SupportedOSPlatformVersion)', '14.0'))">true + + + + <_MauiSplashScreenWithDarkMetadata Include="@(MauiSplashScreen)" Condition="'%(MauiSplashScreen.DarkFile)' != '' Or '%(MauiSplashScreen.DarkColor)' != '' Or '%(MauiSplashScreen.DarkTintColor)' != ''" /> + <_MauiSplashScreenWithColorMetadata Include="@(MauiSplashScreen)" Condition="'%(MauiSplashScreen.Color)' != '' Or '%(MauiSplashScreen.DarkColor)' != ''" /> + + + + <_MauiHasThemedSplashScreen Condition="'@(_MauiSplashScreenWithDarkMetadata)' != ''">true + <_MauiLaunchScreenColorName Condition="'@(_MauiSplashScreenWithColorMetadata)' != ''">MauiSplashColor + <_MauiShouldUseThemedAppleSplashScreen Condition="'$(_ResizetizerIsiOSApp)' == 'True' and '$(_MauiHasSplashScreens)' == 'true' and '$(_MauiHasThemedSplashScreen)' == 'true' and '$(_MauiAppleThemedSplashSupported)' == 'true'">true + + + - + + + <_MauiSplashScreenWithHashesInFilename Include="@(_MauiSplashScreenWithHashes->HasMetadata('Link'))" @@ -447,22 +486,38 @@ Link="%(Filename)_%(InputsFileHash)%(Extension)" /> - + + <_MauiIntermediateStoryboard>$(_MauiIntermediateSplashScreen)MauiSplash.storyboard <_MauiIntermediatePList>$(_MauiIntermediateSplashScreen)MauiInfo.plist + + <_MauiIntermediatePList>$(_MauiIntermediateSplashScreen)MauiInfo.plist + - + + <_MauiSplashAssets Include="$(_MauiIntermediateSplashScreen)**\*" /> <_MauiSplashStoryboard Include="$(_MauiIntermediateStoryboard)" /> <_MauiSplashPList Include="$(_MauiIntermediatePList)" /> @@ -477,6 +532,16 @@ %(_MauiSplashImages.Filename)%(_MauiSplashImages.Extension) + + <_MauiSplashPList Include="$(_MauiIntermediatePList)" /> + <_MauiSplashAssetCatalog Include="$(_MauiIntermediateSplashScreen)Assets.xcassets\**\*" /> + + + Assets.xcassets\%(_MauiSplashAssetCatalog.RecursiveDir)%(_MauiSplashAssetCatalog.Filename)%(_MauiSplashAssetCatalog.Extension) + Assets.xcassets\%(_MauiSplashAssetCatalog.RecursiveDir)%(_MauiSplashAssetCatalog.Filename)%(_MauiSplashAssetCatalog.Extension) + Assets.xcassets\%(_MauiSplashAssetCatalog.RecursiveDir)%(_MauiSplashAssetCatalog.Filename)%(_MauiSplashAssetCatalog.Extension) + + { readonly string _colors; + readonly string _colorsNight; readonly string _drawable; + readonly string _drawableNight; readonly string _drawable_v31; + readonly string _drawableNight_v31; static readonly Dictionary ResizeMetadata = new() { ["Resize"] = "true" }; @@ -22,8 +25,11 @@ public GenerateSplashAndroidResourcesTests(ITestOutputHelper outputHelper) : base(outputHelper) { _colors = Path.Combine(DestinationDirectory, "values", "maui_colors.xml"); + _colorsNight = Path.Combine(DestinationDirectory, "values-night", "maui_colors.xml"); _drawable = Path.Combine(DestinationDirectory, "drawable", "maui_splash_image.xml"); + _drawableNight = Path.Combine(DestinationDirectory, "drawable-night", "maui_splash_image.xml"); _drawable_v31 = Path.Combine(DestinationDirectory, "drawable-v31", "maui_splash_image.xml"); + _drawableNight_v31 = Path.Combine(DestinationDirectory, "drawable-night-v31", "maui_splash_image.xml"); } protected GenerateSplashAndroidResources GetNewTask(params ITaskItem[] splash) => @@ -49,9 +55,123 @@ public void XmlIsValid(string inputColor, string outputColor) var success = task.Execute(); Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); - AssertColorsFile("maui_colors.xml", outputColor); + AssertDefaultColorsFile("maui_colors.xml", outputColor); AssertImageFile("maui_splash_image.xml", _drawable, "@drawable/appiconfg"); AssertImageFile("maui_splash_image_v31.xml", _drawable_v31, "@drawable/appiconfg"); + Assert.False(File.Exists(_colorsNight), "Night colors should not be generated without dark metadata."); + Assert.False(File.Exists(_drawableNight), "Night drawable should not be generated without dark metadata."); + Assert.False(File.Exists(_drawableNight_v31), "Night v31 drawable should not be generated without dark metadata."); + } + + [Fact] + public void DarkColorGeneratesNightValuesFile() + { + var splash = new TaskItem("images/appiconfg.svg", new Dictionary + { + ["Color"] = "#ffffff", + ["DarkColor"] = "#000000", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertColorsFile(_colors, "#ffffffff"); + AssertColorsFile(_colorsNight, "#ff000000"); + AssertImageFile("maui_splash_image.xml", _drawable, "@drawable/appiconfg"); + AssertImageFile("maui_splash_image.xml", _drawableNight, "@drawable/appiconfg"); + AssertImageFile("maui_splash_image_v31.xml", _drawable_v31, "@drawable/appiconfg"); + AssertImageFile("maui_splash_image_v31.xml", _drawableNight_v31, "@drawable/appiconfg"); + } + + [Fact] + public void DarkFileGeneratesNightQualifiedImages() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["Resize"] = bool.TrueString, + ["DarkFile"] = "images/camera_color.png", + ["DarkColor"] = "#000000", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileSize("drawable-mdpi/camera.png", 1792, 1792); + AssertFileSize("drawable-night-mdpi/camera.png", 1792, 1792); + AssertFileSize("drawable-xhdpi/camera.png", 3584, 3584); + AssertFileSize("drawable-night-xhdpi/camera.png", 3584, 3584); + AssertImageFile("maui_splash_image.xml", _drawableNight, "@drawable/camera"); + AssertImageFile("maui_splash_image_v31.xml", _drawableNight_v31, "@drawable/camera"); + } + + [Fact] + public void RemovingDarkColorUpdatesNightValuesToLightColor() + { + var splashWithDarkColor = new TaskItem("images/camera.png", new Dictionary + { + ["DarkColor"] = "#000000", + ["DarkFile"] = "images/camera_color.png", + }); + + var firstTask = GetNewTask(splashWithDarkColor); + var firstSuccess = firstTask.Execute(); + Assert.True(firstSuccess, LogErrorEvents.FirstOrDefault()?.Message); + Assert.True(File.Exists(_colorsNight), "Night colors should be generated when DarkColor is set."); + + var splashWithoutDarkColor = new TaskItem("images/camera.png", new Dictionary + { + ["Color"] = "#ffffff", + ["DarkFile"] = "images/camera_color.png", + }); + + var secondTask = GetNewTask(splashWithoutDarkColor); + var secondSuccess = secondTask.Execute(); + Assert.True(secondSuccess, LogErrorEvents.FirstOrDefault()?.Message); + AssertColorsFile(_colorsNight, "#ffffffff"); + } + + [Fact] + public void RemovingDarkColorAndLightColorRemovesStaleNightValuesFile() + { + var splashWithDarkColor = new TaskItem("images/camera.png", new Dictionary + { + ["DarkColor"] = "#000000", + ["DarkFile"] = "images/camera_color.png", + }); + + var firstTask = GetNewTask(splashWithDarkColor); + var firstSuccess = firstTask.Execute(); + Assert.True(firstSuccess, LogErrorEvents.FirstOrDefault()?.Message); + Assert.True(File.Exists(_colorsNight), "Night colors should be generated when DarkColor is set."); + + var splashWithoutColors = new TaskItem("images/camera.png", new Dictionary + { + ["DarkFile"] = "images/camera_color.png", + }); + + var secondTask = GetNewTask(splashWithoutColors); + var secondSuccess = secondTask.Execute(); + Assert.True(secondSuccess, LogErrorEvents.FirstOrDefault()?.Message); + Assert.False(File.Exists(_colorsNight), "Night colors should be removed when dark mode remains but no color metadata remains."); + } + + [Fact] + public void DarkFileWithLightColorWritesNightValuesFileEvenWhenColorsMatch() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["Color"] = "#ffffff", + ["DarkFile"] = "images/camera_color.png", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertColorsFile(_colors, "#ffffffff"); + AssertColorsFile(_colorsNight, "#ffffffff"); } [Theory] @@ -292,15 +412,18 @@ public void SingleImageWithBaseSizeSucceeds(string alias, string outputName) AssertFileSize($"drawable-xhdpi/{outputName}.png", 88, 88); } - void AssertColorsFile(string expectedFilename, string color) + void AssertDefaultColorsFile(string expectedFilename, string color) + => AssertColorsFile(Path.Combine(DestinationDirectory, "values", expectedFilename), color); + + void AssertColorsFile(string actualFilename, string color) { - var expectedXml = File.ReadAllText($"testdata/androidsplash/" + expectedFilename) + var expectedXml = File.ReadAllText($"testdata/androidsplash/maui_colors.xml") .Replace("{maui_splash_color}", color, StringComparison.OrdinalIgnoreCase); - var actual = XElement.Load(_colors); + var actual = XElement.Load(actualFilename); var expected = XElement.Parse(expectedXml); - Assert.True(XNode.DeepEquals(actual, expected), $"{_colors} did not match:\n{actual}"); + Assert.True(XNode.DeepEquals(actual, expected), $"{actualFilename} did not match:\n{actual}"); } void AssertImageFile(string expectedFilename, string actualFilename, string image, string width = "108", string height = "108") diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs new file mode 100644 index 000000000000..04176b0bf6b2 --- /dev/null +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Maui.Resizetizer.Tests +{ + public class GenerateSplashAssetCatalogTests : MSBuildTaskTestFixture + { + public GenerateSplashAssetCatalogTests(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + protected GenerateSplashAssetCatalog GetNewTask(params ITaskItem[] splash) => + new() + { + IntermediateOutputPath = DestinationDirectory, + InputsFile = "mauisplash.inputs", + MauiSplashScreen = splash, + BuildEngine = this, + }; + + [Fact] + public void DarkMetadataGeneratesImageAndColorAssetCatalogs() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["Color"] = "#ffffff", + ["DarkColor"] = "#000000", + ["DarkFile"] = "images/camera_color.png", + ["BaseSize"] = "44", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/Contents.json"); + AssertFileExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png", 44, 44); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.png", 88, 88); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png", 44, 44); + + using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset", "Contents.json"))); + var images = imageJson.RootElement.GetProperty("images").EnumerateArray().ToArray(); + Assert.Contains(images, image => !image.TryGetProperty("appearances", out _)); + Assert.Contains(images, image => GetAppearanceValue(image) == "light"); + Assert.Contains(images, image => GetAppearanceValue(image) == "dark"); + + using var colorJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashColor.colorset", "Contents.json"))); + var colors = colorJson.RootElement.GetProperty("colors").EnumerateArray().ToArray(); + Assert.Contains(colors, color => !color.TryGetProperty("appearances", out _)); + Assert.Contains(colors, color => GetAppearanceValue(color) == "light"); + var darkColor = Assert.Single(colors.Where(color => GetAppearanceValue(color) == "dark")); + Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); + Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("green").GetString()); + Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("blue").GetString()); + } + + [Fact] + public void RasterWithoutBaseSizeGeneratesAllReferencedImageFiles() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["DarkFile"] = "images/camera_color.png", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertAllImageSetFilesExist(); + } + + [Fact] + public void DarkFileWithoutColorDoesNotGenerateColorAsset() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["DarkFile"] = "images/camera_color.png", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/Contents.json"); + AssertFileNotExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); + } + + [Fact] + public void LaunchScreenPlistUsesNamedAssets() + { + var task = new CreatePartialInfoPlistTask + { + IntermediateOutputPath = DestinationDirectory, + PlistName = "MauiInfo.plist", + LaunchScreenImage = "MauiSplashImage", + LaunchScreenColor = "MauiSplashColor", + BuildEngine = this, + }; + + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + var plist = XElement.Load(Path.Combine(DestinationDirectory, "MauiInfo.plist")); + var text = plist.ToString(); + Assert.Contains("UILaunchScreen", text, StringComparison.Ordinal); + Assert.Contains("UIImageName", text, StringComparison.Ordinal); + Assert.Contains("MauiSplashImage", text, StringComparison.Ordinal); + Assert.Contains("UIColorName", text, StringComparison.Ordinal); + Assert.Contains("MauiSplashColor", text, StringComparison.Ordinal); + Assert.DoesNotContain("UILaunchStoryboardName", text, StringComparison.Ordinal); + } + + [Fact] + public void LaunchScreenPlistOmitsColorWhenNoNamedColorIsProvided() + { + var task = new CreatePartialInfoPlistTask + { + IntermediateOutputPath = DestinationDirectory, + PlistName = "MauiInfo.plist", + LaunchScreenImage = "MauiSplashImage", + BuildEngine = this, + }; + + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + var plist = XElement.Load(Path.Combine(DestinationDirectory, "MauiInfo.plist")); + var text = plist.ToString(); + Assert.Contains("UILaunchScreen", text, StringComparison.Ordinal); + Assert.Contains("UIImageName", text, StringComparison.Ordinal); + Assert.DoesNotContain("UIColorName", text, StringComparison.Ordinal); + Assert.DoesNotContain("MauiSplashColor", text, StringComparison.Ordinal); + } + + void AssertAllImageSetFilesExist() + { + var imageSetPath = Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset"); + using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(imageSetPath, "Contents.json"))); + + foreach (var image in imageJson.RootElement.GetProperty("images").EnumerateArray()) + { + if (image.TryGetProperty("filename", out var filename)) + Assert.True(File.Exists(Path.Combine(imageSetPath, filename.GetString()!)), $"Expected {filename.GetString()} to exist."); + } + } + + static string GetAppearanceValue(JsonElement element) => + element.TryGetProperty("appearances", out var appearances) && appearances.GetArrayLength() > 0 + ? appearances[0].GetProperty("value").GetString() + : null; + } +} diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashStoryboardTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashStoryboardTests.cs index 13dea67d637c..7d506eeb61c7 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashStoryboardTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashStoryboardTests.cs @@ -82,6 +82,26 @@ public void XmlIsValidForNoSplash() AssertFile(_storyboard); } + [Fact] + public void RemovesThemedAssetCatalogWhenGeneratingStoryboard() + { + var assetCatalog = Path.Combine(DestinationDirectory, "Assets.xcassets"); + Directory.CreateDirectory(Path.Combine(assetCatalog, "MauiSplashImage.imageset")); + File.WriteAllText(Path.Combine(assetCatalog, "MauiSplashImage.imageset", "Contents.json"), "{}"); + + var splash = new TaskItem("images/appiconfg.svg", new Dictionary + { + ["Color"] = "#ffffff", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + Assert.False(Directory.Exists(assetCatalog), "Themed asset catalog should be removed when falling back to storyboard generation."); + Assert.True(File.Exists(_storyboard), "Storyboard should be generated."); + } + [Theory] [InlineData(null, "appiconfg.png")] [InlineData("images/CustomAlias.svg", "CustomAlias.png")] diff --git a/src/SingleProject/Resizetizer/test/UnitTests/ResizeImageInfoTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/ResizeImageInfoTests.cs index ee07b7e8a423..456052385caf 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/ResizeImageInfoTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/ResizeImageInfoTests.cs @@ -1,9 +1,65 @@ +using System.Collections.Generic; +using System; +using Microsoft.Build.Utilities; +using SkiaSharp; using Xunit; namespace Microsoft.Maui.Resizetizer.Tests { public class ResizeImageInfoTests { + public class Parse + { + [Fact] + public void SupportsDarkSplashMetadata() + { + var image = new TaskItem("images/camera.png", new Dictionary + { + ["DarkFile"] = "images/camera_color.png", + ["DarkColor"] = "#000000", + ["DarkTintColor"] = "#ffffff", + }); + + var info = ResizeImageInfo.Parse(image); + + Assert.Equal(SKColors.Black, info.DarkColor); + Assert.Equal(SKColors.White, info.DarkTintColor); + Assert.EndsWith("camera_color.png", info.DarkFilename, StringComparison.Ordinal); + Assert.False(info.DarkIsVector); + } + + [Fact] + public void DarkTintColorFallsBackToTintColorOnlyWhenDarkFileIsNotSpecified() + { + var image = new TaskItem("images/camera.png", new Dictionary + { + ["TintColor"] = "#ff0000", + }); + + var info = ResizeImageInfo.Parse(image); + var darkInfo = info.CreateDarkVariant(); + + Assert.Equal(SKColors.Red, darkInfo.TintColor); + Assert.Equal(info.Filename, darkInfo.Filename); + } + + [Fact] + public void DarkTintColorDoesNotFallbackToTintColorWhenDarkFileIsSpecified() + { + var image = new TaskItem("images/camera.png", new Dictionary + { + ["TintColor"] = "#ff0000", + ["DarkFile"] = "images/camera_color.png", + }); + + var info = ResizeImageInfo.Parse(image); + var darkInfo = info.CreateDarkVariant(); + + Assert.Null(darkInfo.TintColor); + Assert.EndsWith("camera_color.png", darkInfo.Filename, StringComparison.Ordinal); + } + } + public class IsVector { [Theory] From b384e1d9c7cec7616481bed748eeceb63266e42e Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 18 Jun 2026 14:57:02 +0200 Subject: [PATCH 02/11] Address themed splash review feedback Fixes dark splash Android output extension and stale night-resource cleanup, scopes Apple splash color metadata to the first splash item, and warns/tests DarkColor-only Apple fallback behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/GenerateSplashAndroidResources.cs | 41 +++++++++- .../src/GenerateSplashAssetCatalog.cs | 5 ++ .../Microsoft.Maui.Resizetizer.After.targets | 9 ++- .../GenerateSplashAndroidResourcesTests.cs | 77 +++++++++++++++++++ .../GenerateSplashAssetCatalogTests.cs | 27 +++++++ 5 files changed, 155 insertions(+), 4 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs index 8418af4e1a6a..1ed3e4c05fb4 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs @@ -46,7 +46,7 @@ public override bool Execute() Resizer darkResizer = null; if (info.HasDarkMode) { - var darkInfo = info.CreateDarkVariant(info.OutputName + info.OutputExtension); + var darkInfo = info.CreateDarkVariant(GetDarkOutputAlias(info)); if (darkInfo.BaseSize is null && !string.IsNullOrWhiteSpace(info.DarkFilename)) darkInfo.BaseSize = resizer.BaseSize ?? resizer.GetOriginalSize(); darkResizer = new Resizer(darkInfo, IntermediateOutputPath, this); @@ -54,7 +54,10 @@ public override bool Execute() WriteImages(resizer); if (darkResizer is not null) + { + CleanStaleNightImageResources(darkResizer.Info.Resize); WriteImages(darkResizer, dark: true); + } else CleanNightResources(); @@ -220,6 +223,42 @@ void DeleteNightColors() File.Delete(colorsFile); } + void CleanStaleNightImageResources(bool resize) + { + foreach (var directory in resize ? NightOriginalResourceDirectories : NightDensityResourceDirectories) + { + var path = Path.Combine(IntermediateOutputPath, directory); + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + } + + static readonly string[] NightOriginalResourceDirectories = + { + "drawable-night", + "drawable-night-v31", + }; + + static readonly string[] NightDensityResourceDirectories = + { + "drawable-night-mdpi", + "drawable-night-hdpi", + "drawable-night-xhdpi", + "drawable-night-xxhdpi", + "drawable-night-xxxhdpi", + }; + + static string GetDarkOutputAlias(ResizeImageInfo info) + { + var extension = info.DarkIsVector || (string.IsNullOrWhiteSpace(info.DarkFilename) && info.IsVector) + ? Resizer.RasterFileExtension + : !string.IsNullOrWhiteSpace(info.DarkFilename) + ? Path.GetExtension(info.DarkFilename) + : info.OutputExtension; + + return info.OutputName + extension; + } + static SKSize CalculateScaledSize(Resizer resizer) { var size = resizer.BaseSize ?? resizer.GetOriginalSize(); diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs index 2ed40b1db8f3..0ec471c09dc7 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -44,7 +44,12 @@ public override bool Execute() WriteImages(new Resizer(darkInfo, IntermediateOutputPath, this)); WriteImageSet(); if (info.Color is not null || info.DarkColor is not null) + { + if (info.Color is null && info.DarkColor is not null) + Log.LogWarning("MauiSplashScreen DarkColor was specified without Color; white will be used as the light-mode launch screen background."); + WriteColorSet(info.Color ?? SKColors.White, info.DarkColor ?? info.Color ?? SKColors.White); + } return !Log.HasLoggedErrors; } diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index 9459cfe0eba5..fecd8e2da395 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -327,7 +327,8 @@ - <_MauiSplashScreenDarkFile Include="%(MauiSplashScreen.DarkFile)" Condition="'%(MauiSplashScreen.DarkFile)' != ''" /> + <_MauiSplashScreenWithHashes Update="@(_MauiSplashScreenWithHashes)" DarkFile="$([System.IO.Path]::GetFullPath('%(_MauiSplashScreenWithHashes.DarkFile)'))" Condition="'%(_MauiSplashScreenWithHashes.DarkFile)' != ''" /> + <_MauiSplashScreenDarkFile Include="%(_MauiSplashScreenWithHashes.DarkFile)" Condition="'%(_MauiSplashScreenWithHashes.DarkFile)' != ''" /> @@ -417,14 +418,16 @@ <_MauiHasSplashScreens>false <_MauiHasSplashScreens Condition="'@(MauiSplashScreen->Count())' != '0'">true + <_MauiFirstSplashScreenIdentity Condition="'$(_MauiHasSplashScreens)' == 'true'">@(MauiSplashScreen->'%(Identity)')[0] <_MauiShouldGenerateSplashScreen Condition="'$(_MauiHasSplashScreens)' == 'true'">true <_MauiShouldGenerateSplashScreen Condition="'$(_ResizetizerIsiOSSpecificApp)' == 'True' and '$(_MauiHasSplashScreens)' != 'true' and '$(EnableBlankMauiSplashScreen)' == 'true'">true <_MauiAppleThemedSplashSupported Condition="'$(SupportedOSPlatformVersion)' != '' and $([MSBuild]::VersionGreaterThanOrEquals('$(SupportedOSPlatformVersion)', '14.0'))">true - <_MauiSplashScreenWithDarkMetadata Include="@(MauiSplashScreen)" Condition="'%(MauiSplashScreen.DarkFile)' != '' Or '%(MauiSplashScreen.DarkColor)' != '' Or '%(MauiSplashScreen.DarkTintColor)' != ''" /> - <_MauiSplashScreenWithColorMetadata Include="@(MauiSplashScreen)" Condition="'%(MauiSplashScreen.Color)' != '' Or '%(MauiSplashScreen.DarkColor)' != ''" /> + <_MauiFirstSplashScreen Include="@(MauiSplashScreen)" Condition="'%(MauiSplashScreen.Identity)' == '$(_MauiFirstSplashScreenIdentity)'" /> + <_MauiSplashScreenWithDarkMetadata Include="@(_MauiFirstSplashScreen)" Condition="'%(_MauiFirstSplashScreen.DarkFile)' != '' Or '%(_MauiFirstSplashScreen.DarkColor)' != '' Or '%(_MauiFirstSplashScreen.DarkTintColor)' != ''" /> + <_MauiSplashScreenWithColorMetadata Include="@(_MauiFirstSplashScreen)" Condition="'%(_MauiFirstSplashScreen.Color)' != '' Or '%(_MauiFirstSplashScreen.DarkColor)' != ''" /> diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs index b3972584414f..d096499b013b 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs @@ -106,6 +106,83 @@ public void DarkFileGeneratesNightQualifiedImages() AssertImageFile("maui_splash_image_v31.xml", _drawableNight_v31, "@drawable/camera"); } + [Fact] + public void DarkRasterFileWithLightVectorUsesRasterOutputExtension() + { + var splash = new TaskItem("images/appiconfg.svg", new Dictionary + { + ["Resize"] = bool.TrueString, + ["DarkFile"] = "images/camera_color.png", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("drawable-night-mdpi/appiconfg.png"); + AssertFileNotExists("drawable-night-mdpi/appiconfg.svg"); + AssertImageFile("maui_splash_image.xml", _drawableNight, "@drawable/appiconfg"); + AssertImageFile("maui_splash_image_v31.xml", _drawableNight_v31, "@drawable/appiconfg"); + } + + [Fact] + public void ChangingDarkImageFromResizedToOriginalCleansStaleNightDensityImages() + { + var resizedSplash = new TaskItem("images/camera.png", new Dictionary + { + ["Resize"] = bool.TrueString, + ["DarkFile"] = "images/camera_color.png", + }); + + var firstTask = GetNewTask(resizedSplash); + var firstSuccess = firstTask.Execute(); + Assert.True(firstSuccess, LogErrorEvents.FirstOrDefault()?.Message); + AssertFileExists("drawable-night-mdpi/camera.png"); + + var originalSplash = new TaskItem("images/camera.png", new Dictionary + { + ["Resize"] = bool.FalseString, + ["DarkFile"] = "images/camera_color.png", + }); + + var secondTask = GetNewTask(originalSplash); + var secondSuccess = secondTask.Execute(); + Assert.True(secondSuccess, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("drawable-night/camera.png"); + AssertFileNotExists("drawable-night-mdpi/camera.png"); + AssertFileNotExists("drawable-night-hdpi/camera.png"); + } + + [Fact] + public void ChangingDarkImageFromOriginalToResizedCleansStaleNightOriginalImage() + { + var originalSplash = new TaskItem("images/camera.png", new Dictionary + { + ["Resize"] = bool.FalseString, + ["DarkFile"] = "images/camera_color.png", + }); + + var firstTask = GetNewTask(originalSplash); + var firstSuccess = firstTask.Execute(); + Assert.True(firstSuccess, LogErrorEvents.FirstOrDefault()?.Message); + AssertFileExists("drawable-night/camera.png"); + + var resizedSplash = new TaskItem("images/camera.png", new Dictionary + { + ["Resize"] = bool.TrueString, + ["DarkFile"] = "images/camera_color.png", + }); + + var secondTask = GetNewTask(resizedSplash); + var secondSuccess = secondTask.Execute(); + Assert.True(secondSuccess, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileNotExists("drawable-night/camera.png"); + AssertFileExists("drawable-night-mdpi/camera.png"); + AssertImageFile("maui_splash_image.xml", _drawableNight, "@drawable/camera"); + } + [Fact] public void RemovingDarkColorUpdatesNightValuesToLightColor() { diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index 04176b0bf6b2..3391d4b5172b 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -95,6 +95,33 @@ public void DarkFileWithoutColorDoesNotGenerateColorAsset() AssertFileNotExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); } + [Fact] + public void DarkColorOnlyGeneratesImageSetAndColorAssetWithWarning() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["DarkColor"] = "#000000", + ["BaseSize"] = "44", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/Contents.json"); + AssertFileExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png", 44, 44); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png", 44, 44); + Assert.Contains(LogWarningEvents, warning => warning.Message.Contains("DarkColor was specified without Color", StringComparison.Ordinal)); + + using var colorJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashColor.colorset", "Contents.json"))); + var colors = colorJson.RootElement.GetProperty("colors").EnumerateArray().ToArray(); + var anyColor = Assert.Single(colors.Where(color => !color.TryGetProperty("appearances", out _))); + var darkColor = Assert.Single(colors.Where(color => GetAppearanceValue(color) == "dark")); + Assert.Equal("1", anyColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); + Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); + } + [Fact] public void LaunchScreenPlistUsesNamedAssets() { From 216e25ebca21766d7fec8c573b4fca0116f1e1f9 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Wed, 1 Jul 2026 10:12:09 +0200 Subject: [PATCH 03/11] Fix themed splash target selection Select the first MauiSplashScreen item with valid MSBuild syntax so Apple themed splash metadata is evaluated against the same item consumed by the generators. Add target-level regression coverage and dark-tint task tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/GenerateSplashAndroidResources.cs | 1 - .../Microsoft.Maui.Resizetizer.After.targets | 3 +- .../GenerateSplashAndroidResourcesTests.cs | 21 +++ .../GenerateSplashAssetCatalogTests.cs | 22 +++ .../UnitTests/MauiResizetizerTargetsTests.cs | 175 ++++++++++++++++++ 5 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs index 1ed3e4c05fb4..fc9fa6920341 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs @@ -236,7 +236,6 @@ void CleanStaleNightImageResources(bool resize) static readonly string[] NightOriginalResourceDirectories = { "drawable-night", - "drawable-night-v31", }; static readonly string[] NightDensityResourceDirectories = diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index 9cc271efcc5a..58b2cc8c4e81 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -419,7 +419,8 @@ <_MauiHasSplashScreens>false <_MauiHasSplashScreens Condition="'@(MauiSplashScreen->Count())' != '0'">true - <_MauiFirstSplashScreenIdentity Condition="'$(_MauiHasSplashScreens)' == 'true'">@(MauiSplashScreen->'%(Identity)')[0] + <_MauiSplashScreenIdentities>@(MauiSplashScreen) + <_MauiFirstSplashScreenIdentity Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenIdentities.Split(';')[0]) <_MauiShouldGenerateSplashScreen Condition="'$(_MauiHasSplashScreens)' == 'true'">true <_MauiShouldGenerateSplashScreen Condition="'$(_ResizetizerIsiOSSpecificApp)' == 'True' and '$(_MauiHasSplashScreens)' != 'true' and '$(EnableBlankMauiSplashScreen)' == 'true'">true <_MauiAppleThemedSplashSupported Condition="'$(SupportedOSPlatformVersion)' != '' and $([MSBuild]::VersionGreaterThanOrEquals('$(SupportedOSPlatformVersion)', '14.0'))">true diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs index d096499b013b..84a17590fda0 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAndroidResourcesTests.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using SkiaSharp; using Xunit; using Xunit.Abstractions; @@ -106,6 +107,26 @@ public void DarkFileGeneratesNightQualifiedImages() AssertImageFile("maui_splash_image_v31.xml", _drawableNight_v31, "@drawable/camera"); } + [Fact] + public void DarkTintColorOnlyGeneratesTintedNightImage() + { + var splash = new TaskItem("images/camera.svg", new Dictionary + { + ["DarkTintColor"] = "#ff0000", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileSize("drawable-mdpi/camera.png", 1792, 1792); + AssertFileSize("drawable-night-mdpi/camera.png", 1792, 1792); + AssertFileContains("drawable-night-mdpi/camera.png", SKColors.Red, 350, 350); + AssertFileDoesNotContain("drawable-mdpi/camera.png", SKColors.Red, 350, 350); + AssertImageFile("maui_splash_image.xml", _drawableNight, "@drawable/camera"); + Assert.False(File.Exists(_colorsNight), "Night colors should not be generated without dark color metadata."); + } + [Fact] public void DarkRasterFileWithLightVectorUsesRasterOutputExtension() { diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index 3391d4b5172b..f2ce17e96b99 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -6,6 +6,7 @@ using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using SkiaSharp; using Xunit; using Xunit.Abstractions; @@ -95,6 +96,27 @@ public void DarkFileWithoutColorDoesNotGenerateColorAsset() AssertFileNotExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); } + [Fact] + public void DarkTintColorOnlyGeneratesTintedDarkImage() + { + var splash = new TaskItem("images/camera.svg", new Dictionary + { + ["DarkTintColor"] = "#ff0000", + ["BaseSize"] = "44", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/Contents.json"); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png", 44, 44); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png", 44, 44); + AssertFileContains("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png", SKColors.Red); + AssertFileDoesNotContain("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png", SKColors.Red); + AssertFileNotExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); + } + [Fact] public void DarkColorOnlyGeneratesImageSetAndColorAssetWithWarning() { diff --git a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs new file mode 100644 index 000000000000..d09bf2878a5f --- /dev/null +++ b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Maui.Resizetizer.Tests +{ + public class MauiResizetizerTargetsTests : BaseTest + { + public MauiResizetizerTargetsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void ProcessMauiSplashScreensSelectsFirstSplashForThemedAppleMetadata() + { + var targetFile = Path.Combine( + FindRepositoryRoot(), + "src", + "SingleProject", + "Resizetizer", + "src", + "nuget", + "buildTransitive", + "Microsoft.Maui.Resizetizer.After.targets"); + var metadataElements = GetSplashMetadataElements(targetFile); + var projectFile = Path.Combine(DestinationDirectory, "SelectFirstSplash.proj"); + Directory.CreateDirectory(DestinationDirectory); + + File.WriteAllText(projectFile, + $$""" + + + <_ResizetizerIsiOSApp>True + 14.0 + + + + + + + {{metadataElements}} + + + + + + + + """); + + var output = RunDotnetMSBuild(projectFile); + + Assert.Contains("First=first.png; Themed=true; Color=", output, StringComparison.Ordinal); + } + + [Fact] + public void ProcessMauiSplashScreensWarnsWhenThemedAppleSplashIsUnsupported() + { + var targetFile = Path.Combine( + FindRepositoryRoot(), + "src", + "SingleProject", + "Resizetizer", + "src", + "nuget", + "buildTransitive", + "Microsoft.Maui.Resizetizer.After.targets"); + var metadataAndWarnings = GetSplashMetadataElements(targetFile, includeWarnings: true); + var projectFile = Path.Combine(DestinationDirectory, "UnsupportedThemedSplash.proj"); + Directory.CreateDirectory(DestinationDirectory); + + File.WriteAllText(projectFile, + $$""" + + + <_ResizetizerIsiOSSpecificApp>True + 13.0 + + + + + + {{metadataAndWarnings}} + + + + + """); + + var output = RunDotnetMSBuild(projectFile); + + Assert.Contains("Themed MauiSplashScreen assets require iOS or iPadOS 14.0 or later", output, StringComparison.Ordinal); + } + + static string GetSplashMetadataElements(string targetFile, bool includeWarnings = false) + { + var doc = XDocument.Load(targetFile); + var processSplashScreens = Assert.Single(doc.Root.Elements().Where(e => e.Name.LocalName == "Target" && e.Attribute("Name")?.Value == "ProcessMauiSplashScreens")); + var groups = processSplashScreens + .Elements() + .Where(e => e.Name.LocalName is "PropertyGroup" or "ItemGroup") + .Take(3) + .ToArray(); + + Assert.Equal(new[] { "PropertyGroup", "ItemGroup", "PropertyGroup" }, groups.Select(e => e.Name.LocalName).ToArray()); + + var elements = includeWarnings + ? groups.Concat(processSplashScreens.Elements().Where(e => e.Name.LocalName == "Warning")) + : groups; + + return string.Join(Environment.NewLine, elements.Select(e => StripNamespace(e).ToString(SaveOptions.DisableFormatting))); + } + + static XElement StripNamespace(XElement element) => + new( + element.Name.LocalName, + element.Attributes().Where(a => !a.IsNamespaceDeclaration).Select(a => new XAttribute(a.Name.LocalName, a.Value)), + element.Nodes().Select(n => n is XElement child ? StripNamespace(child) : n)); + + string RunDotnetMSBuild(string projectFile) + { + var startInfo = new ProcessStartInfo + { + FileName = GetDotNetHost(), + WorkingDirectory = DestinationDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + startInfo.ArgumentList.Add("msbuild"); + startInfo.ArgumentList.Add(projectFile); + startInfo.ArgumentList.Add("-t:Validate"); + startInfo.ArgumentList.Add("-nologo"); + startInfo.ArgumentList.Add("-v:minimal"); + + using var process = Process.Start(startInfo); + Assert.NotNull(process); + + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + Output.WriteLine(output); + Output.WriteLine(error); + + Assert.Equal(0, process.ExitCode); + + return output + error; + } + + static string GetDotNetHost() + { + var dotnetHost = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + + return !string.IsNullOrWhiteSpace(dotnetHost) && File.Exists(dotnetHost) + ? dotnetHost + : "dotnet"; + } + + static string FindRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Microsoft.Maui.sln"))) + directory = directory.Parent; + + return directory?.FullName ?? throw new DirectoryNotFoundException("Could not find the repository root."); + } + } +} From 15ca41d09e4a45440e2f4af17030735bde597b0e Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 2 Jul 2026 10:59:14 +0200 Subject: [PATCH 04/11] Address themed splash review feedback Handle duplicate splash item identities by evaluating first-item metadata by position, remove stale Apple color assets when color metadata is removed, and harden target-level regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/GenerateSplashAssetCatalog.cs | 11 +++++++ .../Microsoft.Maui.Resizetizer.After.targets | 22 +++++++------- .../GenerateSplashAssetCatalogTests.cs | 29 ++++++++++++++++++ .../UnitTests/MauiResizetizerTargetsTests.cs | 30 +++++++++++-------- 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs index 0ec471c09dc7..f2b61e81266d 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -50,6 +50,10 @@ public override bool Execute() WriteColorSet(info.Color ?? SKColors.White, info.DarkColor ?? info.Color ?? SKColors.White); } + else + { + DeleteColorSet(); + } return !Log.HasLoggedErrors; } @@ -258,6 +262,13 @@ private void CleanStoryboard() File.Delete(storyboard); } + private void DeleteColorSet() + { + var colorSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashColorSetPath); + if (Directory.Exists(colorSetPath)) + Directory.Delete(colorSetPath, recursive: true); + } + void ILogger.Log(string message) { Log?.LogMessage(message); diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index 58b2cc8c4e81..40273b298c0e 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -419,22 +419,24 @@ <_MauiHasSplashScreens>false <_MauiHasSplashScreens Condition="'@(MauiSplashScreen->Count())' != '0'">true - <_MauiSplashScreenIdentities>@(MauiSplashScreen) - <_MauiFirstSplashScreenIdentity Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenIdentities.Split(';')[0]) + <_MauiSplashScreenIdentities>@(MauiSplashScreen->'%(Identity)', '|') + <_MauiSplashScreenColors>@(MauiSplashScreen->'%(Color)', '|') + <_MauiSplashScreenDarkColors>@(MauiSplashScreen->'%(DarkColor)', '|') + <_MauiSplashScreenDarkFiles>@(MauiSplashScreen->'%(DarkFile)', '|') + <_MauiSplashScreenDarkTintColors>@(MauiSplashScreen->'%(DarkTintColor)', '|') + <_MauiFirstSplashScreenIdentity Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenIdentities.Split('|')[0]) + <_MauiFirstSplashScreenColor Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenColors.Split('|')[0]) + <_MauiFirstSplashScreenDarkColor Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenDarkColors.Split('|')[0]) + <_MauiFirstSplashScreenDarkFile Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenDarkFiles.Split('|')[0]) + <_MauiFirstSplashScreenDarkTintColor Condition="'$(_MauiHasSplashScreens)' == 'true'">$(_MauiSplashScreenDarkTintColors.Split('|')[0]) <_MauiShouldGenerateSplashScreen Condition="'$(_MauiHasSplashScreens)' == 'true'">true <_MauiShouldGenerateSplashScreen Condition="'$(_ResizetizerIsiOSSpecificApp)' == 'True' and '$(_MauiHasSplashScreens)' != 'true' and '$(EnableBlankMauiSplashScreen)' == 'true'">true <_MauiAppleThemedSplashSupported Condition="'$(SupportedOSPlatformVersion)' != '' and $([MSBuild]::VersionGreaterThanOrEquals('$(SupportedOSPlatformVersion)', '14.0'))">true - - <_MauiFirstSplashScreen Include="@(MauiSplashScreen)" Condition="'%(MauiSplashScreen.Identity)' == '$(_MauiFirstSplashScreenIdentity)'" /> - <_MauiSplashScreenWithDarkMetadata Include="@(_MauiFirstSplashScreen)" Condition="'%(_MauiFirstSplashScreen.DarkFile)' != '' Or '%(_MauiFirstSplashScreen.DarkColor)' != '' Or '%(_MauiFirstSplashScreen.DarkTintColor)' != ''" /> - <_MauiSplashScreenWithColorMetadata Include="@(_MauiFirstSplashScreen)" Condition="'%(_MauiFirstSplashScreen.Color)' != '' Or '%(_MauiFirstSplashScreen.DarkColor)' != ''" /> - - - <_MauiHasThemedSplashScreen Condition="'@(_MauiSplashScreenWithDarkMetadata)' != ''">true - <_MauiLaunchScreenColorName Condition="'@(_MauiSplashScreenWithColorMetadata)' != ''">MauiSplashColor + <_MauiHasThemedSplashScreen Condition="'$(_MauiFirstSplashScreenDarkFile)' != '' Or '$(_MauiFirstSplashScreenDarkColor)' != '' Or '$(_MauiFirstSplashScreenDarkTintColor)' != ''">true + <_MauiLaunchScreenColorName Condition="'$(_MauiFirstSplashScreenColor)' != '' Or '$(_MauiFirstSplashScreenDarkColor)' != ''">MauiSplashColor <_MauiShouldUseThemedAppleSplashScreen Condition="'$(_ResizetizerIsiOSApp)' == 'True' and '$(_MauiHasSplashScreens)' == 'true' and '$(_MauiHasThemedSplashScreen)' == 'true' and '$(_MauiAppleThemedSplashSupported)' == 'true'">true diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index f2ce17e96b99..3658ab20fdef 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -96,6 +96,35 @@ public void DarkFileWithoutColorDoesNotGenerateColorAsset() AssertFileNotExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); } + [Fact] + public void RemovingColorMetadataDeletesStaleColorAsset() + { + var splashWithColor = new TaskItem("images/camera.png", new Dictionary + { + ["Color"] = "#ffffff", + ["DarkColor"] = "#000000", + ["DarkFile"] = "images/camera_color.png", + ["BaseSize"] = "44", + }); + + var firstTask = GetNewTask(splashWithColor); + var firstSuccess = firstTask.Execute(); + Assert.True(firstSuccess, LogErrorEvents.FirstOrDefault()?.Message); + AssertFileExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); + + var splashWithoutColor = new TaskItem("images/camera.png", new Dictionary + { + ["DarkFile"] = "images/camera_color.png", + ["BaseSize"] = "44", + }); + + var secondTask = GetNewTask(splashWithoutColor); + var secondSuccess = secondTask.Execute(); + Assert.True(secondSuccess, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileNotExists("Assets.xcassets/MauiSplashColor.colorset/Contents.json"); + } + [Fact] public void DarkTintColorOnlyGeneratesTintedDarkImage() { diff --git a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs index d09bf2878a5f..05e1b49c4cf5 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs @@ -39,13 +39,15 @@ public void ProcessMauiSplashScreensSelectsFirstSplashForThemedAppleMetadata() 14.0 - - + + {{metadataElements}} - - + + + + @@ -55,7 +57,7 @@ public void ProcessMauiSplashScreensSelectsFirstSplashForThemedAppleMetadata() var output = RunDotnetMSBuild(projectFile); - Assert.Contains("First=first.png; Themed=true; Color=", output, StringComparison.Ordinal); + Assert.Contains("First=splash.png; DarkFile=first-dark.png; FirstColor=; Themed=true; Color=", output, StringComparison.Ordinal); } [Fact] @@ -101,17 +103,17 @@ static string GetSplashMetadataElements(string targetFile, bool includeWarnings { var doc = XDocument.Load(targetFile); var processSplashScreens = Assert.Single(doc.Root.Elements().Where(e => e.Name.LocalName == "Target" && e.Attribute("Name")?.Value == "ProcessMauiSplashScreens")); - var groups = processSplashScreens + var propertyGroups = processSplashScreens .Elements() - .Where(e => e.Name.LocalName is "PropertyGroup" or "ItemGroup") - .Take(3) + .Where(e => e.Name.LocalName == "PropertyGroup") .ToArray(); - Assert.Equal(new[] { "PropertyGroup", "ItemGroup", "PropertyGroup" }, groups.Select(e => e.Name.LocalName).ToArray()); + var firstSplashMetadata = Assert.Single(propertyGroups.Where(e => e.Elements().Any(child => child.Name.LocalName == "_MauiHasSplashScreens"))); + var themedSplashMetadata = Assert.Single(propertyGroups.Where(e => e.Elements().Any(child => child.Name.LocalName == "_MauiHasThemedSplashScreen"))); var elements = includeWarnings - ? groups.Concat(processSplashScreens.Elements().Where(e => e.Name.LocalName == "Warning")) - : groups; + ? new[] { firstSplashMetadata, themedSplashMetadata }.Concat(processSplashScreens.Elements().Where(e => e.Name.LocalName == "Warning")) + : new[] { firstSplashMetadata, themedSplashMetadata }; return string.Join(Environment.NewLine, elements.Select(e => StripNamespace(e).ToString(SaveOptions.DisableFormatting))); } @@ -141,9 +143,11 @@ string RunDotnetMSBuild(string projectFile) using var process = Process.Start(startInfo); Assert.NotNull(process); - var output = process.StandardOutput.ReadToEnd(); - var error = process.StandardError.ReadToEnd(); + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); process.WaitForExit(); + var output = outputTask.GetAwaiter().GetResult(); + var error = errorTask.GetAwaiter().GetResult(); Output.WriteLine(output); Output.WriteLine(error); From 0a0814eec92f0903bacda13e79ae0e2b83fa544b Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Thu, 9 Jul 2026 11:33:58 +0200 Subject: [PATCH 05/11] Honor Resize false for themed Apple splash Copy non-resized themed splash images into asset catalog scale slots instead of re-rasterizing them, preserving explicit Resize=false behavior for raster splash assets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/GenerateSplashAssetCatalog.cs | 12 +++++++++-- .../GenerateSplashAssetCatalogTests.cs | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs index f2b61e81266d..6fa9025b3aca 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -80,8 +80,16 @@ private void WriteImages(Resizer resizer) { foreach (var dpi in DpiPath.Ios.SplashImageAsset) { - Log.LogMessage(MessageImportance.Low, $"Splash Screen Asset Catalog Resize: " + dpi); - resizer.Resize(dpi, InputsFile); + if (resizer.Info.Resize) + { + Log.LogMessage(MessageImportance.Low, $"Splash Screen Asset Catalog Resize: " + dpi); + resizer.Resize(dpi, InputsFile); + } + else + { + Log.LogMessage(MessageImportance.Low, $"Splash Screen Asset Catalog Copy: " + dpi); + resizer.CopyFile(dpi, InputsFile); + } } } diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index 3658ab20fdef..afe7128a3edb 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -80,6 +80,27 @@ public void RasterWithoutBaseSizeGeneratesAllReferencedImageFiles() AssertAllImageSetFilesExist(); } + [Fact] + public void RasterWithoutResizePreservesOriginalImageDimensions() + { + var splash = new TaskItem("images/camera.png", new Dictionary + { + ["Resize"] = bool.FalseString, + ["DarkFile"] = "images/camera_color.png", + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png", 1792, 1792); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.png", 1792, 1792); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@3x.png", 1792, 1792); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png", 256, 256); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@2x.png", 256, 256); + AssertFileSize("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@3x.png", 256, 256); + } + [Fact] public void DarkFileWithoutColorDoesNotGenerateColorAsset() { From 4697fcff70c1a31323b4e084fb51e8f3621d4da2 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Fri, 10 Jul 2026 10:39:21 +0200 Subject: [PATCH 06/11] Normalize themed splash DarkFile inputs Hash only the first splash item's DarkFile and pass normalized splash items to the Android resource task so relative dark files resolve consistently. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Maui.Resizetizer.After.targets | 12 ++- .../UnitTests/MauiResizetizerTargetsTests.cs | 75 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index 40273b298c0e..c51aae13fc42 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -329,7 +329,15 @@ <_MauiSplashScreenWithHashes Update="@(_MauiSplashScreenWithHashes)" DarkFile="$([System.IO.Path]::GetFullPath('%(_MauiSplashScreenWithHashes.DarkFile)'))" Condition="'%(_MauiSplashScreenWithHashes.DarkFile)' != ''" /> - <_MauiSplashScreenDarkFile Include="%(_MauiSplashScreenWithHashes.DarkFile)" Condition="'%(_MauiSplashScreenWithHashes.DarkFile)' != ''" /> + + + + <_MauiSplashScreenDarkFilesForHash>@(_MauiSplashScreenWithHashes->'%(DarkFile)', '|') + <_MauiFirstSplashScreenDarkFileForHash Condition="'@(_MauiSplashScreenWithHashes)' != ''">$(_MauiSplashScreenDarkFilesForHash.Split('|')[0]) + + + + <_MauiSplashScreenDarkFile Include="$(_MauiFirstSplashScreenDarkFileForHash)" Condition="'$(_MauiFirstSplashScreenDarkFileForHash)' != ''" /> @@ -464,7 +472,7 @@ diff --git a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs index 05e1b49c4cf5..e87f93d65e75 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs @@ -99,6 +99,64 @@ public void ProcessMauiSplashScreensWarnsWhenThemedAppleSplashIsUnsupported() Assert.Contains("Themed MauiSplashScreen assets require iOS or iPadOS 14.0 or later", output, StringComparison.Ordinal); } + [Fact] + public void ProcessMauiSplashScreensPassesNormalizedSplashItemsToAndroid() + { + var targetFile = Path.Combine( + FindRepositoryRoot(), + "src", + "SingleProject", + "Resizetizer", + "src", + "nuget", + "buildTransitive", + "Microsoft.Maui.Resizetizer.After.targets"); + + var doc = XDocument.Load(targetFile); + var processSplashScreens = Assert.Single(doc.Root.Elements().Where(e => e.Name.LocalName == "Target" && e.Attribute("Name")?.Value == "ProcessMauiSplashScreens")); + var generateAndroid = Assert.Single(processSplashScreens.Elements().Where(e => e.Name.LocalName == "GenerateSplashAndroidResources")); + + Assert.Equal("@(_MauiSplashScreenWithHashes)", generateAndroid.Attribute("MauiSplashScreen")?.Value); + } + + [Fact] + public void ResizetizeCollectItemsHashesOnlyFirstSplashDarkFile() + { + var targetFile = Path.Combine( + FindRepositoryRoot(), + "src", + "SingleProject", + "Resizetizer", + "src", + "nuget", + "buildTransitive", + "Microsoft.Maui.Resizetizer.After.targets"); + var darkFileHashElements = GetDarkFileHashElements(targetFile); + var projectFile = Path.Combine(DestinationDirectory, "FirstDarkFileHash.proj"); + var firstDarkFile = Path.Combine(DestinationDirectory, "first-dark.png"); + var secondDarkFile = Path.Combine(DestinationDirectory, "second-dark.png"); + Directory.CreateDirectory(DestinationDirectory); + + File.WriteAllText(projectFile, + $$""" + + + <_MauiSplashScreenWithHashes Include="first.png" DarkFile="{{firstDarkFile}}" /> + <_MauiSplashScreenWithHashes Include="second.png" DarkFile="{{secondDarkFile}}" /> + + + {{darkFileHashElements}} + + + + + """); + + var output = RunDotnetMSBuild(projectFile); + + Assert.Contains($"DarkFiles={firstDarkFile}", output, StringComparison.Ordinal); + } + static string GetSplashMetadataElements(string targetFile, bool includeWarnings = false) { var doc = XDocument.Load(targetFile); @@ -118,6 +176,23 @@ static string GetSplashMetadataElements(string targetFile, bool includeWarnings return string.Join(Environment.NewLine, elements.Select(e => StripNamespace(e).ToString(SaveOptions.DisableFormatting))); } + static string GetDarkFileHashElements(string targetFile) + { + var doc = XDocument.Load(targetFile); + var collectItems = Assert.Single(doc.Root.Elements().Where(e => e.Name.LocalName == "Target" && e.Attribute("Name")?.Value == "ResizetizeCollectItems")); + var darkFileUpdate = Assert.Single(collectItems.Elements().Where(e => + e.Name.LocalName == "ItemGroup" && + e.Elements().Any(child => child.Name.LocalName == "_MauiSplashScreenWithHashes" && child.Attribute("DarkFile") is not null))); + var firstDarkFileMetadata = Assert.Single(collectItems.Elements().Where(e => + e.Name.LocalName == "PropertyGroup" && + e.Elements().Any(child => child.Name.LocalName == "_MauiFirstSplashScreenDarkFileForHash"))); + var darkFileHashItem = Assert.Single(collectItems.Elements().Where(e => + e.Name.LocalName == "ItemGroup" && + e.Elements().Any(child => child.Name.LocalName == "_MauiSplashScreenDarkFile"))); + + return string.Join(Environment.NewLine, new[] { darkFileUpdate, firstDarkFileMetadata, darkFileHashItem }.Select(e => StripNamespace(e).ToString(SaveOptions.DisableFormatting))); + } + static XElement StripNamespace(XElement element) => new( element.Name.LocalName, From cbef001d30a0e7c23ae19b77c8b87f441ce2e020 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Wed, 15 Jul 2026 10:24:07 +0200 Subject: [PATCH 07/11] Preserve copied splash asset extensions Keep non-resized raster themed Apple splash asset filenames aligned with their copied source bytes, update asset catalog metadata accordingly, and harden target tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/GenerateSplashAssetCatalog.cs | 32 ++++++++----- .../Microsoft.Maui.Resizetizer.After.targets | 2 +- .../GenerateSplashAssetCatalogTests.cs | 47 +++++++++++++++++++ .../UnitTests/MauiResizetizerTargetsTests.cs | 11 ++++- 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs index 6fa9025b3aca..f15c49a98512 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -42,7 +42,7 @@ public override bool Execute() WriteImages(lightResizer); WriteImages(new Resizer(darkInfo, IntermediateOutputPath, this)); - WriteImageSet(); + WriteImageSet(lightInfo, darkInfo); if (info.Color is not null || info.DarkColor is not null) { if (info.Color is null && info.DarkColor is not null) @@ -68,7 +68,7 @@ private static ResizeImageInfo CloneForAsset(ResizeImageInfo info, string alias) new ResizeImageInfo { ItemSpec = info.ItemSpec, - Alias = alias + Resizer.RasterFileExtension, + Alias = alias + GetAssetExtension(info), Filename = info.Filename, BaseSize = info.BaseSize, Resize = info.Resize, @@ -76,6 +76,11 @@ private static ResizeImageInfo CloneForAsset(ResizeImageInfo info, string alias) Color = info.Color, }; + private static string GetAssetExtension(ResizeImageInfo info) => + !info.Resize && !info.IsVector && !string.IsNullOrEmpty(info.OutputExtension) + ? info.OutputExtension + : Resizer.RasterFileExtension; + private void WriteImages(Resizer resizer) { foreach (var dpi in DpiPath.Ios.SplashImageAsset) @@ -93,7 +98,7 @@ private void WriteImages(Resizer resizer) } } - private void WriteImageSet() + private void WriteImageSet(ResizeImageInfo lightInfo, ResizeImageInfo darkInfo) { var imageSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashImageSetPath); Directory.CreateDirectory(imageSetPath); @@ -105,22 +110,22 @@ private void WriteImageSet() "images": [ { "idiom": "universal", - "filename": "MauiSplashImage.png", + "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[0])}}", "scale": "1x" }, { "idiom": "universal", - "filename": "MauiSplashImage@2x.png", + "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[1])}}", "scale": "2x" }, { "idiom": "universal", - "filename": "MauiSplashImage@3x.png", + "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[2])}}", "scale": "3x" }, { "idiom": "universal", - "filename": "MauiSplashImage.png", + "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[0])}}", "scale": "1x", "appearances": [ { @@ -131,7 +136,7 @@ private void WriteImageSet() }, { "idiom": "universal", - "filename": "MauiSplashImage@2x.png", + "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[1])}}", "scale": "2x", "appearances": [ { @@ -142,7 +147,7 @@ private void WriteImageSet() }, { "idiom": "universal", - "filename": "MauiSplashImage@3x.png", + "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[2])}}", "scale": "3x", "appearances": [ { @@ -153,7 +158,7 @@ private void WriteImageSet() }, { "idiom": "universal", - "filename": "MauiSplashImageDark.png", + "filename": "{{GetAssetFilename(darkInfo, DpiPath.Ios.SplashImageAsset[0])}}", "scale": "1x", "appearances": [ { @@ -164,7 +169,7 @@ private void WriteImageSet() }, { "idiom": "universal", - "filename": "MauiSplashImageDark@2x.png", + "filename": "{{GetAssetFilename(darkInfo, DpiPath.Ios.SplashImageAsset[1])}}", "scale": "2x", "appearances": [ { @@ -175,7 +180,7 @@ private void WriteImageSet() }, { "idiom": "universal", - "filename": "MauiSplashImageDark@3x.png", + "filename": "{{GetAssetFilename(darkInfo, DpiPath.Ios.SplashImageAsset[2])}}", "scale": "3x", "appearances": [ { @@ -193,6 +198,9 @@ private void WriteImageSet() """); } + private static string GetAssetFilename(ResizeImageInfo info, DpiPath dpi) => + info.OutputName + dpi.FileSuffix + info.OutputExtension; + private void WriteColorSet(SKColor lightColor, SKColor darkColor) { var colorSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashColorSetPath); diff --git a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets index c51aae13fc42..0adbb8238f2c 100644 --- a/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets +++ b/src/SingleProject/Resizetizer/src/nuget/buildTransitive/Microsoft.Maui.Resizetizer.After.targets @@ -483,7 +483,7 @@ - + + { + ["Resize"] = bool.FalseString, + ["DarkFile"] = darkFile, + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@2x.jpg"); + AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png"); + AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png"); + + using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset", "Contents.json"))); + var filenames = imageJson.RootElement.GetProperty("images").EnumerateArray() + .Select(image => image.GetProperty("filename").GetString()) + .ToArray(); + Assert.Contains("MauiSplashImage.jpg", filenames); + Assert.Contains("MauiSplashImage@2x.jpg", filenames); + Assert.Contains("MauiSplashImageDark.jpg", filenames); + Assert.Contains("MauiSplashImageDark@2x.jpg", filenames); + Assert.DoesNotContain("MauiSplashImage.png", filenames); + Assert.DoesNotContain("MauiSplashImageDark.png", filenames); + } + [Fact] public void DarkFileWithoutColorDoesNotGenerateColorAsset() { @@ -257,5 +291,18 @@ static string GetAppearanceValue(JsonElement element) => element.TryGetProperty("appearances", out var appearances) && appearances.GetArrayLength() > 0 ? appearances[0].GetProperty("value").GetString() : null; + + string CreateJpegImage(string filename, string sourceFile) + { + Directory.CreateDirectory(DestinationDirectory); + var destination = Path.Combine(DestinationDirectory, filename); + using var bitmap = SKBitmap.Decode(sourceFile); + using var image = SKImage.FromBitmap(bitmap); + using var data = image.Encode(SKEncodedImageFormat.Jpeg, 90); + using var stream = File.OpenWrite(destination); + data.SaveTo(stream); + + return destination; + } } } diff --git a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs index e87f93d65e75..069d9cdb5019 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs @@ -201,6 +201,7 @@ static XElement StripNamespace(XElement element) => string RunDotnetMSBuild(string projectFile) { + const int timeoutMilliseconds = 60_000; var startInfo = new ProcessStartInfo { FileName = GetDotNetHost(), @@ -220,7 +221,15 @@ string RunDotnetMSBuild(string projectFile) var outputTask = process.StandardOutput.ReadToEndAsync(); var errorTask = process.StandardError.ReadToEndAsync(); - process.WaitForExit(); + if (!process.WaitForExit(timeoutMilliseconds)) + { + process.Kill(entireProcessTree: true); + process.WaitForExit(); + var timedOutOutput = outputTask.GetAwaiter().GetResult(); + var timedOutError = errorTask.GetAwaiter().GetResult(); + Assert.Fail($"MSBuild timed out after {timeoutMilliseconds}ms.\n{timedOutOutput}{timedOutError}"); + } + var output = outputTask.GetAwaiter().GetResult(); var error = errorTask.GetAwaiter().GetResult(); From bbff84ae439983a9e18a01cc4a1586b7f239fd6a Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Mon, 3 Aug 2026 11:39:59 +0200 Subject: [PATCH 08/11] Fix themed splash test isolation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83d5a2cc-c4eb-4be5-ad04-e89a051c2989 --- .../GenerateSplashAssetCatalogTests.cs | 77 ++++++++++--------- .../UnitTests/MauiResizetizerTargetsTests.cs | 50 ++---------- .../UnitTests/Resizetizer.UnitTests.csproj | 1 + 3 files changed, 50 insertions(+), 78 deletions(-) diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index fb14ec12f64d..6f39af4fabdc 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -104,35 +104,44 @@ public void RasterWithoutResizePreservesOriginalImageDimensions() [Fact] public void NonPngRasterWithoutResizeUsesMatchingAssetFilenames() { - var lightFile = CreateJpegImage("camera.jpg", "images/camera.png"); - var darkFile = CreateJpegImage("camera_color.jpg", "images/camera_color.png"); - var splash = new TaskItem(lightFile, new Dictionary + var inputDirectory = Path.Combine(Path.GetTempPath(), "Microsoft.Maui.Resizetizer.Tests", nameof(GenerateSplashAssetCatalogTests), Path.GetRandomFileName()); + try { - ["Resize"] = bool.FalseString, - ["DarkFile"] = darkFile, - }); - - var task = GetNewTask(splash); - var success = task.Execute(); - Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); - - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.jpg"); - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.jpg"); - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.jpg"); - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@2x.jpg"); - AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png"); - AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png"); - - using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset", "Contents.json"))); - var filenames = imageJson.RootElement.GetProperty("images").EnumerateArray() - .Select(image => image.GetProperty("filename").GetString()) - .ToArray(); - Assert.Contains("MauiSplashImage.jpg", filenames); - Assert.Contains("MauiSplashImage@2x.jpg", filenames); - Assert.Contains("MauiSplashImageDark.jpg", filenames); - Assert.Contains("MauiSplashImageDark@2x.jpg", filenames); - Assert.DoesNotContain("MauiSplashImage.png", filenames); - Assert.DoesNotContain("MauiSplashImageDark.png", filenames); + var lightFile = CopyImageWithExtension(inputDirectory, "camera.jpg", "images/camera.png"); + var darkFile = CopyImageWithExtension(inputDirectory, "camera_color.jpg", "images/camera_color.png"); + var splash = new TaskItem(lightFile, new Dictionary + { + ["Resize"] = bool.FalseString, + ["DarkFile"] = darkFile, + }); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@2x.jpg"); + AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png"); + AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png"); + + using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset", "Contents.json"))); + var filenames = imageJson.RootElement.GetProperty("images").EnumerateArray() + .Select(image => image.GetProperty("filename").GetString()) + .ToArray(); + Assert.Contains("MauiSplashImage.jpg", filenames); + Assert.Contains("MauiSplashImage@2x.jpg", filenames); + Assert.Contains("MauiSplashImageDark.jpg", filenames); + Assert.Contains("MauiSplashImageDark@2x.jpg", filenames); + Assert.DoesNotContain("MauiSplashImage.png", filenames); + Assert.DoesNotContain("MauiSplashImageDark.png", filenames); + } + finally + { + if (Directory.Exists(inputDirectory)) + Directory.Delete(inputDirectory, recursive: true); + } } [Fact] @@ -292,15 +301,11 @@ static string GetAppearanceValue(JsonElement element) => ? appearances[0].GetProperty("value").GetString() : null; - string CreateJpegImage(string filename, string sourceFile) + static string CopyImageWithExtension(string inputDirectory, string filename, string sourceFile) { - Directory.CreateDirectory(DestinationDirectory); - var destination = Path.Combine(DestinationDirectory, filename); - using var bitmap = SKBitmap.Decode(sourceFile); - using var image = SKImage.FromBitmap(bitmap); - using var data = image.Encode(SKEncodedImageFormat.Jpeg, 90); - using var stream = File.OpenWrite(destination); - data.SaveTo(stream); + Directory.CreateDirectory(inputDirectory); + var destination = Path.Combine(inputDirectory, filename); + File.Copy(sourceFile, destination); return destination; } diff --git a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs index 069d9cdb5019..64b31f3df9c4 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs @@ -18,15 +18,7 @@ public MauiResizetizerTargetsTests(ITestOutputHelper output) [Fact] public void ProcessMauiSplashScreensSelectsFirstSplashForThemedAppleMetadata() { - var targetFile = Path.Combine( - FindRepositoryRoot(), - "src", - "SingleProject", - "Resizetizer", - "src", - "nuget", - "buildTransitive", - "Microsoft.Maui.Resizetizer.After.targets"); + var targetFile = GetTargetFile(); var metadataElements = GetSplashMetadataElements(targetFile); var projectFile = Path.Combine(DestinationDirectory, "SelectFirstSplash.proj"); Directory.CreateDirectory(DestinationDirectory); @@ -63,15 +55,7 @@ public void ProcessMauiSplashScreensSelectsFirstSplashForThemedAppleMetadata() [Fact] public void ProcessMauiSplashScreensWarnsWhenThemedAppleSplashIsUnsupported() { - var targetFile = Path.Combine( - FindRepositoryRoot(), - "src", - "SingleProject", - "Resizetizer", - "src", - "nuget", - "buildTransitive", - "Microsoft.Maui.Resizetizer.After.targets"); + var targetFile = GetTargetFile(); var metadataAndWarnings = GetSplashMetadataElements(targetFile, includeWarnings: true); var projectFile = Path.Combine(DestinationDirectory, "UnsupportedThemedSplash.proj"); Directory.CreateDirectory(DestinationDirectory); @@ -102,15 +86,7 @@ public void ProcessMauiSplashScreensWarnsWhenThemedAppleSplashIsUnsupported() [Fact] public void ProcessMauiSplashScreensPassesNormalizedSplashItemsToAndroid() { - var targetFile = Path.Combine( - FindRepositoryRoot(), - "src", - "SingleProject", - "Resizetizer", - "src", - "nuget", - "buildTransitive", - "Microsoft.Maui.Resizetizer.After.targets"); + var targetFile = GetTargetFile(); var doc = XDocument.Load(targetFile); var processSplashScreens = Assert.Single(doc.Root.Elements().Where(e => e.Name.LocalName == "Target" && e.Attribute("Name")?.Value == "ProcessMauiSplashScreens")); @@ -122,15 +98,7 @@ public void ProcessMauiSplashScreensPassesNormalizedSplashItemsToAndroid() [Fact] public void ResizetizeCollectItemsHashesOnlyFirstSplashDarkFile() { - var targetFile = Path.Combine( - FindRepositoryRoot(), - "src", - "SingleProject", - "Resizetizer", - "src", - "nuget", - "buildTransitive", - "Microsoft.Maui.Resizetizer.After.targets"); + var targetFile = GetTargetFile(); var darkFileHashElements = GetDarkFileHashElements(targetFile); var projectFile = Path.Combine(DestinationDirectory, "FirstDarkFileHash.proj"); var firstDarkFile = Path.Combine(DestinationDirectory, "first-dark.png"); @@ -250,14 +218,12 @@ static string GetDotNetHost() : "dotnet"; } - static string FindRepositoryRoot() + static string GetTargetFile() { - var directory = new DirectoryInfo(AppContext.BaseDirectory); + var targetFile = Path.Combine(AppContext.BaseDirectory, "Microsoft.Maui.Resizetizer.After.targets"); + Assert.True(File.Exists(targetFile), $"Expected target file to be copied to test output: {targetFile}"); - while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Microsoft.Maui.sln"))) - directory = directory.Parent; - - return directory?.FullName ?? throw new DirectoryNotFoundException("Could not find the repository root."); + return targetFile; } } } diff --git a/src/SingleProject/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj b/src/SingleProject/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj index 87fb8cdf65fa..92dd03795da9 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj +++ b/src/SingleProject/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj @@ -26,6 +26,7 @@ + From 2960487ca2717f2bf5fd64d0ed00d85e82dd7664 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Mon, 3 Aug 2026 17:41:49 +0200 Subject: [PATCH 09/11] Fix themed splash Helix cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83d5a2cc-c4eb-4be5-ad04-e89a051c2989 --- .../src/GenerateSplashAssetCatalog.cs | 8 +++ .../GenerateSplashAssetCatalogTests.cs | 68 +++++++++---------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs index f15c49a98512..23a257f4b5bb 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -32,6 +32,7 @@ public override bool Execute() return true; CleanStoryboard(); + CleanImageSet(); var info = ResizeImageInfo.Parse(splash); var lightInfo = CloneForAsset(info, DpiPath.Ios.SplashImageName); @@ -278,6 +279,13 @@ private void CleanStoryboard() File.Delete(storyboard); } + private void CleanImageSet() + { + var imageSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashImageSetPath); + if (Directory.Exists(imageSetPath)) + Directory.Delete(imageSetPath, recursive: true); + } + private void DeleteColorSet() { var colorSetPath = Path.Combine(IntermediateOutputPath, DpiPath.Ios.SplashColorSetPath); diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index 6f39af4fabdc..c299af3bf309 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -105,43 +105,39 @@ public void RasterWithoutResizePreservesOriginalImageDimensions() public void NonPngRasterWithoutResizeUsesMatchingAssetFilenames() { var inputDirectory = Path.Combine(Path.GetTempPath(), "Microsoft.Maui.Resizetizer.Tests", nameof(GenerateSplashAssetCatalogTests), Path.GetRandomFileName()); - try + var lightFile = CopyImageWithExtension(inputDirectory, "camera.jpg", "images/camera.png"); + var darkFile = CopyImageWithExtension(inputDirectory, "camera_color.jpg", "images/camera_color.png"); + var splash = new TaskItem(lightFile, new Dictionary { - var lightFile = CopyImageWithExtension(inputDirectory, "camera.jpg", "images/camera.png"); - var darkFile = CopyImageWithExtension(inputDirectory, "camera_color.jpg", "images/camera_color.png"); - var splash = new TaskItem(lightFile, new Dictionary - { - ["Resize"] = bool.FalseString, - ["DarkFile"] = darkFile, - }); - - var task = GetNewTask(splash); - var success = task.Execute(); - Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); - - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.jpg"); - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.jpg"); - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.jpg"); - AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@2x.jpg"); - AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png"); - AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png"); - - using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset", "Contents.json"))); - var filenames = imageJson.RootElement.GetProperty("images").EnumerateArray() - .Select(image => image.GetProperty("filename").GetString()) - .ToArray(); - Assert.Contains("MauiSplashImage.jpg", filenames); - Assert.Contains("MauiSplashImage@2x.jpg", filenames); - Assert.Contains("MauiSplashImageDark.jpg", filenames); - Assert.Contains("MauiSplashImageDark@2x.jpg", filenames); - Assert.DoesNotContain("MauiSplashImage.png", filenames); - Assert.DoesNotContain("MauiSplashImageDark.png", filenames); - } - finally - { - if (Directory.Exists(inputDirectory)) - Directory.Delete(inputDirectory, recursive: true); - } + ["Resize"] = bool.FalseString, + ["DarkFile"] = darkFile, + }); + var imageSetPath = Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset"); + Directory.CreateDirectory(imageSetPath); + File.WriteAllText(Path.Combine(imageSetPath, "MauiSplashImage.png"), "stale"); + File.WriteAllText(Path.Combine(imageSetPath, "MauiSplashImageDark.png"), "stale"); + + var task = GetNewTask(splash); + var success = task.Execute(); + Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message); + + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage@2x.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.jpg"); + AssertFileExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark@2x.jpg"); + AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImage.png"); + AssertFileNotExists("Assets.xcassets/MauiSplashImage.imageset/MauiSplashImageDark.png"); + + using var imageJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashImage.imageset", "Contents.json"))); + var filenames = imageJson.RootElement.GetProperty("images").EnumerateArray() + .Select(image => image.GetProperty("filename").GetString()) + .ToArray(); + Assert.Contains("MauiSplashImage.jpg", filenames); + Assert.Contains("MauiSplashImage@2x.jpg", filenames); + Assert.Contains("MauiSplashImageDark.jpg", filenames); + Assert.Contains("MauiSplashImageDark@2x.jpg", filenames); + Assert.DoesNotContain("MauiSplashImage.png", filenames); + Assert.DoesNotContain("MauiSplashImageDark.png", filenames); } [Fact] From 2e194556db9179ecc609650224d363e7c2ba67d4 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Mon, 3 Aug 2026 18:12:25 +0200 Subject: [PATCH 10/11] Retry Resizetizer test cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83d5a2cc-c4eb-4be5-ad04-e89a051c2989 --- .../Resizetizer/test/UnitTests/BaseTest.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/SingleProject/Resizetizer/test/UnitTests/BaseTest.cs b/src/SingleProject/Resizetizer/test/UnitTests/BaseTest.cs index 668a64c46a12..e6df906b37f8 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/BaseTest.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/BaseTest.cs @@ -42,7 +42,35 @@ public virtual void Dispose() Output.WriteLine($"Cleaning up directories={DeleteDirectory}"); if (Directory.Exists(DeleteDirectory)) - Directory.Delete(DeleteDirectory, true); + DeleteDirectoryWithRetries(DeleteDirectory); + } + + void DeleteDirectoryWithRetries(string directory) + { + const int attempts = 3; + + for (var attempt = 1; ; attempt++) + { + Exception cleanupException; + try + { + Directory.Delete(directory, true); + return; + } + catch (IOException ex) when (attempt < attempts) + { + cleanupException = ex; + } + catch (UnauthorizedAccessException ex) when (attempt < attempts) + { + cleanupException = ex; + } + + Output.WriteLine($"Retrying cleanup for {directory} after attempt {attempt}: {cleanupException.Message}"); + GC.Collect(); + GC.WaitForPendingFinalizers(); + System.Threading.Thread.Sleep(100); + } } protected void AssertFileSize(string file, int width, int height) From feaac60c8b378cee1c9b648f4b3810ab7ab070a9 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:57:29 +0200 Subject: [PATCH 11/11] Fix iOS themed splash color opacity Asset catalog components written as bare 1 values are interpreted by actool as 1/255. Emit a floating-point 1.0 value for fully opaque and full-intensity components, and cover the generated alpha values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Resizetizer/src/GenerateSplashAssetCatalog.cs | 4 +++- .../UnitTests/GenerateSplashAssetCatalogTests.cs | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs index 23a257f4b5bb..f492d0b74906 100644 --- a/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs +++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs @@ -270,7 +270,9 @@ private void WriteColorSet(SKColor lightColor, SKColor darkColor) } private static string ToComponent(byte value) => - (value / (float)byte.MaxValue).ToString("0.#######", CultureInfo.InvariantCulture); + value == byte.MaxValue + ? "1.0" + : (value / (float)byte.MaxValue).ToString("0.#######", CultureInfo.InvariantCulture); private void CleanStoryboard() { diff --git a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs index c299af3bf309..24ace2f5df30 100644 --- a/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs +++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; @@ -59,7 +59,9 @@ public void DarkMetadataGeneratesImageAndColorAssetCatalogs() var colors = colorJson.RootElement.GetProperty("colors").EnumerateArray().ToArray(); Assert.Contains(colors, color => !color.TryGetProperty("appearances", out _)); Assert.Contains(colors, color => GetAppearanceValue(color) == "light"); - var darkColor = Assert.Single(colors.Where(color => GetAppearanceValue(color) == "dark")); + Assert.All(colors, color => + Assert.Equal("1.0", color.GetProperty("color").GetProperty("components").GetProperty("alpha").GetString())); + var darkColor = Assert.Single(colors, color => GetAppearanceValue(color) == "dark"); Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("green").GetString()); Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("blue").GetString()); @@ -227,9 +229,9 @@ public void DarkColorOnlyGeneratesImageSetAndColorAssetWithWarning() using var colorJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(DestinationDirectory, "Assets.xcassets", "MauiSplashColor.colorset", "Contents.json"))); var colors = colorJson.RootElement.GetProperty("colors").EnumerateArray().ToArray(); - var anyColor = Assert.Single(colors.Where(color => !color.TryGetProperty("appearances", out _))); - var darkColor = Assert.Single(colors.Where(color => GetAppearanceValue(color) == "dark")); - Assert.Equal("1", anyColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); + var anyColor = Assert.Single(colors, color => !color.TryGetProperty("appearances", out _)); + var darkColor = Assert.Single(colors, color => GetAppearanceValue(color) == "dark"); + Assert.Equal("1.0", anyColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); Assert.Equal("0", darkColor.GetProperty("color").GetProperty("components").GetProperty("red").GetString()); }