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 b38fb37cd5d6..abf17614405a 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..fc9fa6920341 100644
--- a/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs
+++ b/src/SingleProject/Resizetizer/src/GenerateSplashAndroidResources.cs
@@ -43,11 +43,33 @@ 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(GetDarkOutputAlias(info));
+ 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)
+ {
+ CleanStaleNightImageResources(darkResizer.Info.Resize);
+ 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 +85,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 +97,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)
{
- var dir = Path.Combine(IntermediateOutputPath, "values");
+ 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, directory);
Directory.CreateDirectory(dir);
var colorsFile = Path.Combine(dir, "maui_colors.xml");
@@ -95,25 +127,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 +162,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 +196,68 @@ 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);
+ }
+
+ 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",
+ };
+
+ 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
new file mode 100644
index 000000000000..f492d0b74906
--- /dev/null
+++ b/src/SingleProject/Resizetizer/src/GenerateSplashAssetCatalog.cs
@@ -0,0 +1,303 @@
+#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();
+ CleanImageSet();
+
+ 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(lightInfo, darkInfo);
+ 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);
+ }
+ else
+ {
+ DeleteColorSet();
+ }
+
+ 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 + GetAssetExtension(info),
+ Filename = info.Filename,
+ BaseSize = info.BaseSize,
+ Resize = info.Resize,
+ TintColor = info.TintColor,
+ 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)
+ {
+ 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);
+ }
+ }
+ }
+
+ private void WriteImageSet(ResizeImageInfo lightInfo, ResizeImageInfo darkInfo)
+ {
+ 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": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[0])}}",
+ "scale": "1x"
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[1])}}",
+ "scale": "2x"
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[2])}}",
+ "scale": "3x"
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[0])}}",
+ "scale": "1x",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ]
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[1])}}",
+ "scale": "2x",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ]
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(lightInfo, DpiPath.Ios.SplashImageAsset[2])}}",
+ "scale": "3x",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "light"
+ }
+ ]
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(darkInfo, DpiPath.Ios.SplashImageAsset[0])}}",
+ "scale": "1x",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(darkInfo, DpiPath.Ios.SplashImageAsset[1])}}",
+ "scale": "2x",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ },
+ {
+ "idiom": "universal",
+ "filename": "{{GetAssetFilename(darkInfo, DpiPath.Ios.SplashImageAsset[2])}}",
+ "scale": "3x",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ }
+ ],
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+ }
+ """);
+ }
+
+ 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);
+ 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 == byte.MaxValue
+ ? "1.0"
+ : (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);
+ }
+
+ 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);
+ if (Directory.Exists(colorSetPath))
+ Directory.Delete(colorSetPath, recursive: true);
+ }
+
+ 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 79913d4b6bba..604aadbdcf50 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; }
@@ -112,6 +125,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;
@@ -128,6 +151,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;
+ }
+
var monoFile = image.GetMetadata("MonochromeFile");
if (!string.IsNullOrEmpty(monoFile))
{
@@ -150,5 +183,25 @@ 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,
+ MonochromeFilename = MonochromeFilename,
+ };
+ }
}
}
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 d3fd5474e8bb..4786cd71357a 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" />
+
+
@@ -257,7 +261,8 @@
-
+
+
@@ -337,6 +342,23 @@
+
+ <_MauiSplashScreenWithHashes Update="@(_MauiSplashScreenWithHashes)" DarkFile="$([System.IO.Path]::GetFullPath('%(_MauiSplashScreenWithHashes.DarkFile)'))" Condition="'%(_MauiSplashScreenWithHashes.DarkFile)' != ''" />
+
+
+
+ <_MauiSplashScreenDarkFilesForHash>@(_MauiSplashScreenWithHashes->'%(DarkFile)', '|')
+ <_MauiFirstSplashScreenDarkFileForHash Condition="'@(_MauiSplashScreenWithHashes)' != ''">$(_MauiSplashScreenDarkFilesForHash.Split('|')[0])
+
+
+
+ <_MauiSplashScreenDarkFile Include="$(_MauiFirstSplashScreenDarkFileForHash)" Condition="'$(_MauiFirstSplashScreenDarkFileForHash)' != ''" />
+
+
+
+
+
+
@@ -451,10 +473,36 @@
<_MauiHasSplashScreens>false
<_MauiHasSplashScreens Condition="'@(MauiSplashScreen->Count())' != '0'">true
+ <_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
+
+
+
+ <_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
+
+
+
@@ -481,8 +529,13 @@
-
-
+
+
+
+
<_MauiSplashScreenWithHashesInFilename
Include="@(_MauiSplashScreenWithHashes->HasMetadata('Link'))"
@@ -494,22 +547,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)" />
@@ -524,6 +593,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 +26,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 +56,220 @@ 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 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()
+ {
+ 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()
+ {
+ 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 +510,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..24ace2f5df30
--- /dev/null
+++ b/src/SingleProject/Resizetizer/test/UnitTests/GenerateSplashAssetCatalogTests.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using SkiaSharp;
+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");
+ 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());
+ }
+
+ [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 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 NonPngRasterWithoutResizeUsesMatchingAssetFilenames()
+ {
+ var inputDirectory = Path.Combine(Path.GetTempPath(), "Microsoft.Maui.Resizetizer.Tests", nameof(GenerateSplashAssetCatalogTests), Path.GetRandomFileName());
+ 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 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]
+ 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 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()
+ {
+ 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()
+ {
+ 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, 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());
+ }
+
+ [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;
+
+ static string CopyImageWithExtension(string inputDirectory, string filename, string sourceFile)
+ {
+ Directory.CreateDirectory(inputDirectory);
+ var destination = Path.Combine(inputDirectory, filename);
+ File.Copy(sourceFile, destination);
+
+ return destination;
+ }
+ }
+}
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/MauiResizetizerTargetsTests.cs b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs
new file mode 100644
index 000000000000..64b31f3df9c4
--- /dev/null
+++ b/src/SingleProject/Resizetizer/test/UnitTests/MauiResizetizerTargetsTests.cs
@@ -0,0 +1,229 @@
+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 = GetTargetFile();
+ 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=splash.png; DarkFile=first-dark.png; FirstColor=; Themed=true; Color=", output, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ProcessMauiSplashScreensWarnsWhenThemedAppleSplashIsUnsupported()
+ {
+ var targetFile = GetTargetFile();
+ 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);
+ }
+
+ [Fact]
+ public void ProcessMauiSplashScreensPassesNormalizedSplashItemsToAndroid()
+ {
+ 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"));
+ 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 = GetTargetFile();
+ 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);
+ var processSplashScreens = Assert.Single(doc.Root.Elements().Where(e => e.Name.LocalName == "Target" && e.Attribute("Name")?.Value == "ProcessMauiSplashScreens"));
+ var propertyGroups = processSplashScreens
+ .Elements()
+ .Where(e => e.Name.LocalName == "PropertyGroup")
+ .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
+ ? 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)));
+ }
+
+ 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,
+ 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)
+ {
+ const int timeoutMilliseconds = 60_000;
+ 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 outputTask = process.StandardOutput.ReadToEndAsync();
+ var errorTask = process.StandardError.ReadToEndAsync();
+ 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();
+
+ 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 GetTargetFile()
+ {
+ 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}");
+
+ return targetFile;
+ }
+ }
+}
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]
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 @@
+