File: ResizetizeImages.cs
Web Access
Project: src\src\SingleProject\Resizetizer\src\Resizetizer.csproj (Microsoft.Maui.Resizetizer)
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
 
namespace Microsoft.Maui.Resizetizer
{
	public class ResizetizeImages : MauiAsyncTask, ILogger
	{
		[Required]
		public string PlatformType { get; set; } = "android";
 
		[Required]
		public string IntermediateOutputPath { get; set; }
 
		public bool ThrowsErrorOnDuplicateOutput { get; set; } = true;
 
		public string DuplicateOutputErrorMessage { get; set; }
 
		public string InputsFile { get; set; }
 
		public ITaskItem[] Images { get; set; }
 
		[Output]
		public ITaskItem[] CopiedResources { get; set; }
 
		public string IsMacEnabled { get; set; }
 
		public ILogger Logger => this;
 
		public override System.Threading.Tasks.Task ExecuteAsync()
		{
			var inputImages = ResizeImageInfo.Parse(Images);
			var images = RemoveDuplicates(inputImages);
 
			var dpis = DpiPath.GetDpis(PlatformType);
 
			if (dpis == null || dpis.Length <= 0)
				return System.Threading.Tasks.Task.CompletedTask;
 
			var originalScaleDpi = DpiPath.GetOriginal(PlatformType);
 
			var resizedImages = new ConcurrentBag<ResizedImageInfo>();
 
			this.ParallelForEach(images, img =>
			{
				try
				{
					var opStopwatch = new Stopwatch();
					opStopwatch.Start();
 
					string op;
 
					if (img.IsAppIcon)
					{
						// App icons are special
						ProcessAppIcon(img, resizedImages);
 
						op = "App Icon";
					}
					else
					{
						// By default we resize, but let's make sure
						if (img.Resize)
						{
							ProcessImageResize(img, dpis, resizedImages);
 
							op = "Resize";
						}
						else
						{
							// Otherwise just copy the thing over to the 1.0 scale
							ProcessImageCopy(img, originalScaleDpi, resizedImages);
 
							op = "Copy";
						}
					}
 
					opStopwatch.Stop();
 
					LogDebugMessage($"{op} took {opStopwatch.ElapsedMilliseconds}ms");
				}
				catch (Exception ex)
				{
					LogCodedError("MAUI0000", $"There was an exception processing the image '{img.Filename}': {ex}");
				}
			});
 
			if (PlatformType == "tizen")
			{
				var tizenResourceXmlGenerator = new TizenResourceXmlGenerator(IntermediateOutputPath, Logger);
				var r = tizenResourceXmlGenerator.Generate();
				if (r is not null)
					resizedImages.Add(r);
			}
 
			var copiedResources = new List<TaskItem>();
 
			foreach (var img in resizedImages)
			{
				var attr = new Dictionary<string, string>(StringComparer.Ordinal);
				string itemSpec = Path.GetFullPath(img.Filename);
 
				// Fix the item spec to be relative for mac
				if (bool.TryParse(IsMacEnabled, out bool isMac) && isMac)
					itemSpec = img.Filename;
 
				// Add DPI info to the itemspec so we can use it in the targets
				attr.Add("_ResizetizerDpiPath", img.Dpi.Path);
				attr.Add("_ResizetizerDpiScale", img.Dpi.Scale.ToString("0.0", CultureInfo.InvariantCulture));
 
				copiedResources.Add(new TaskItem(itemSpec, attr));
				// Make sure the file is not readonly
				Utils.SetWriteable(itemSpec);
				// force the date time so we never update an image if its not changed.
				File.SetLastWriteTimeUtc(itemSpec, DateTime.UtcNow);
			}
 
			CopiedResources = copiedResources.ToArray();
 
			return System.Threading.Tasks.Task.CompletedTask;
		}
 
		IEnumerable<ResizeImageInfo> RemoveDuplicates(IEnumerable<ResizeImageInfo> inputImages)
		{
			var imagesPairs = new Dictionary<string, ResizeImageInfo>();
 
			var builder = new StringBuilder();
			builder.Append(DuplicateOutputErrorMessage);
 
			var hasDuplicates = false;
			foreach (var image in inputImages)
			{
				if (imagesPairs.ContainsKey(image.OutputName))
				{
					if (hasDuplicates)
						builder.Append(", ");
 
					builder.Append($"{image.OutputName} ({image.ItemSpec})");
 
					hasDuplicates = true;
				}
 
				imagesPairs[image.OutputName] = image;
			}
 
			if (hasDuplicates)
			{
				if (ThrowsErrorOnDuplicateOutput)
					Log.LogError(builder.ToString());
				else
					Log.LogMessage(builder.ToString());
			}
 
			return imagesPairs.Values;
		}
 
		void ProcessAppIcon(ResizeImageInfo img, ConcurrentBag<ResizedImageInfo> resizedImages)
		{
			var appIconName = img.OutputName;
 
			// Generate the actual bitmap app icons themselves
			var appIconDpis = DpiPath.GetAppIconDpis(PlatformType, appIconName);
 
			LogDebugMessage($"App Icon");
 
			// Apple and Android have special additional files to generate for app icons
			if (PlatformType == "android")
			{
				LogDebugMessage($"Android Adaptive Icon Generator");
 
				appIconName = appIconName.ToLowerInvariant();
 
				var adaptiveIconGen = new AndroidAdaptiveIconGenerator(img, appIconName, IntermediateOutputPath, this);
				var iconsGenerated = adaptiveIconGen.Generate();
 
				foreach (var iconGenerated in iconsGenerated)
					resizedImages.Add(iconGenerated);
			}
			else if (PlatformType == "ios")
			{
				LogDebugMessage($"iOS Icon Assets Generator");
 
				var appleAssetGen = new AppleIconAssetsGenerator(img, appIconName, IntermediateOutputPath, appIconDpis, this);
 
				var assetsGenerated = appleAssetGen.Generate();
 
				foreach (var assetGenerated in assetsGenerated)
					resizedImages.Add(assetGenerated);
			}
			else if (PlatformType == "uwp")
			{
				LogDebugMessage($"Windows Icon Generator");
 
				var windowsIconGen = new WindowsIconGenerator(img, IntermediateOutputPath, this);
 
				resizedImages.Add(windowsIconGen.Generate());
			}
 
			LogDebugMessage($"Generating App Icon Bitmaps for DPIs");
 
			var appTool = new SkiaSharpAppIconTools(img, this);
 
			LogDebugMessage($"App Icon: Intermediate Path " + IntermediateOutputPath);
 
			foreach (var dpi in appIconDpis)
			{
				LogDebugMessage($"App Icon: " + dpi);
 
				var destination = Resizer.GetRasterFileDestination(img, dpi, IntermediateOutputPath)
					.Replace("{name}", appIconName);
				var (sourceExists, sourceModified) = Utils.FileExists(img.Filename);
				var (destinationExists, destinationModified) = Utils.FileExists(destination);
 
				LogDebugMessage($"App Icon Destination: " + destination);
 
				if (destinationModified > sourceModified)
				{
					Logger.Log($"Skipping `{img.Filename}` => `{destination}` file is up to date.");
					resizedImages.Add(new ResizedImageInfo() { Dpi = dpi, Filename = destination });
					continue;
				}
 
				appTool.Resize(dpi, destination);
				var r = appTool.Resize(dpi, destination);
				resizedImages.Add(r);
			}
		}
 
		void ProcessImageResize(ResizeImageInfo img, DpiPath[] dpis, ConcurrentBag<ResizedImageInfo> resizedImages)
		{
			var resizer = new Resizer(img, IntermediateOutputPath, this);
 
			foreach (var dpi in dpis)
			{
				LogDebugMessage($"Resizing {img.Filename}");
 
				var r = resizer.Resize(dpi, InputsFile);
				resizedImages.Add(r);
 
				LogDebugMessage($"Resized {img.Filename}");
			}
		}
 
		void ProcessImageCopy(ResizeImageInfo img, DpiPath originalScaleDpi, ConcurrentBag<ResizedImageInfo> resizedImages)
		{
			var resizer = new Resizer(img, IntermediateOutputPath, this);
 
			LogDebugMessage($"Copying {img.Filename}");
 
			var r = resizer.CopyFile(originalScaleDpi, InputsFile);
			resizedImages.Add(r);
 
			LogDebugMessage($"Copied {img.Filename}");
		}
 
		void ILogger.Log(string message)
		{
			Log?.LogMessage(message);
		}
	}
}