File: SkiaSharpTools.cs
Web Access
Project: src\src\SingleProject\Resizetizer\src\Resizetizer.csproj (Microsoft.Maui.Resizetizer)
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using SkiaSharp;
 
namespace Microsoft.Maui.Resizetizer
{
	internal abstract class SkiaSharpTools
	{
		const int ERROR_ACCESS_DENIED = -2147024891;
		const int ERROR_SHARING_VIOLATION = -2147024864;
		const int DEFAULT_FILE_WRITE_RETRY_ATTEMPTS = 10;
		const int DEFAULT_FILE_WRITE_RETRY_DELAY_MS = 1000;
 
		static int fileWriteRetry = -1;
		static int fileWriteRetryDelay = -1;
 
		/// <summary>
		/// Checks for the environment variable DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS to
		/// see if a custom value for the number of times to retry writing a file has been 
		/// set.
		/// </summary>
		/// <returns>The value of DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS or the default of DEFAULT_FILE_WRITE_RETRY_ATTEMPTS</returns>
		public static int GetFileWriteRetryAttempts ()
		{
			if (fileWriteRetry == -1) {
				var retryVariable = Environment.GetEnvironmentVariable ("DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS");
				if (string.IsNullOrEmpty (retryVariable) || !int.TryParse (retryVariable, out fileWriteRetry))
					fileWriteRetry = DEFAULT_FILE_WRITE_RETRY_ATTEMPTS;
			}
			return fileWriteRetry;
		}
 
		/// <summary>
		/// Checks for the environment variable DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS to
		/// see if a custom value for the delay between trying to write a file has been 
		/// set.
		/// </summary>
		/// <returns>The value of DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS or the default of DEFAULT_FILE_WRITE_RETRY_DELAY_MS</returns>
		public static int GetFileWriteRetryDelay ()
		{
			if (fileWriteRetryDelay == -1) {
				var delayVariable = Environment.GetEnvironmentVariable ("DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS");
				if (string.IsNullOrEmpty (delayVariable) || !int.TryParse (delayVariable, out fileWriteRetryDelay))
					fileWriteRetryDelay = DEFAULT_FILE_WRITE_RETRY_DELAY_MS;
			}
			return fileWriteRetryDelay;
		} 
 
		static SkiaSharpTools()
		{
			// DO NOT DELETE!
			// Because we are doing dangerous things - like using a net462 assembly in a netstandard2.0 assembly
			// we need to make sure the correct dependencies are loaded. We use net462 because this has special
			// native library loading logic for the .NET Framework (Visual Studio). 
			var span = (Span<SKPoint>)new SKPoint[1];
			span[0] = new SKPoint();
		}
 
		public static SkiaSharpTools Create(bool isVector, string filename, SKSize? baseSize, SKColor? backgroundColor, SKColor? tintColor, ILogger logger)
			=> isVector
				? new SkiaSharpSvgTools(filename, baseSize, backgroundColor, tintColor, logger) as SkiaSharpTools
				: new SkiaSharpBitmapTools(filename, baseSize, backgroundColor, tintColor, logger);
 
		public static SkiaSharpTools CreateImaginary(SKColor? backgroundColor, ILogger logger)
			=> new SkiaSharpImaginaryTools(backgroundColor, logger);
 
		public SkiaSharpTools(ResizeImageInfo info, ILogger logger)
			: this(info.Filename, info.BaseSize, info.Color, info.TintColor, logger)
		{
		}
 
		public SkiaSharpTools(string filename, SKSize? baseSize, SKColor? backgroundColor, SKColor? tintColor, ILogger logger)
		{
			Logger = logger;
			Filename = filename;
			BaseSize = baseSize;
			BackgroundColor = backgroundColor;
			Paint = new SKPaint
			{
				FilterQuality = SKFilterQuality.High
			};
 
			if (tintColor is SKColor tint)
			{
				Logger?.Log($"Detected a tint color of {tint}");
				Paint.ColorFilter = SKColorFilter.CreateBlendMode(tint, SKBlendMode.SrcIn);
			}
		}
 
		public string Filename { get; }
 
		public SKSize? BaseSize { get; }
 
		public SKColor? BackgroundColor { get; }
 
		public ILogger Logger { get; }
 
		public SKPaint Paint { get; }
 
		public void Resize(DpiPath dpi, string destination, double additionalScale = 1.0, bool dpiSizeIsAbsolute = false)
		{
			var sw = new Stopwatch();
			sw.Start();
 
			var originalSize = GetOriginalSize();
			var absoluteSize = dpiSizeIsAbsolute ? dpi.Size : null;
			var (scaledSize, scale) = GetScaledSize(originalSize, dpi, absoluteSize);
			var (canvasSize, _) = GetCanvasSize(dpi, null, this);
 
			using (var tempBitmap = new SKBitmap(canvasSize.Width, canvasSize.Height))
			{
				Draw(tempBitmap, additionalScale, originalSize, scale, scaledSize);
				Save(destination, tempBitmap);
			}
 
			sw.Stop();
			Logger?.Log($"Save Image took {sw.ElapsedMilliseconds}ms ({destination})");
		}
 
		public static (SKSizeI Scaled, SKSize Unscaled) GetCanvasSize(DpiPath dpi, SKSize? baseSize = null, SkiaSharpTools baseTools = null)
		{
			// if an explicit size was given by the type of image, use that
			if (dpi.Size is SKSize size)
			{
				var scale = (float)dpi.Scale;
				var scaled = new SKSizeI(
					(int)(size.Width * scale),
					(int)(size.Height * scale));
				return (scaled, size);
			}
 
			// if an explicit size was given in the csproj, use that
			if (baseSize is SKSize bs)
			{
				var scale = (float)dpi.Scale;
				var scaled = new SKSizeI(
					(int)(bs.Width * scale),
					(int)(bs.Height * scale));
				return (scaled, bs);
			}
 
			// try determine the best size based on the loaded image
			if (baseTools is not null)
			{
				var baseOriginalSize = baseTools.GetOriginalSize();
				var (baseScaledSize, _) = baseTools.GetScaledSize(baseOriginalSize, dpi.Scale);
				return (baseScaledSize, baseOriginalSize);
			}
 
			throw new InvalidOperationException("The canvas size cannot be calculated if there is no size to start from (DPI size, BaseSize or image size).");
		}
 
		void Draw(SKBitmap tempBitmap, double additionalScale, SKSize originalSize, float scale, SKSizeI scaledSize)
		{
			using var canvas = new SKCanvas(tempBitmap);
 
			var canvasSize = tempBitmap.Info.Size;
 
			// clear
			canvas.Clear(BackgroundColor ?? SKColors.Transparent);
 
			// center the drawing
			canvas.Translate(
				(canvasSize.Width - scaledSize.Width) / 2,
				(canvasSize.Height - scaledSize.Height) / 2);
 
			// apply initial scale to size the image to fit the canvas
			canvas.Scale(scale, scale);
 
			// apply additional user scaling
			if (additionalScale != 1.0)
			{
				var userFgScale = (float)additionalScale;
 
				// add the user scale to the main scale
				scale *= userFgScale;
 
				// work out the center as if the canvas was exactly the same size as the foreground
				var fgCenterX = originalSize.Width / 2;
				var fgCenterY = originalSize.Height / 2;
 
				// scale to the user scale, centering
				canvas.Scale(userFgScale, userFgScale, fgCenterX, fgCenterY);
			}
 
			// draw
			DrawUnscaled(canvas, scale);
		}
 
		void Save(string destination, SKBitmap tempBitmap)
		{
			int attempt = 0;
			int attempts = GetFileWriteRetryAttempts ();
			int delay = GetFileWriteRetryDelay ();
			while (attempt <= attempts)
			{
				try
				{
					using var stream = File.Create(destination);
					tempBitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
					return;
				}
				catch (Exception ex)
				{
					switch (ex)
					{
						case UnauthorizedAccessException:
						case IOException:
							var code = Marshal.GetHRForException(ex);
							if ((code != ERROR_ACCESS_DENIED && code  != ERROR_SHARING_VIOLATION) || attempt >= attempts)
							{
								throw;
							}
							break;
						default:
							throw;
					}
					attempt++;
					Thread.Sleep(delay);
				}
			}
		}
 
		public abstract SKSize GetOriginalSize();
 
		public abstract void DrawUnscaled(SKCanvas canvas, float scale);
 
		public (SKSizeI, float) GetScaledSize(SKSize originalSize, DpiPath dpi, SKSize? absoluteSize = null) =>
			GetScaledSize(originalSize, dpi.Scale, absoluteSize ?? dpi.Size);
 
		public (SKSizeI, float) GetScaledSize(SKSize originalSize, decimal resizeRatio, SKSize? absoluteSize = null)
		{
			var sourceNominalWidth = (int)(absoluteSize?.Width ?? BaseSize?.Width ?? originalSize.Width);
			var sourceNominalHeight = (int)(absoluteSize?.Height ?? BaseSize?.Height ?? originalSize.Height);
 
			// Find the actual size of the image
			var sourceActualWidth = (double)originalSize.Width;
			var sourceActualHeight = (double)originalSize.Height;
 
			// Figure out what the ratio to convert the actual image size to the nominal size is
			var nominalRatio = Math.Min(
				sourceNominalWidth / sourceActualWidth,
				sourceNominalHeight / sourceActualHeight);
 
			// Multiply nominal ratio by the resize ratio to get our final ratio we actually adjust by
			var adjustRatio = nominalRatio * (double)resizeRatio;
 
			// Figure out our scaled width and height to make a new canvas for
			var scaledWidth = sourceActualWidth * adjustRatio;
			var scaledHeight = sourceActualHeight * adjustRatio;
			var scaledSize = new SKSizeI(
				(int)Math.Round(scaledWidth),
				(int)Math.Round(scaledHeight));
 
			return (scaledSize, (float)adjustRatio);
		}
	}
}