Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions src/SingleProject/Resizetizer/src/AndroidAdaptiveIconGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ public AndroidAdaptiveIconGenerator(ResizeImageInfo info, string appIconName, st
<monochrome android:drawable=""@mipmap/{name}_foreground"" />
</adaptive-icon>";

const string AdaptiveIconDrawableWithMonochromeXml =
@"<?xml version=""1.0"" encoding=""utf-8""?>
<adaptive-icon xmlns:android=""http://schemas.android.com/apk/res/android"">
<background android:drawable=""@mipmap/{name}_background""/>
<foreground android:drawable=""@mipmap/{name}_foreground""/>
<monochrome android:drawable=""@mipmap/{name}_monochrome"" />
</adaptive-icon>";

bool HasMonochromeFile => !string.IsNullOrEmpty(Info.MonochromeFilename);

public IEnumerable<ResizedImageInfo> Generate()
{
var sw = new Stopwatch();
Expand All @@ -42,6 +52,8 @@ public IEnumerable<ResizedImageInfo> Generate()

ProcessBackground(results, fullIntermediateOutputPath);
ProcessForeground(results, fullIntermediateOutputPath);
if (HasMonochromeFile)
ProcessMonochrome(results, fullIntermediateOutputPath);
ProcessAdaptiveIcon(results, fullIntermediateOutputPath);

sw.Stop();
Expand Down Expand Up @@ -138,29 +150,71 @@ void ProcessForeground(List<ResizedImageInfo> results, DirectoryInfo fullInterme
}
}

void ProcessMonochrome(List<ResizedImageInfo> results, DirectoryInfo fullIntermediateOutputPath)
{
var monochromeFile = Info.MonochromeFilename;
var (monochromeExists, monochromeModified) = Utils.FileExists(monochromeFile);
var monochromeDestFilename = AppIconName + "_monochrome.png";

if (monochromeExists)
Logger.Log("Converting Monochrome SVG to PNG: " + monochromeFile);
else
Logger.Log("Monochrome was not found (will manufacture): " + monochromeFile);

foreach (var dpi in DpiPath.Android.AppIconParts)
{
var dir = Path.Combine(fullIntermediateOutputPath.FullName, dpi.Path);
var destination = Path.Combine(dir, monochromeDestFilename);
var (destinationExists, destinationModified) = Utils.FileExists(destination);
Directory.CreateDirectory(dir);

if (destinationModified > monochromeModified)
{
Logger.Log($"Skipping `{monochromeFile}` => `{destination}` file is up to date.");
results.Add(new ResizedImageInfo { Dpi = dpi, Filename = destination });
continue;
}

Logger.Log($"App Icon Monochrome Part: " + destination);

if (monochromeExists)
{
var tools = SkiaSharpTools.Create(Info.MonochromeIsVector, Info.MonochromeFilename, dpi.Size, null, null, Logger);
tools.Resize(dpi, destination, dpiSizeIsAbsolute: true);
}
else
{
var tools = SkiaSharpTools.CreateImaginary(null, Logger);
tools.Resize(dpi, destination);
}

results.Add(new ResizedImageInfo { Dpi = dpi, Filename = destination });
}
}

void ProcessAdaptiveIcon(List<ResizedImageInfo> results, DirectoryInfo fullIntermediateOutputPath)
{
var dir = Path.Combine(fullIntermediateOutputPath.FullName, "mipmap-anydpi-v26");
var adaptiveIconDestination = Path.Combine(dir, AppIconName + ".xml");
var adaptiveIconRoundDestination = Path.Combine(dir, AppIconName + "_round.xml");
Directory.CreateDirectory(dir);

if (File.Exists(adaptiveIconDestination) && File.Exists(adaptiveIconRoundDestination))
{
results.Add(new ResizedImageInfo { Dpi = new DpiPath("mipmap-anydpi-v26", 1), Filename = adaptiveIconDestination });
results.Add(new ResizedImageInfo { Dpi = new DpiPath("mipmap-anydpi-v26", 1, "_round"), Filename = adaptiveIconRoundDestination });
return;
}

var adaptiveIconXmlStr = AdaptiveIconDrawableXml
var adaptiveIconXmlStr = (HasMonochromeFile ? AdaptiveIconDrawableWithMonochromeXml : AdaptiveIconDrawableXml)
.Replace("{name}", AppIconName);

// Write out the adaptive icon xml drawables
File.WriteAllText(adaptiveIconDestination, adaptiveIconXmlStr);
File.WriteAllText(adaptiveIconRoundDestination, adaptiveIconXmlStr);
// Only write when content changed to avoid unnecessary timestamp updates
WriteFileIfChanged(adaptiveIconDestination, adaptiveIconXmlStr);
WriteFileIfChanged(adaptiveIconRoundDestination, adaptiveIconXmlStr);

results.Add(new ResizedImageInfo { Dpi = new DpiPath("mipmap-anydpi-v26", 1), Filename = adaptiveIconDestination });
results.Add(new ResizedImageInfo { Dpi = new DpiPath("mipmap-anydpi-v26", 1, "_round"), Filename = adaptiveIconRoundDestination });
}

static void WriteFileIfChanged(string path, string content)
{
if (File.Exists(path) && File.ReadAllText(path) == content)
return;
File.WriteAllText(path, content);
}
}
}
14 changes: 14 additions & 0 deletions src/SingleProject/Resizetizer/src/ResizeImageInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ internal class ResizeImageInfo

public double ForegroundScale { get; set; } = 1.0;

public string? MonochromeFilename { get; set; }

public bool MonochromeIsVector => IsVectorFilename(MonochromeFilename);

private static bool IsVectorFilename(string? filename)
=> IsVectorExtension(Path.GetExtension(filename));

Expand Down Expand Up @@ -124,6 +128,16 @@ public static ResizeImageInfo Parse(ITaskItem image)
info.ForegroundFilename = fgFileInfo.FullName;
}

var monoFile = image.GetMetadata("MonochromeFile");
if (!string.IsNullOrEmpty(monoFile))
{
var monoFileInfo = new FileInfo(monoFile);
if (!monoFileInfo.Exists)
throw new FileNotFoundException("Unable to find monochrome file: " + monoFileInfo.FullName, monoFileInfo.FullName);

info.MonochromeFilename = monoFileInfo.FullName;
}

// make sure the image is a foreground if this is an icon
if (info.IsAppIcon && string.IsNullOrEmpty(info.ForegroundFilename))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,82 @@ public void NonExistantFilesAreDeleted()

// AssertFileSize(DestinationFilename, exWidth, exHeight);
//}

[Fact]
public void AppIconWithMonochromeFileGeneratesMonochromeLayer()
{
var items = new[]
{
new TaskItem("images/dotnet_background.svg", new Dictionary<string, string>
{
["IsAppIcon"] = bool.TrueString,
["ForegroundFile"] = "images/appicon.svg",
["MonochromeFile"] = "images/camera.svg",
}),
};

var task = GetNewTask(items);
var success = task.Execute();
Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message);

// Monochrome PNG should be generated at each density
AssertFileExists("mipmap-mdpi/dotnet_background_monochrome.png");
AssertFileExists("mipmap-xhdpi/dotnet_background_monochrome.png");

// Adaptive icon XML should reference the monochrome layer
AssertFileContains("mipmap-anydpi-v26/dotnet_background.xml",
"<monochrome android:drawable=\"@mipmap/dotnet_background_monochrome\"");
AssertFileContains("mipmap-anydpi-v26/dotnet_background_round.xml",
"<monochrome android:drawable=\"@mipmap/dotnet_background_monochrome\"");
}

[Fact]
public void AppIconWithoutMonochromeFileFallsBackToForeground()
{
var items = new[]
{
new TaskItem("images/dotnet_background.svg", new Dictionary<string, string>
{
["IsAppIcon"] = bool.TrueString,
["ForegroundFile"] = "images/appicon.svg",
}),
};

var task = GetNewTask(items);
var success = task.Execute();
Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message);

// No monochrome PNG should be generated
AssertFileNotExists("mipmap-mdpi/dotnet_background_monochrome.png");

// Adaptive icon XML should reference the foreground as monochrome (backwards compat)
AssertFileContains("mipmap-anydpi-v26/dotnet_background.xml",
"<monochrome android:drawable=\"@mipmap/dotnet_background_foreground\"");
AssertFileContains("mipmap-anydpi-v26/dotnet_background_round.xml",
"<monochrome android:drawable=\"@mipmap/dotnet_background_foreground\"");
}

[Fact]
public void AppIconMonochromeLayerHasCorrectSize()
{
var items = new[]
{
new TaskItem("images/dotnet_background.svg", new Dictionary<string, string>
{
["IsAppIcon"] = bool.TrueString,
["ForegroundFile"] = "images/appicon.svg",
["MonochromeFile"] = "images/camera.svg",
}),
};

var task = GetNewTask(items);
var success = task.Execute();
Assert.True(success, LogErrorEvents.FirstOrDefault()?.Message);

// Monochrome follows the same AppIconParts sizing as foreground/background
AssertFileSize("mipmap-mdpi/dotnet_background_monochrome.png", 108, 108);
AssertFileSize("mipmap-xhdpi/dotnet_background_monochrome.png", 216, 216);
}
}

public class ExecuteForiOS : ExecuteForPlatformApp
Expand Down
Loading