File: SkiaCanvas.cs
Web Access
Project: src\src\Graphics\src\Graphics.Skia\Graphics.Skia.csproj (Microsoft.Maui.Graphics.Skia)
using System;
using System.Numerics;
 
using Microsoft.Maui.Graphics.Text;
using SkiaSharp;
 
namespace Microsoft.Maui.Graphics.Skia
{
	public class SkiaCanvas : AbstractCanvas<SkiaCanvasState>, IBlurrableCanvas
	{
		private readonly SkiaCanvasStateService _stateService;
 
		private SKCanvas _canvas;
		private float _displayScale = 1;
		private SKShader _shader;
 
		public SkiaCanvas()
			: base(CreateStateService(out var stateService), new SkiaStringSizeService())
		{
			_stateService = stateService;
		}
 
		static SkiaCanvasStateService CreateStateService(out SkiaCanvasStateService stateService) =>
			stateService = new SkiaCanvasStateService();
 
		public override void Dispose()
		{
			_stateService.Dispose();
			base.Dispose();
		}
 
		public override float DisplayScale => _displayScale;
 
		public SKCanvas Canvas
		{
			get => _canvas;
			set
			{
				_canvas = null;
				ResetState();
				_canvas = value;
			}
		}
 
		public override bool Antialias
		{
			set => CurrentState.AntiAlias = value;
		}
 
		protected override float PlatformStrokeSize
		{
			set => CurrentState.PlatformStrokeSize = value;
		}
 
		public override float MiterLimit
		{
			set => CurrentState.MiterLimit = value;
		}
 
		public override float Alpha
		{
			set => CurrentState.Alpha = value;
		}
 
		public override LineCap StrokeLineCap
		{
			set => CurrentState.StrokeLineCap = value;
		}
 
		public override LineJoin StrokeLineJoin
		{
			set => CurrentState.StrokeLineJoin = value;
		}
 
		public override Color StrokeColor
		{
			set => CurrentState.StrokeColor = value ?? Colors.Black;
		}
 
		public override Color FontColor
		{
			set => CurrentState.FontColor = value ?? Colors.Black;
		}
 
		public override IFont Font
		{
			set => CurrentState.Font = value;
		}
 
		public override float FontSize
		{
			set => CurrentState.FontSize = value;
		}
 
		public override Color FillColor
		{
			set
			{
				if (_shader != null)
				{
					CurrentState.SetFillPaintShader(null);
					_shader.Dispose();
					_shader = null;
				}
 
				CurrentState.FillColor = value ?? Colors.White;
			}
		}
 
		public override BlendMode BlendMode
		{
			set
			{
				/* todo: implement this
				CGBlendMode blendMode = CGBlendMode.Normal;
 
				switch (value)
				{
					case BlendMode.Clear:
						blendMode = CGBlendMode.Clear;
						break;
					case BlendMode.Color:
						blendMode = CGBlendMode.Color;
						break;
					case BlendMode.ColorBurn:
						blendMode = CGBlendMode.ColorBurn;
						break;
					case BlendMode.ColorDodge:
						blendMode = CGBlendMode.ColorDodge;
						break;
					case BlendMode.Copy:
						blendMode = CGBlendMode.Copy;
						break;
					case BlendMode.Darken:
						blendMode = CGBlendMode.Darken;
						break;
					case BlendMode.DestinationAtop:
						blendMode = CGBlendMode.DestinationAtop;
						break;
					case BlendMode.DestinationIn:
						blendMode = CGBlendMode.DestinationIn;
						break;
					case BlendMode.DestinationOut:
						blendMode = CGBlendMode.DestinationOut;
						break;
					case BlendMode.DestinationOver:
						blendMode = CGBlendMode.DestinationOver;
						break;
					case BlendMode.Difference:
						blendMode = CGBlendMode.Difference;
						break;
					case BlendMode.Exclusion:
						blendMode = CGBlendMode.Exclusion;
						break;
					case BlendMode.HardLight:
						blendMode = CGBlendMode.HardLight;
						break;
					case BlendMode.Hue:
						blendMode = CGBlendMode.Hue;
						break;
					case BlendMode.Lighten:
						blendMode = CGBlendMode.Lighten;
						break;
					case BlendMode.Luminosity:
						blendMode = CGBlendMode.Luminosity;
						break;
					case BlendMode.Multiply:
						blendMode = CGBlendMode.Multiply;
						break;
					case BlendMode.Normal:
						blendMode = CGBlendMode.Normal;
						break;
					case BlendMode.Overlay:
						blendMode = CGBlendMode.Overlay;
						break;
					case BlendMode.PlusDarker:
						blendMode = CGBlendMode.PlusDarker;
						break;
					case BlendMode.PlusLighter:
						blendMode = CGBlendMode.PlusLighter;
						break;
					case BlendMode.Saturation:
						blendMode = CGBlendMode.Saturation;
						break;
					case BlendMode.Screen:
						blendMode = CGBlendMode.Screen;
						break;
					case BlendMode.SoftLight:
						blendMode = CGBlendMode.SoftLight;
						break;
					case BlendMode.SourceAtop:
						blendMode = CGBlendMode.SourceAtop;
						break;
					case BlendMode.SourceIn:
						blendMode = CGBlendMode.SourceIn;
						break;
					case BlendMode.SourceOut:
						blendMode = CGBlendMode.SourceOut;
						break;
					case BlendMode.XOR:
						blendMode = CGBlendMode.XOR;
						break;
				}
 
				canvas.SetBlendMode(blendMode);*/
 
				//CurrentState.FillPaint.SetXfermode(new
			}
		}
 
		public void SetDisplayScale(float value)
		{
			_displayScale = value;
		}
 
		protected override void PlatformSetStrokeDashPattern(
			float[] strokePattern,
			float strokeDashOffset,
			float strokeSize)
		{
			CurrentState.SetStrokeDashPattern(strokePattern, strokeDashOffset, strokeSize);
		}
 
		public override void SetFillPaint(Paint paint, RectF rectangle)
		{
			if (paint == null)
				paint = Colors.White.AsPaint();
 
			if (_shader != null)
			{
				CurrentState.SetFillPaintShader(null);
				_shader.Dispose();
				_shader = null;
			}
 
			if (paint is SolidPaint solidPaint)
			{
				FillColor = solidPaint.Color;
			}
			else if (paint is LinearGradientPaint linearGradientPaint)
			{
				float x1 = (float)(linearGradientPaint.StartPoint.X * rectangle.Width) + rectangle.X;
				float y1 = (float)(linearGradientPaint.StartPoint.Y * rectangle.Height) + rectangle.Y;
 
				float x2 = (float)(linearGradientPaint.EndPoint.X * rectangle.Width) + rectangle.X;
				float y2 = (float)(linearGradientPaint.EndPoint.Y * rectangle.Height) + rectangle.Y;
 
				var colors = new SKColor[linearGradientPaint.GradientStops.Length];
				var stops = new float[colors.Length];
 
				var vStops = linearGradientPaint.GetSortedStops();
 
				for (var i = 0; i < vStops.Length; i++)
				{
					colors[i] = vStops[i].Color.ToColor(CurrentState.Alpha);
					stops[i] = vStops[i].Offset;
				}
 
				try
				{
					CurrentState.FillColor = Colors.White;
					_shader = SKShader.CreateLinearGradient(
						new SKPoint(x1, y1),
						new SKPoint(x2, y2),
						colors,
						stops,
						SKShaderTileMode.Clamp);
					CurrentState.SetFillPaintShader(_shader);
				}
				catch (Exception exc)
				{
					System.Diagnostics.Debug.WriteLine(exc);
					FillColor = linearGradientPaint.BlendStartAndEndColors();
				}
			}
			else if (paint is RadialGradientPaint radialGradientPaint)
			{
				var colors = new SKColor[radialGradientPaint.GradientStops.Length];
				var stops = new float[colors.Length];
 
				var vStops = radialGradientPaint.GetSortedStops();
 
				for (var i = 0; i < vStops.Length; i++)
				{
					colors[i] = vStops[i].Color.ToColor(CurrentState.Alpha);
					stops[i] = vStops[i].Offset;
				}
 
				float centerX = (float)(radialGradientPaint.Center.X * rectangle.Width) + rectangle.X;
				float centerY = (float)(radialGradientPaint.Center.Y * rectangle.Height) + rectangle.Y;
				float radius = (float)radialGradientPaint.Radius * Math.Max(rectangle.Height, rectangle.Width);
 
				if (radius == 0)
					radius = GeometryUtil.GetDistance(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
 
				try
				{
					CurrentState.FillColor = Colors.White;
					_shader = SKShader.CreateRadialGradient(
						new SKPoint(centerX, centerY),
						radius,
						colors,
						stops,
						SKShaderTileMode.Clamp);
					CurrentState.SetFillPaintShader(_shader);
				}
				catch (Exception exc)
				{
					System.Diagnostics.Debug.WriteLine(exc);
					FillColor = radialGradientPaint.BlendStartAndEndColors();
				}
			}
			else if (paint is PatternPaint patternPaint)
			{
				SKBitmap bitmap = patternPaint.GetPatternBitmap(DisplayScale);
 
				if (bitmap != null)
				{
					try
					{
						CurrentState.FillColor = Colors.White;
						CurrentState.SetFillPaintFilterBitmap(true);
 
						_shader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat);
 
						//_shaderMatrix.Reset ();
						//_shaderMatrix.PreScale (CurrentState.ScaleX, CurrentState.ScaleY);
						//_shader.SetLocalMatrix (shaderMatrix);
 
						CurrentState.SetFillPaintShader(_shader);
					}
					catch (Exception exc)
					{
						System.Diagnostics.Debug.WriteLine(exc);
						FillColor = paint.BackgroundColor;
					}
				}
				else
				{
					FillColor = paint.BackgroundColor;
				}
			}
			else if (paint is ImagePaint imagePaint)
			{
				var image = imagePaint.Image as SkiaImage;
				if (image != null)
				{
					SKBitmap bitmap = image.PlatformRepresentation;
 
					if (bitmap != null)
					{
						try
						{
							CurrentState.FillColor = Colors.White;
							CurrentState.SetFillPaintFilterBitmap(true);
 
							_shader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat);
							//_shaderMatrix.Reset ();
							//_shaderMatrix.PreScale (CurrentState.ScaleX, CurrentState.ScaleY);
							//_shader.SetLocalMatrix (shaderMatrix);
 
							CurrentState.SetFillPaintShader(_shader);
						}
						catch (Exception exc)
						{
							System.Diagnostics.Debug.WriteLine(exc);
							FillColor = paint.BackgroundColor;
						}
					}
					else
					{
						FillColor = Colors.White;
					}
				}
				else
				{
					FillColor = Colors.White;
				}
			}
			else
			{
				FillColor = paint.BackgroundColor;
			}
		}
 
		protected override void PlatformDrawLine(
			float x1,
			float y1,
			float x2,
			float y2)
		{
			_canvas.DrawLine(x1, y1, x2, y2, CurrentState.StrokePaintWithAlpha);
		}
 
		protected override void PlatformDrawArc(
			float x,
			float y,
			float width,
			float height,
			float startAngle,
			float endAngle,
			bool clockwise,
			bool closed)
		{
			while (startAngle < 0)
				startAngle += 360;
 
			while (endAngle < 0)
				endAngle += 360;
 
			var rectX = x;
			var rectY = y;
			var rectWidth = width;
			var rectHeight = height;
 
			var sweep = GeometryUtil.GetSweep(startAngle, endAngle, clockwise);
 
			var rect = new SKRect(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
 
			startAngle *= -1;
			if (!clockwise)
				sweep *= -1;
 
			if (closed)
			{
				var platformPath = new SKPath();
				platformPath.AddArc(rect, startAngle, sweep);
				platformPath.Close();
				_canvas.DrawPath(platformPath, CurrentState.StrokePaintWithAlpha);
				platformPath.Dispose();
			}
			else
			{
				// todo: delete this after the api is bound
				var platformPath = new SKPath();
				platformPath.AddArc(rect, startAngle, sweep);
				_canvas.DrawPath(platformPath, CurrentState.StrokePaintWithAlpha);
				platformPath.Dispose();
 
				// todo: restore this when the api is bound.
				//_canvas.DrawArc (rect, startAngle, sweep, false, CurrentState.StrokePaintWithAlpha);
			}
		}
 
		public override void FillArc(
			float x,
			float y,
			float width,
			float height,
			float startAngle,
			float endAngle,
			bool clockwise)
		{
			while (startAngle < 0)
				startAngle += 360;
 
			while (endAngle < 0)
				endAngle += 360;
 
			var sweep = GeometryUtil.GetSweep(startAngle, endAngle, clockwise);
			var rect = new SKRect(x, y, x + width, y + height);
 
			startAngle *= -1;
			if (!clockwise)
				sweep *= -1;
 
			// todo: delete this after the api is bound
			var platformPath = new SKPath();
			platformPath.AddArc(rect, startAngle, sweep);
			_canvas.DrawPath(platformPath, CurrentState.FillPaintWithAlpha);
			platformPath.Dispose();
 
			// todo: restore this when the api is bound.
			//_canvas.DrawArc (rect, startAngle, sweep, false, CurrentState.FillPaintWithAlpha);
		}
 
		protected override void PlatformDrawRectangle(
			float x,
			float y,
			float width,
			float height)
		{
			float rectX = 0, rectY = 0, rectWidth = 0, rectHeight = 0;
 
			var strokeSize = CurrentState.ScaledStrokeSize;
			if (strokeSize <= 0)
				return;
 
			rectX = x;
			rectY = y;
			rectWidth = width;
			rectHeight = height;
 
			_canvas.DrawRect(rectX, rectY, rectWidth, rectHeight, CurrentState.StrokePaintWithAlpha);
		}
 
		public override void FillRectangle(
			float x,
			float y,
			float width,
			float height)
		{
			var rectX = x;
			var rectY = y;
			var rectWidth = width;
			var rectHeight = height;
 
			_canvas.DrawRect(rectX, rectY, rectWidth, rectHeight, CurrentState.FillPaintWithAlpha);
		}
 
		protected override void PlatformDrawRoundedRectangle(
			float x,
			float y,
			float width,
			float height,
			float aCornerRadius)
		{
			// These values work for a stroke location of center.
			var strokeSize = CurrentState.ScaledStrokeSize;
 
			var rectX = x;
			var rectY = y;
			var rectWidth = width;
			var rectHeight = height;
			var radius = aCornerRadius;
 
			_canvas.DrawRoundRect(rectX, rectY, rectWidth, rectHeight, radius, radius, CurrentState.StrokePaintWithAlpha);
		}
 
		public override void FillRoundedRectangle(
			float x,
			float y,
			float width,
			float height,
			float aCornerRadius)
		{
			var rectX = x;
			var rectY = y;
			var rectWidth = width;
			var rectHeight = height;
			var radius = aCornerRadius;
 
			var rect = new SKRect(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
			_canvas.DrawRoundRect(rect, radius, radius, CurrentState.FillPaintWithAlpha);
		}
 
		protected override void PlatformDrawEllipse(
			float x,
			float y,
			float width,
			float height)
		{
			// These values work for a stroke location of center.
			var strokeSize = CurrentState.ScaledStrokeSize;
 
			var rectX = x;
			var rectY = y;
			var rectWidth = width;
			var rectHeight = height;
 
			var rect = new SKRect(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
			_canvas.DrawOval(rect, CurrentState.StrokePaintWithAlpha);
		}
 
		public override void FillEllipse(
			float x,
			float y,
			float width,
			float height)
		{
			// These values work for a stroke location of center.
			var rectX = x;
			var rectY = y;
			var rectWidth = width;
			var rectHeight = height;
 
			var rect = new SKRect(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
			_canvas.DrawOval(rect, CurrentState.FillPaintWithAlpha);
		}
 
		public override void SubtractFromClip(
			float x,
			float y,
			float width,
			float height)
		{
			var rect = new SKRect(x, y, x + width, y + height);
			_canvas.ClipRect(rect, SKClipOperation.Difference);
		}
 
		protected override void PlatformDrawPath(
			PathF path)
		{
			var platformPath = path.AsSkiaPath();
			_canvas.DrawPath(platformPath, CurrentState.StrokePaintWithAlpha);
			platformPath.Dispose();
		}
 
		public override void ClipPath(PathF path,
			WindingMode windingMode = WindingMode.NonZero)
		{
			var platformPath = path.AsSkiaPath();
			platformPath.FillType = windingMode == WindingMode.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd;
			_canvas.ClipPath(platformPath);
		}
 
		public override void FillPath(PathF path,
			WindingMode windingMode)
		{
			var platformPath = path.AsSkiaPath();
			platformPath.FillType = windingMode == WindingMode.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd;
			_canvas.DrawPath(platformPath, CurrentState.FillPaintWithAlpha);
			platformPath.Dispose();
		}
 
		public override void DrawString(
			string value,
			float x,
			float y,
			HorizontalAlignment horizAlignment)
		{
			if (string.IsNullOrEmpty(value))
				return;
 
			if (horizAlignment == HorizontalAlignment.Left)
			{
				_canvas.DrawText(value, x, y, CurrentState.FontPaint);
			}
			else if (horizAlignment == HorizontalAlignment.Right)
			{
				var paint = CurrentState.FontPaint;
				var width = paint.MeasureText(value);
				x -= width;
				_canvas.DrawText(value, x, y, CurrentState.FontPaint);
			}
			else
			{
				var paint = CurrentState.FontPaint;
				var width = paint.MeasureText(value);
				x -= width / 2;
				_canvas.DrawText(value, x, y, CurrentState.FontPaint);
			}
		}
 
		public override void DrawString(
			string value,
			float x,
			float y,
			float width,
			float height,
			HorizontalAlignment horizAlignment,
			VerticalAlignment vertAlignment,
			TextFlow textFlow = TextFlow.ClipBounds,
			float lineSpacingAdjustment = 0)
		{
			if (string.IsNullOrEmpty(value))
				return;
 
			var rect = new RectF(x, y, width, height);
 
			var attributes = new StandardTextAttributes()
			{
				FontSize = CurrentState.FontPaint.TextSize,
				Font = CurrentState.Font,
				HorizontalAlignment = horizAlignment,
				VerticalAlignment = vertAlignment,
			};
 
			LayoutLine callback = (
				point,
				textual,
				text,
				ascent,
				descent,
				leading) =>
			{
				_canvas.DrawText(text, point.X, point.Y, CurrentState.FontPaint);
			};
 
			using (var textLayout = new SkiaTextLayout(value, rect, attributes, callback, textFlow, CurrentState.FontPaint))
			{
				textLayout.LayoutText();
			}
		}
 
		public override void DrawText(IAttributedText value, float x, float y, float width, float height)
		{
			System.Diagnostics.Debug.WriteLine("SkiaCanvas.DrawText not yet implemented.");
			DrawString(value?.Text, x, y, width, height, HorizontalAlignment.Left, VerticalAlignment.Top);
		}
 
		public override void ResetState()
		{
			base.ResetState();
 
			if (_shader != null)
			{
				_shader.Dispose();
				_shader = null;
			}
 
			_stateService.Reset(CurrentState);
		}
 
		public override bool RestoreState()
		{
			if (_shader != null)
			{
				_shader.Dispose();
				_shader = null;
			}
 
			return base.RestoreState();
		}
 
		protected override void StateRestored(SkiaCanvasState state)
		{
			_canvas?.Restore();
		}
 
		public override void SetShadow(
			SizeF offset,
			float blur,
			Color color)
		{
			var actualOffset = offset;
 
			var sx = actualOffset.Width;
			var sy = actualOffset.Height;
 
			if (color == null)
			{
				var actualColor = Colors.Black.AsSKColorMultiplyAlpha(CurrentState.Alpha);
				CurrentState.SetShadow(blur, sx, sy, actualColor);
			}
			else
			{
				var actualColor = color.AsSKColorMultiplyAlpha(CurrentState.Alpha);
				CurrentState.SetShadow(blur, sx, sy, actualColor);
			}
		}
 
		protected override void PlatformRotate(
			float degrees,
			float radians,
			float x,
			float y)
		{
			_canvas.RotateDegrees(degrees, x, y);
		}
 
		protected override void PlatformRotate(
			float degrees,
			float radians)
		{
			_canvas.RotateDegrees(degrees);
		}
 
		protected override void PlatformScale(
			float xFactor,
			float yFactor)
		{
			//canvas.Scale((float)aXFactor, (float)aYFactor);
			CurrentState.SetScale(Math.Abs(xFactor), Math.Abs(yFactor));
			if (xFactor < 0 || yFactor < 0)
				_canvas.Scale(xFactor < 0 ? -1 : 1, yFactor < 0 ? -1 : 1);
		}
 
		protected override void PlatformTranslate(
			float tx,
			float ty)
		{
			_canvas.Translate(tx * CurrentState.ScaleX, ty * CurrentState.ScaleY);
		}
 
		protected override void PlatformConcatenateTransform(Matrix3x2 transform)
		{
			var matrix = new SKMatrix();
 
			var values = new float[6];
			_canvas.TotalMatrix.GetValues(values);
			matrix.Values = values;
 
			// todo: implement me
			//matrix.PostConcat (transform.AsMatrix ());
			//canvas.Matrix = matrix;
		}
 
		public override void SaveState()
		{
			_canvas.Save();
			base.SaveState();
		}
 
		public void SetBlur(float radius)
		{
			CurrentState.SetBlur(radius);
		}
 
		public override void DrawImage(
			IImage image,
			float x,
			float y,
			float width,
			float height)
		{
			var skiaImage = image as SkiaImage;
			var bitmap = skiaImage?.PlatformRepresentation;
			if (bitmap != null)
			{
				var scaleX = CurrentState.ScaleX < 0 ? -1 : 1;
				var scaleY = CurrentState.ScaleY < 0 ? -1 : 1;
 
				_canvas.Save();
				//canvas.Scale (scaleX, scaleY);
				var srcRect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
 
				x *= scaleX;
				y *= scaleY;
				width *= scaleX;
				height *= scaleY;
 
				var rx1 = Math.Min(x, x + width);
				var ry1 = Math.Min(y, y + height);
				var rx2 = Math.Max(x, x + width);
				var ry2 = Math.Max(y, y + height);
 
				var destRect = new SKRect(rx1, ry1, rx2, ry2);
				var paint = CurrentState.GetImagePaint(1, 1);
				_canvas.DrawBitmap(bitmap, srcRect, destRect, paint);
				paint?.Dispose();
				_canvas.Restore();
			}
		}
 
		public override void ClipRectangle(
			float x,
			float y,
			float width,
			float height)
		{
			_canvas.ClipRect(new SKRect(x, y, x + width, y + height));
		}
	}
}