File: SkiaSharpAppIconTools.cs
Web Access
Project: src\src\SingleProject\Resizetizer\src\Resizetizer.csproj (Microsoft.Maui.Resizetizer)
#nullable enable
using System;
using System.Diagnostics;
using System.IO;
using SkiaSharp;
 
namespace Microsoft.Maui.Resizetizer
{
	internal class SkiaSharpAppIconTools
	{
		public SkiaSharpAppIconTools(ResizeImageInfo info, ILogger? logger)
		{
			Info = info;
			Logger = logger;
 
			AppIconName = info.OutputName;
 
			var hasBackground = !string.IsNullOrWhiteSpace(info.Filename) && File.Exists(info.Filename);
			var hasForeground = !string.IsNullOrWhiteSpace(info.ForegroundFilename) && File.Exists(info.ForegroundFilename);
			if (!hasBackground && !hasForeground)
				throw new InvalidOperationException("An app icon needs at least one image.");
 
			if (hasBackground)
				backgroundTools = SkiaSharpTools.Create(info.IsVector, info.Filename, null, null, null, logger);
			if (hasForeground)
				foregroundTools = SkiaSharpTools.Create(info.ForegroundIsVector, info.ForegroundFilename, null, null, info.TintColor, logger);
		}
 
		SkiaSharpTools? backgroundTools;
		SkiaSharpTools? foregroundTools;
 
		public ResizeImageInfo Info { get; }
 
		public ILogger? Logger { get; }
 
		public string AppIconName { get; }
 
		public ResizedImageInfo Resize(DpiPath dpi, string destination, Func<Stream>? getStream = null)
		{
			var sw = new Stopwatch();
			sw.Start();
 
			// 1. if an explicit size was given by the type of image, use that
			// 2. if an explicit size was given in the csproj, use that
			// 3. try determine the best size based on the background then foreground
			var (canvasSize, unscaledCanvasSize) = SkiaSharpTools.GetCanvasSize(
				dpi,
				Info.BaseSize,
				backgroundTools ?? foregroundTools);
 
			using (var tempBitmap = new SKBitmap(canvasSize.Width, canvasSize.Height))
			{
				Draw(tempBitmap, dpi, unscaledCanvasSize);
				Save(tempBitmap, destination, getStream);
			}
 
			sw.Stop();
			Logger?.Log($"Save app icon took {sw.ElapsedMilliseconds}ms ({destination})");
 
			return new ResizedImageInfo { Dpi = dpi, Filename = destination };
		}
 
		void Save(SKBitmap tempBitmap, string destination, Func<Stream>? getStream)
		{
			if (getStream is not null)
			{
				var stream = getStream();
				tempBitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
			}
			else
			{
				var dir = Path.GetDirectoryName(destination);
				Directory.CreateDirectory(dir);
 
				using var wrapper = File.Create(destination);
				tempBitmap.Encode(wrapper, SKEncodedImageFormat.Png, 100);
			}
		}
 
		void Draw(SKBitmap tempBitmap, DpiPath dpi, SKSize unscaledCanvasSize)
		{
			var canvasSize = tempBitmap.Info.Size;
 
			using var canvas = new SKCanvas(tempBitmap);
 
			if (GetClipPath(dpi, canvasSize, unscaledCanvasSize) is { } clipPath)
			{
				canvas.Clear(SKColors.Transparent);
 
				canvas.ClipPath(clipPath, antialias: true);
			}
 
			canvas.Clear(Info.Color ?? SKColors.Transparent);
 
			// draw background
			if (backgroundTools is not null)
			{
				canvas.Save();
 
				var backgroundOriginalSize = backgroundTools.GetOriginalSize();
				var (bgScaledSize, bgScale) = backgroundTools.GetScaledSize(backgroundOriginalSize, dpi, unscaledCanvasSize);
 
				// center the background
				canvas.Translate(
					(canvasSize.Width - bgScaledSize.Width) / 2,
					(canvasSize.Height - bgScaledSize.Height) / 2);
 
				// scale the background to the desired size
				canvas.Scale(bgScale, bgScale);
 
				// draw
				backgroundTools.DrawUnscaled(canvas, bgScale);
 
				canvas.Restore();
			}
 
			// draw foreground
			if (foregroundTools is not null)
			{
				var foregroundOriginalSize = foregroundTools.GetOriginalSize();
				var (fgScaledSize, fgScale) = foregroundTools.GetScaledSize(foregroundOriginalSize, dpi, unscaledCanvasSize);
 
				// center the foreground
				canvas.Translate(
					(canvasSize.Width - fgScaledSize.Width) / 2,
					(canvasSize.Height - fgScaledSize.Height) / 2);
 
				// scale the background to the desired size
				canvas.Scale(fgScale, fgScale);
 
				// add any foreground scale on top
				if (Info.ForegroundScale != 1.0)
				{
					var userFgScale = (float)Info.ForegroundScale;
 
					// add the user scale to the main scale
					fgScale *= userFgScale;
 
					// work out the center as if the canvas was exactly the same size as the foreground
					var fgCenterX = foregroundOriginalSize.Width / 2;
					var fgCenterY = foregroundOriginalSize.Height / 2;
 
					// scale to the user scale, centering
					canvas.Scale(userFgScale, userFgScale, fgCenterX, fgCenterY);
				}
 
				// draw
				foregroundTools.DrawUnscaled(canvas, fgScale);
			}
		}
 
		static SKPath? GetClipPath(DpiPath dpi, SKSize canvasSize, SKSize unscaledCanvasSize)
		{
			if (dpi.ClipShape == ClipShape.Circle)
			{
				var radius = Math.Min(canvasSize.Width, canvasSize.Height) / 2;
 
				var clip = new SKPath();
				clip.AddCircle(
					canvasSize.Width / 2,
					canvasSize.Height / 2,
					radius);
				return clip;
			}
 
			return null;
		}
	}
}