From 88bfdf87ea31d9e49ab30ed29c8f8317f8b7c030 Mon Sep 17 00:00:00 2001 From: Nelson Carrillo Date: Wed, 20 Jun 2018 18:52:25 -0400 Subject: [PATCH] Skia backend cleanup. --- Avalonia.sln | 44 + build.cake | 1 + build/SkiaSharp.props | 4 +- samples/ControlCatalog.NetCore/Program.cs | 4 +- .../Rendering/DeferredRenderer.cs | 3 + .../Rendering/ImmediateRenderer.cs | 1 + .../Rendering/RenderLayers.cs | 16 +- src/Avalonia.Visuals/Vector.cs | 52 +- src/Skia/Avalonia.Skia/BitmapImpl.cs | 142 ---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 783 ++++++++++++------ src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 22 +- .../Avalonia.Skia/FramebufferRenderTarget.cs | 211 +++-- src/Skia/Avalonia.Skia/GeometryImpl.cs | 170 +++- .../Helpers/ImageSavingHelper.cs | 47 ++ .../Helpers/PixelFormatHelper.cs | 35 + src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs | 23 + src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 92 ++ .../Avalonia.Skia/PlatformRenderInterface.cs | 80 +- .../SkiaApplicationExtensions.cs | 28 + src/Skia/Avalonia.Skia/SkiaPlatform.cs | 46 +- src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 6 +- src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 124 +-- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 169 ++++ .../Avalonia.Skia/TransformedGeometryImpl.cs | 62 +- src/Skia/Avalonia.Skia/TypefaceCache.cs | 9 +- src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs | 151 ++++ src/Skia/Avalonia.Skia/readme.md | 34 +- .../Interop/UnmanagedMethods.cs | 28 + src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- .../Avalonia.RenderTests/Media/BitmapTests.cs | 2 +- .../Media/ImageBrushTests.cs | 12 +- .../Media/LinearGradientBrushTests.cs | 12 +- .../Media/RadialGradientBrushTests.cs | 6 +- .../Media/VisualBrushTests.cs | 6 +- .../Avalonia.Skia.UnitTests.csproj | 22 + tests/Avalonia.Skia.UnitTests/HitTesting.cs | 79 ++ .../Properties/AssemblyInfo.cs | 8 + ...Brush_RedBlue_Horizontal_Fill.expected.png | Bin 652 -> 642 bytes ...ntBrush_RedBlue_Vertical_Fill.expected.png | Bin 1959 -> 1998 bytes .../RadialGradientBrush_RedBlue.expected.png | Bin 0 -> 9792 bytes .../VisualBrush_InTree_Visual.expected.png | Bin 0 -> 5905 bytes .../Path/Path_With_PenLineCap.expected.png | Bin 0 -> 1155 bytes 42 files changed, 1831 insertions(+), 705 deletions(-) delete mode 100644 src/Skia/Avalonia.Skia/BitmapImpl.cs create mode 100644 src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs create mode 100644 src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs create mode 100644 src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs create mode 100644 src/Skia/Avalonia.Skia/ImmutableBitmap.cs create mode 100644 src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs create mode 100644 src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs create mode 100644 src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj create mode 100644 tests/Avalonia.Skia.UnitTests/HitTesting.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png create mode 100644 tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png create mode 100644 tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png diff --git a/Avalonia.sln b/Avalonia.sln index 39dc11f41f..39396f3ab8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props build\NetCore.props = build\NetCore.props + build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props @@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2468,6 +2471,46 @@ Global {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2520,6 +2563,7 @@ Global {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2} {4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build.cake b/build.cake index bf3ae41b58..561a33186a 100644 --- a/build.cake +++ b/build.cake @@ -207,6 +207,7 @@ Task("Run-Unit-Tests") RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); if (data.Parameters.IsRunningOnWindows) { RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 04e8a3ad4f..35c979a95e 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index b45a93455e..1f53dedc14 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using Avalonia; +using Avalonia.Skia; namespace ControlCatalog.NetCore { @@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); static void ConsoleSilencer() { diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index fd6b149837..a14923b410 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -117,6 +117,9 @@ namespace Avalonia.Rendering var scene = Interlocked.Exchange(ref _scene, null); scene?.Dispose(); Stop(); + + Layers.Clear(); + RenderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index e830d5c313..2118b66de2 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -133,6 +133,7 @@ namespace Avalonia.Rendering /// public void Dispose() { + _renderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index bafd644603..6a45ecd912 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -11,11 +11,7 @@ namespace Avalonia.Rendering { private List _inner = new List(); private Dictionary _index = new Dictionary(); - - public RenderLayers() - { - } - + public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -51,6 +47,16 @@ namespace Avalonia.Rendering } } + public void Clear() + { + foreach (var layer in _index.Values) + { + layer.Bitmap.Dispose(); + } + + _index.Clear(); + } + public bool TryGetValue(IVisual layerRoot, out RenderLayer value) { return _index.TryGetValue(layerRoot, out value); diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index acde49a84a..c2db17cd86 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; -using System.Xml.Linq; +using JetBrains.Annotations; namespace Avalonia { @@ -122,6 +122,56 @@ namespace Avalonia return new Vector(a._x - b._x, a._y - b._y); } + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(Vector other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && _y == other._y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(Vector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is Vector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(Vector left, Vector right) + { + return left.Equals(right); + } + + public static bool operator !=(Vector left, Vector right) + { + return !left.Equals(right); + } + /// /// Returns the string representation of the point. /// diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs deleted file mode 100644 index ccc5a37105..0000000000 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using Avalonia.Platform; -using Avalonia.Rendering; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl - { - private Vector _dpi; - - public SKBitmap Bitmap { get; private set; } - - public BitmapImpl(SKBitmap bm) - { - Bitmap = bm; - PixelHeight = bm.Height; - PixelWidth = bm.Width; - _dpi = new Vector(96, 96); - } - - static void ReleaseProc(IntPtr address, object ctx) - { - ((IUnmanagedBlob) ctx).Dispose(); - } - - private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; - - public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) - { - PixelHeight = height; - PixelWidth = width; - _dpi = dpi; - var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; - var runtimePlatform = AvaloniaLocator.Current?.GetService(); - var runtime = runtimePlatform?.GetRuntimeInfo(); - if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) - colorType = SKColorType.Bgra8888; - - if (runtimePlatform != null) - { - Bitmap = new SKBitmap(); - var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); - var plat = AvaloniaLocator.Current.GetService(); - var blob = plat.AllocBlob(nfo.BytesSize); - Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); - - } - else - Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); - Bitmap.Erase(SKColor.Empty); - } - - public void Dispose() - { - Bitmap.Dispose(); - } - - public int PixelWidth { get; private set; } - public int PixelHeight { get; private set; } - - class BitmapDrawingContext : DrawingContextImpl - { - private readonly SKSurface _surface; - - public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : this(CreateSurface(bitmap), dpi, visualBrushRenderer) - { - CanUseLcdRendering = false; - } - - private static SKSurface CreateSurface(SKBitmap bitmap) - { - IntPtr length; - var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes); - if (rv == null) - throw new Exception("Unable to create Skia surface"); - return rv; - } - - public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : base(surface.Canvas, dpi, visualBrushRenderer) - { - _surface = surface; - } - - public override void Dispose() - { - base.Dispose(); - _surface.Dispose(); - } - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer); - } - - public void Save(Stream stream) - { - IntPtr length; - using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes)) - using (var data = image.Encode()) - { - data.SaveTo(stream); - } - } - - public void Save(string fileName) - { - using (var stream = File.Create(fileName)) - Save(stream); - } - - class BitmapFramebuffer : ILockedFramebuffer - { - private SKBitmap _bmp; - - public BitmapFramebuffer(SKBitmap bmp) - { - _bmp = bmp; - _bmp.LockPixels(); - } - - public void Dispose() - { - _bmp.UnlockPixels(); - _bmp = null; - } - - public IntPtr Address => _bmp.GetPixels(); - public int Width => _bmp.Width; - public int Height => _bmp.Height; - public int RowBytes => _bmp.RowBytes; - public Vector Dpi { get; } = new Vector(96, 96); - public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); - } - - public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap); - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 22e5652cfb..b7ce6eedc4 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1,57 +1,114 @@ -using Avalonia.Media; -using SkiaSharp; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; +using SkiaSharp; namespace Avalonia.Skia { - internal class DrawingContextImpl : IDrawingContextImpl + /// + /// Skia based drawing context. + /// + public class DrawingContextImpl : IDrawingContextImpl { + private readonly IDisposable[] _disposables; private readonly Vector _dpi; + private readonly Stack _maskStack = new Stack(); + private readonly Stack _opacityStack = new Stack(); private readonly Matrix? _postTransform; - private readonly IDisposable[] _disposables; private readonly IVisualBrushRenderer _visualBrushRenderer; - private Stack maskStack = new Stack(); - protected bool CanUseLcdRendering = true; - public SKCanvas Canvas { get; private set; } - - public DrawingContextImpl( - SKCanvas canvas, - Vector dpi, - IVisualBrushRenderer visualBrushRenderer, - params IDisposable[] disposables) + private double _currentOpacity = 1.0f; + private readonly bool _canTextUseLcdRendering; + private Matrix _currentTransform; + + /// + /// Context create info. + /// + public struct CreateInfo + { + /// + /// Canvas to draw to. + /// + public SKCanvas Canvas; + + /// + /// Dpi of drawings. + /// + public Vector Dpi; + + /// + /// Visual brush renderer. + /// + public IVisualBrushRenderer VisualBrushRenderer; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + + /// + /// Create new drawing context. + /// + /// Create info. + /// Array of elements to dispose after drawing has finished. + public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables) { - _dpi = dpi; - if (dpi.X != 96 || dpi.Y != 96) - _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96); - _visualBrushRenderer = visualBrushRenderer; + _dpi = createInfo.Dpi; + _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; - Canvas = canvas; + _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; + + Canvas = createInfo.Canvas; + + if (Canvas == null) + { + throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + } + + if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) + { + _postTransform = + Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + } + Transform = Matrix.Identity; } + + /// + /// Skia canvas. + /// + public SKCanvas Canvas { get; } + /// public void Clear(Color color) { Canvas.Clear(color.ToSKColor()); } + /// public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { - var impl = (BitmapImpl)source.Item; + var drawableImage = (IDrawableBitmapImpl) source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - using (var paint = new SKPaint() - { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) + + using (var paint = + new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))}) { - Canvas.DrawBitmap(impl.Bitmap, s, d, paint); + drawableImage.Draw(this, s, d, paint); } } + /// public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); @@ -59,17 +116,19 @@ namespace Avalonia.Skia PopOpacityMask(); } + /// public void DrawLine(Pen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } } + /// public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = (GeometryImpl)geometry; + var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) @@ -79,6 +138,7 @@ namespace Avalonia.Skia { Canvas.DrawPath(impl.EffectivePath, fill.Paint); } + if (stroke.Paint != null) { Canvas.DrawPath(impl.EffectivePath, stroke.Paint); @@ -86,227 +146,424 @@ namespace Avalonia.Skia } } - private struct PaintState : IDisposable + /// + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) { - private readonly SKColor _color; - private readonly SKShader _shader; - private readonly SKPaint _paint; + using (var paint = CreatePaint(pen, rect.Size)) + { + var rc = rect.ToSKRect(); - public PaintState(SKPaint paint, SKColor color, SKShader shader) + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } + } + } + + /// + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + using (var paint = CreatePaint(brush, rect.Size)) { - _paint = paint; - _color = color; - _shader = shader; + var rc = rect.ToSKRect(); + + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } } + } - public void Dispose() + /// + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + using (var paint = CreatePaint(foreground, text.Size)) { - _paint.Color = _color; - _paint.Shader = _shader; + var textImpl = (FormattedTextImpl) text; + textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); } } - internal struct PaintWrapper : IDisposable + /// + public IRenderTargetBitmapImpl CreateLayer(Size size) { - //We are saving memory allocations there - //TODO: add more disposable fields if needed - public readonly SKPaint Paint; + var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + var pixelSize = size * normalizedDpi; - private IDisposable _disposable1; - private IDisposable _disposable2; + return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi); + } - public IDisposable ApplyTo(SKPaint paint) - { - var state = new PaintState(paint, paint.Color, paint.Shader); + /// + public void PushClip(Rect clip) + { + Canvas.Save(); + Canvas.ClipRect(clip.ToSKRect()); + } - paint.Color = Paint.Color; - paint.Shader = Paint.Shader; + /// + public void PopClip() + { + Canvas.Restore(); + } - return state; - } + /// + public void PushOpacity(double opacity) + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } - public void AddDisposable(IDisposable disposable) - { - if (_disposable1 == null) - _disposable1 = disposable; - else if (_disposable2 == null) - _disposable2 = disposable; - else - throw new InvalidOperationException(); - } + /// + public void PopOpacity() + { + _currentOpacity = _opacityStack.Pop(); + } - public PaintWrapper(SKPaint paint) + /// + public virtual void Dispose() + { + if (_disposables == null) { - Paint = paint; - _disposable1 = null; - _disposable2 = null; + return; } - public void Dispose() + foreach (var disposable in _disposables) { - Paint?.Dispose(); - _disposable1?.Dispose(); - _disposable2?.Dispose(); + disposable?.Dispose(); } } - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + /// + public void PushGeometryClip(IGeometryImpl clip) { - SKPaint paint = new SKPaint(); - var rv = new PaintWrapper(paint); - paint.IsStroke = false; + Canvas.Save(); + Canvas.ClipPath(((GeometryImpl)clip).EffectivePath); + } - - double opacity = brush.Opacity * _currentOpacity; - paint.IsAntialias = true; + /// + public void PopGeometryClip() + { + Canvas.Restore(); + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + // TODO: This should be disposed + var paint = new SKPaint(); + + Canvas.SaveLayer(paint); + _maskStack.Push(CreatePaint(mask, bounds.Size)); + } - var solid = brush as ISolidColorBrush; - if (solid != null) + /// + public void PopOpacityMask() + { + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { - paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); - return rv; + Canvas.SaveLayer(paint); + using (var paintWrapper = _maskStack.Pop()) + { + Canvas.DrawPaint(paintWrapper.Paint); + } + Canvas.Restore(); } - paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity))); - var gradient = brush as IGradientBrush; - if (gradient != null) + Canvas.Restore(); + } + + /// + public Matrix Transform + { + get { return _currentTransform; } + set { - var tileMode = gradient.SpreadMethod.ToSKShaderTileMode(); - var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); - var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray(); + if (_currentTransform == value) + return; + + _currentTransform = value; + + var transform = value; - var linearGradient = brush as ILinearGradientBrush; - if (linearGradient != null) + if (_postTransform.HasValue) + { + transform *= _postTransform.Value; + } + + Canvas.SetMatrix(transform.ToSKMatrix()); + } + } + + /// + /// Configure paint wrapper for using gradient brush. + /// + /// Paint wrapper. + /// Target size. + /// Gradient brush. + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) + { + var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); + var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); + var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); + + switch (gradientBrush) + { + case ILinearGradientBrush linearGradient: { var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + break; } - else + case IRadialGradientBrush radialGradient: { - var radialGradient = brush as IRadialGradientBrush; - if (radialGradient != null) - { - var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); - var radius = (float)radialGradient.Radius; + var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetSize.Width); - // TODO: There is no SetAlpha in SkiaSharp - //paint.setAlpha(128); - - // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + // TODO: There is no SetAlpha in SkiaSharp + //paint.setAlpha(128); + // would be nice to cache these shaders possibly? + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; } + + break; } + } + } + + /// + /// Configure paint wrapper for using tile brush. + /// + /// Paint wrapper. + /// Target size. + /// Tile brush to use. + /// Tile brush image. + private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) + { + var calc = new TileBrushCalculator(tileBrush, + new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); + + var intermediate = CreateRenderTarget( + (int)calc.IntermediateSize.Width, + (int)calc.IntermediateSize.Height, _dpi); + + paintWrapper.AddDisposable(intermediate); + + using (var context = intermediate.CreateDrawingContext(null)) + { + var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); - return rv; + context.Clear(Colors.Transparent); + context.PushClip(calc.IntermediateClip); + context.Transform = calc.IntermediateTransform; + context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); + context.PopClip(); } - var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; - var tileBrushImage = default(BitmapImpl); + var tileTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.MakeIdentity(); - if (visualBrush != null) + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + + var image = intermediate.SnapshotImage(); + paintWrapper.AddDisposable(image); + + using (var shader = image.ToShader(tileX, tileY, tileTransform)) { - if (_visualBrushRenderer != null) - { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + paintWrapper.Paint.Shader = shader; + } + } - if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) - { - var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + /// + /// Configure paint wrapper to use visual brush. + /// + /// Paint wrapper. + /// Visual brush. + /// Visual brush renderer. + /// Tile brush image. + private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage) + { + if (_visualBrushRenderer == null) + { + throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + } - using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer)) - { - ctx.Clear(Colors.Transparent); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); - } + var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); - tileBrushImage = intermediate; - rv.AddDisposable(tileBrushImage); - } - } - else + if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) + { + var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + + using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + ctx.Clear(Colors.Transparent); + + visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); } + + tileBrushImage = intermediate; + paintWrapper.AddDisposable(tileBrushImage); } - else + } + + /// + /// Creates paint wrapper for given brush. + /// + /// Source brush. + /// Target size. + /// Paint wrapper for given brush. + internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + { + var paint = new SKPaint + { + IsStroke = false, + IsAntialias = true + }; + + var paintWrapper = new PaintWrapper(paint); + + double opacity = brush.Opacity * _currentOpacity; + + if (brush is ISolidColorBrush solid) { - tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item); + paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); + + return paintWrapper; } - if (tileBrush != null && tileBrushImage != null) + paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); + + if (brush is IGradientBrush gradient) { - var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); - var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi); - rv.AddDisposable(bitmap); - using (var context = bitmap.CreateDrawingContext(null)) - { - var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); + ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); - context.Clear(Colors.Transparent); - context.PushClip(calc.IntermediateClip); - context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); - context.PopClip(); - } + return paintWrapper; + } + + var tileBrush = brush as ITileBrush; + var visualBrush = brush as IVisualBrush; + var tileBrushImage = default(IDrawableBitmapImpl); - SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y); - SKShaderTileMode tileX = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - - SKShaderTileMode tileY = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation)) - paint.Shader = shader; + if (visualBrush != null) + { + ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + } + else + { + tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item; } - return rv; + if (tileBrush != null && tileBrushImage != null) + { + ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); + } + + return paintWrapper; } + /// + /// Creates paint wrapper for given pen. + /// + /// Source pen. + /// Target size. + /// private PaintWrapper CreatePaint(Pen pen, Size targetSize) { var rv = CreatePaint(pen.Brush, targetSize); var paint = rv.Paint; paint.IsStroke = true; - paint.StrokeWidth = (float)pen.Thickness; + paint.StrokeWidth = (float) pen.Thickness; - if (pen.StartLineCap == PenLineCap.Round) - paint.StrokeCap = SKStrokeCap.Round; - else if (pen.StartLineCap == PenLineCap.Square) - paint.StrokeCap = SKStrokeCap.Square; - else - paint.StrokeCap = SKStrokeCap.Butt; + // Need to modify dashes due to Skia modifying their lengths + // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots + // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. + float dashLengthModifier; + float gapLengthModifier; - if (pen.LineJoin == PenLineJoin.Miter) - paint.StrokeJoin = SKStrokeJoin.Miter; - else if (pen.LineJoin == PenLineJoin.Round) - paint.StrokeJoin = SKStrokeJoin.Round; - else - paint.StrokeJoin = SKStrokeJoin.Bevel; + switch (pen.StartLineCap) + { + case PenLineCap.Round: + paint.StrokeCap = SKStrokeCap.Round; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + case PenLineCap.Square: + paint.StrokeCap = SKStrokeCap.Square; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + default: + paint.StrokeCap = SKStrokeCap.Butt; + dashLengthModifier = 0.0f; + gapLengthModifier = 0.0f; + break; + } + + switch (pen.LineJoin) + { + case PenLineJoin.Miter: + paint.StrokeJoin = SKStrokeJoin.Miter; + break; + case PenLineJoin.Round: + paint.StrokeJoin = SKStrokeJoin.Round; + break; + default: + paint.StrokeJoin = SKStrokeJoin.Bevel; + break; + } - paint.StrokeMiter = (float)pen.MiterLimit; + paint.StrokeMiter = (float) pen.MiterLimit; if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { - var pe = SKPathEffect.CreateDash( - pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(), - (float)pen.DashStyle.Offset); + var srcDashes = pen.DashStyle.Dashes; + var dashesArray = new float[srcDashes.Count]; + + for (var i = 0; i < srcDashes.Count; ++i) + { + var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier; + + // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale + dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier; + } + + var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset); + paint.PathEffect = pe; rv.AddDisposable(pe); } @@ -314,128 +571,118 @@ namespace Avalonia.Skia return rv; } - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + /// + /// Create new render target compatible with this drawing context. + /// + /// Width. + /// Height. + /// Drawing dpi. + /// Pixel format. + /// + private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null) { - using (var paint = CreatePaint(pen, rect.Size)) + var createInfo = new SurfaceRenderTarget.CreateInfo { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } - } + Width = width, + Height = height, + Dpi = dpi, + Format = format, + DisableTextLcdRendering = !_canTextUseLcdRendering + }; + + return new SurfaceRenderTarget(createInfo); } - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + /// + /// Skia cached paint state. + /// + private struct PaintState : IDisposable { - using (var paint = CreatePaint(brush, rect.Size)) + private readonly SKColor _color; + private readonly SKShader _shader; + private readonly SKPaint _paint; + + public PaintState(SKPaint paint, SKColor color, SKShader shader) { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } + _paint = paint; + _color = color; + _shader = shader; } - } - public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) - { - using (var paint = CreatePaint(foreground, text.Size)) + /// + public void Dispose() { - var textImpl = (FormattedTextImpl)text; - textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering); + _paint.Color = _color; + _paint.Shader = _shader; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) - { - var pixelSize = size * (_dpi / 96); - return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi); - } - - public void PushClip(Rect clip) - { - Canvas.Save(); - Canvas.ClipRect(clip.ToSKRect()); - } - - public void PopClip() - { - Canvas.Restore(); - } - - private double _currentOpacity = 1.0f; - private readonly Stack _opacityStack = new Stack(); - - public void PushOpacity(double opacity) + /// + /// Skia paint wrapper. + /// + internal struct PaintWrapper : IDisposable { - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; - } + //We are saving memory allocations there + public readonly SKPaint Paint; - public void PopOpacity() - { - _currentOpacity = _opacityStack.Pop(); - } + private IDisposable _disposable1; + private IDisposable _disposable2; + private IDisposable _disposable3; - public virtual void Dispose() - { - if(_disposables!=null) - foreach (var disposable in _disposables) - disposable?.Dispose(); - } + public PaintWrapper(SKPaint paint) + { + Paint = paint; - public void PushGeometryClip(IGeometryImpl clip) - { - Canvas.Save(); - Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath); - } + _disposable1 = null; + _disposable2 = null; + _disposable3 = null; + } - public void PopGeometryClip() - { - Canvas.Restore(); - } + public IDisposable ApplyTo(SKPaint paint) + { + var state = new PaintState(paint, paint.Color, paint.Shader); - public void PushOpacityMask(IBrush mask, Rect bounds) - { - Canvas.SaveLayer(new SKPaint()); - maskStack.Push(CreatePaint(mask, bounds.Size)); - } + paint.Color = Paint.Color; + paint.Shader = Paint.Shader; - public void PopOpacityMask() - { - Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn }); - using (var paintWrapper = maskStack.Pop()) - { - Canvas.DrawPaint(paintWrapper.Paint); + return state; } - Canvas.Restore(); - Canvas.Restore(); - } - private Matrix _currentTransform; - - public Matrix Transform - { - get { return _currentTransform; } - set + /// + /// Add new disposable to a wrapper. + /// + /// Disposable to add. + public void AddDisposable(IDisposable disposable) { - if (_currentTransform == value) - return; + if (_disposable1 == null) + { + _disposable1 = disposable; + } + else if (_disposable2 == null) + { + _disposable2 = disposable; + } + else if (_disposable3 == null) + { + _disposable3 = disposable; + } + else + { + Debug.Assert(false); - _currentTransform = value; - var transform = value; - if (_postTransform.HasValue) - transform *= _postTransform.Value; - Canvas.SetMatrix(transform.ToSKMatrix()); + // ReSharper disable once HeuristicUnreachableCode + throw new InvalidOperationException( + "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables."); + } + } + + /// + public void Dispose() + { + Paint?.Dispose(); + _disposable1?.Dispose(); + _disposable2?.Dispose(); + _disposable3?.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 9c18d52cea..d835c83aa6 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -10,6 +10,9 @@ using System.Linq; namespace Avalonia.Skia { + /// + /// Skia formatted text implementation. + /// public class FormattedTextImpl : IFormattedTextImpl { public FormattedTextImpl( @@ -21,7 +24,7 @@ namespace Avalonia.Skia IReadOnlyList spans) { Text = text ?? string.Empty; - + // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); @@ -352,7 +355,7 @@ namespace Avalonia.Skia { float measuredWidth; string subText = textInput.Substring(textIndex, stop - textIndex); - lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2; + lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth); } //Check for white space or line breakers before the lengthBreak @@ -451,7 +454,6 @@ namespace Avalonia.Skia private void BuildRects() { // Build character rects - var fm = _paint.FontMetrics; SKTextAlign align = _paint.TextAlign; for (int li = 0; li < _skiaLines.Count; li++) @@ -559,18 +561,16 @@ namespace Avalonia.Skia string subString; - float widthConstraint = (_constraint.Width != double.PositiveInfinity) - ? (float)_constraint.Width - : -1; - - for (int c = 0; curOff < length; c++) + float widthConstraint = double.IsPositiveInfinity(_constraint.Width) + ? -1 + : (float)_constraint.Width; + + while(curOff < length) { float lineWidth = -1; int measured; int trailingnumber = 0; - - subString = Text.Substring(curOff); - + float constraint = -1; if (_wrapping == TextWrapping.Wrap) diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1956f02d1b..99dbbefd4d 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,82 +1,199 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Skia.Helpers; using SkiaSharp; namespace Avalonia.Skia { + /// + /// Skia render target that renders to a framebuffer surface. No gpu acceleration available. + /// public class FramebufferRenderTarget : IRenderTarget { - private readonly IFramebufferPlatformSurface _surface; + private readonly IFramebufferPlatformSurface _platformSurface; + private SKImageInfo _currentImageInfo; + private IntPtr _currentFramebufferAddress; + private SKSurface _framebufferSurface; + private PixelFormatConversionShim _conversionShim; + private IDisposable _preFramebufferCopyHandler; - public FramebufferRenderTarget(IFramebufferPlatformSurface surface) + /// + /// Create new framebuffer render target using a target surface. + /// + /// Target surface. + public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { - _surface = surface; + _platformSurface = platformSurface; } + /// public void Dispose() { - //Nothing to do here, since we don't own framebuffer + FreeSurface(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var framebuffer = _platformSurface.Lock(); + var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height, + framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); + + CreateSurface(framebufferImageInfo, framebuffer); + + var canvas = _framebufferSurface.Canvas; + + canvas.RestoreToCount(-1); + canvas.Save(); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = framebuffer.Dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + + return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer); + } + + /// + /// Check if two images info are compatible. + /// + /// Current. + /// Desired. + /// True, if images are compatible. + private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo) + { + return currentImageInfo.Width == desiredImageInfo.Width && + currentImageInfo.Height == desiredImageInfo.Height && + currentImageInfo.ColorType == desiredImageInfo.ColorType; + } + + /// + /// Create Skia surface backed by given framebuffer. + /// + /// Desired image info. + /// Backing framebuffer. + private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) + { + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) + { + return; + } + + FreeSurface(); + + _currentFramebufferAddress = framebuffer.Address; + + var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes); + + // If surface cannot be created - try to create a compatibilty shim first + if (surface == null) + { + _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); + _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; + + surface = _conversionShim.Surface; + } + + _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " + + framebuffer.Format + + " or pixel format translator"); + _currentImageInfo = desiredImageInfo; + } + + /// + /// Free Skia surface. + /// + private void FreeSurface() + { + _conversionShim?.Dispose(); + _conversionShim = null; + _preFramebufferCopyHandler = null; + + if (_conversionShim != null) + { + _framebufferSurface?.Dispose(); + } + + _framebufferSurface = null; + _currentFramebufferAddress = IntPtr.Zero; } - class PixelFormatShim : IDisposable + /// + /// Converts non-compatible pixel formats using bitmap copies. + /// + private class PixelFormatConversionShim : IDisposable { - private readonly SKImageInfo _nfo; - private readonly IntPtr _fb; - private readonly int _rowBytes; - private SKBitmap _bitmap; + private readonly SKBitmap _bitmap; + private readonly SKImageInfo _destinationInfo; + private readonly IntPtr _framebufferAddress; - public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes) + public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) { - _nfo = nfo; - _fb = fb; - _rowBytes = rowBytes; + _destinationInfo = destinationInfo; + _framebufferAddress = framebufferAddress; + + // Create bitmap using default platform settings + _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); + + if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) + { + _bitmap.Dispose(); + + throw new Exception( + $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); + } + + Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); - - _bitmap = new SKBitmap(nfo.Width, nfo.Height); - if (!_bitmap.CanCopyTo(nfo.ColorType)) + if (Surface == null) { _bitmap.Dispose(); + throw new Exception( - $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}"); + $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); } + + SurfaceCopyHandler = Disposable.Create(CopySurface); } - public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + /// + /// Skia surface. + /// + public SKSurface Surface { get; } + /// + /// Handler to start conversion via surface copy. + /// + public IDisposable SurfaceCopyHandler { get; } + + /// public void Dispose() { - using (var tmp = _bitmap.Copy(_nfo.ColorType)) - tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes); + Surface.Dispose(); _bitmap.Dispose(); } - - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - var fb = _surface.Lock(); - PixelFormatShim shim = null; - SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(), - SKAlphaType.Premul); - var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ?? - (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes)) - .CreateSurface(); - if (surface == null) - throw new Exception("Unable to create a surface for pixel format " + fb.Format + - " or pixel format translator"); - var canvas = surface.Canvas; - - - canvas.RestoreToCount(0); - canvas.Save(); - canvas.ResetMatrix(); - return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb); + /// + /// Convert and copy surface to a framebuffer. + /// + private void CopySurface() + { + using (var snapshot = Surface.Snapshot()) + { + snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); + } + } } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index fb134b728c..af4cdb8056 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; @@ -5,14 +8,169 @@ using SkiaSharp; namespace Avalonia.Skia { - abstract class GeometryImpl : IGeometryImpl + /// + /// A Skia implementation of . + /// + public abstract class GeometryImpl : IGeometryImpl { + private PathCache _pathCache; + + /// public abstract Rect Bounds { get; } public abstract SKPath EffectivePath { get; } - public abstract bool FillContains(Point point); - public abstract Rect GetRenderBounds(Pen pen); - public abstract IGeometryImpl Intersect(IGeometryImpl geometry); - public abstract bool StrokeContains(Pen pen, Point point); - public abstract ITransformedGeometryImpl WithTransform(Matrix transform); + + /// + public bool FillContains(Point point) + { + return PathContainsCore(EffectivePath, point); + } + + /// + public bool StrokeContains(Pen pen, Point point) + { + // Skia requires to compute stroke path to check for point containment. + // Due to that we are caching using stroke width. + // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic. + + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return PathContainsCore(_pathCache.CachedStrokePath, point); + } + + /// + /// Update path cache for given stroke width. + /// + /// Stroke width. + private void UpdatePathCache(float strokeWidth) + { + var strokePath = new SKPath(); + + // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path. + if (Math.Abs(strokeWidth) < float.Epsilon) + { + _pathCache.Cache(strokePath, strokeWidth, Bounds); + } + else + { + using (var paint = new SKPaint()) + { + paint.IsStroke = true; + paint.StrokeWidth = strokeWidth; + + paint.GetFillPath(EffectivePath, strokePath); + + _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); + } + } + } + + /// + /// Check Skia path if it contains a point. + /// + /// Path to check. + /// Point. + /// True, if point is contained in a path. + private static bool PathContainsCore(SKPath path, Point point) + { + return path.Contains((float)point.X, (float)point.Y); + } + + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect); + + return result == null ? null : new StreamGeometryImpl(result); + } + + /// + public Rect GetRenderBounds(Pen pen) + { + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0); + } + + /// + public ITransformedGeometryImpl WithTransform(Matrix transform) + { + return new TransformedGeometryImpl(this, transform); + } + + /// + /// Invalidate all caches. Call after chaning path contents. + /// + protected void InvalidateCaches() + { + _pathCache.Invalidate(); + } + + private struct PathCache + { + private float _cachedStrokeWidth; + + /// + /// Tolerance for two stroke widths to be deemed equal + /// + public const float Tolerance = float.Epsilon; + + /// + /// Cached contour path. + /// + public SKPath CachedStrokePath { get; private set; } + + /// + /// Cached geometry render bounds. + /// + public Rect CachedGeometryRenderBounds { get; private set; } + + /// + /// Is cached valid for given stroke width. + /// + /// Stroke width to check. + /// True, if CachedStrokePath can be used for given stroke width. + public bool HasCacheFor(float strokeWidth) + { + return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance; + } + + /// + /// Cache path for given stroke width. Takes ownership of a passed path. + /// + /// Path to cache. + /// Stroke width to cache. + /// Render bounds to use. + public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds) + { + if (CachedStrokePath != path) + { + CachedStrokePath?.Dispose(); + } + + CachedStrokePath = path; + CachedGeometryRenderBounds = geometryRenderBounds; + _cachedStrokeWidth = strokeWidth; + } + + /// + /// Invalidate cache state. + /// + public void Invalidate() + { + CachedStrokePath?.Dispose(); + CachedGeometryRenderBounds = Rect.Empty; + _cachedStrokeWidth = default(float); + } + } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs new file mode 100644 index 0000000000..d587a989cc --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with saving images to stream/file. + /// + public static class ImageSavingHelper + { + /// + /// Save Skia image to a file. + /// + /// Image to save + /// Target file. + public static void SaveImage(SKImage image, string fileName) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + + using (var stream = File.Create(fileName)) + { + SaveImage(image, stream); + } + } + + /// + /// Save Skia image to a stream. + /// + /// Image to save + /// Target stream. + public static void SaveImage(SKImage image, Stream stream) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + using (var data = image.Encode()) + { + data.SaveTo(stream); + } + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs new file mode 100644 index 0000000000..307af708af --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with resolving pixel formats to Skia color types. + /// + public static class PixelFormatHelper + { + /// + /// Resolve given format to Skia color type. + /// + /// Format to resolve. + /// Resolved color type. + public static SKColorType ResolveColorType(PixelFormat? format) + { + var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; + + // TODO: This looks like some leftover hack + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + var runtime = runtimePlatform?.GetRuntimeInfo(); + + if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux) + { + colorType = SKColorType.Bgra8888; + } + + return colorType; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs new file mode 100644 index 0000000000..5aa5de2abc --- /dev/null +++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs @@ -0,0 +1,23 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Extended bitmap implementation that allows for drawing it's contents. + /// + internal interface IDrawableBitmapImpl : IBitmapImpl + { + /// + /// Draw bitmap to a drawing context. + /// + /// Drawing context. + /// Source rect. + /// Destination rect. + /// Paint to use. + void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs new file mode 100644 index 0000000000..332b8547bf --- /dev/null +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -0,0 +1,92 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Immutable Skia bitmap. + /// + public class ImmutableBitmap : IDrawableBitmapImpl + { + private readonly SKImage _image; + + /// + /// Create immutable bitmap from given stream. + /// + /// Stream containing encoded data. + public ImmutableBitmap(Stream stream) + { + using (var skiaStream = new SKManagedStream(stream)) + { + _image = SKImage.FromEncodedData(SKData.Create(skiaStream)); + + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } + + PixelWidth = _image.Width; + PixelHeight = _image.Height; + } + } + + /// + /// Create immutable bitmap from given pixel data copy. + /// + /// Width of data pixels. + /// Height of data pixels. + /// Stride of data pixels. + /// Format of data pixels. + /// Data pixels. + public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data) + { + var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul); + + _image = SKImage.FromPixelCopy(imageInfo, data, stride); + + if (_image == null) + { + throw new ArgumentException("Unable to create bitmap from provided data"); + } + + PixelWidth = width; + PixelHeight = height; + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Dispose() + { + _image.Dispose(); + } + + /// + public void Save(string fileName) + { + ImageSavingHelper.SaveImage(_image, fileName); + } + + /// + public void Save(Stream stream) + { + ImageSavingHelper.SaveImage(_image, stream); + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawImage(_image, sourceRect, destRect, paint); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 50e65f45dc..d4e6403dc9 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,21 +1,21 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Platform; -using SkiaSharp; namespace Avalonia.Skia { - public partial class PlatformRenderInterface : IPlatformRenderInterface + /// + /// Skia platform render interface. + /// + public class PlatformRenderInterface : IPlatformRenderInterface { - public IBitmapImpl CreateBitmap(int width, int height) - { - return CreateRenderTargetBitmap(width, height, 96, 96); - } - + /// public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, @@ -27,27 +27,19 @@ namespace Avalonia.Skia return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } + /// public IStreamGeometryImpl CreateStreamGeometry() { return new StreamGeometryImpl(); } - public IBitmapImpl LoadBitmap(System.IO.Stream stream) + /// + public IBitmapImpl LoadBitmap(Stream stream) { - using (var s = new SKManagedStream(stream)) - { - var bitmap = SKBitmap.Decode(s); - if (bitmap != null) - { - return new BitmapImpl(bitmap); - } - else - { - throw new ArgumentException("Unable to load bitmap from provided data"); - } - } + return new ImmutableBitmap(stream); } + /// public IBitmapImpl LoadBitmap(string fileName) { using (var stream = File.OpenRead(fileName)) @@ -56,16 +48,13 @@ namespace Avalonia.Skia } } + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) { - using (var tmp = new SKBitmap()) - { - tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul) - , data, stride); - return new BitmapImpl(tmp.Copy()); - } + return new ImmutableBitmap(width, height, stride, format, data); } + /// public IRenderTargetBitmapImpl CreateRenderTargetBitmap( int width, int height, @@ -73,24 +62,47 @@ namespace Avalonia.Skia double dpiY) { if (width < 1) + { throw new ArgumentException("Width can't be less than 1", nameof(width)); + } + if (height < 1) + { throw new ArgumentException("Height can't be less than 1", nameof(height)); + } + + var dpi = new Vector(dpiX, dpiY); - return new BitmapImpl(width, height, new Vector(dpiX, dpiY)); + var createInfo = new SurfaceRenderTarget.CreateInfo + { + Width = width, + Height = height, + Dpi = dpi, + DisableTextLcdRendering = false + }; + + return new SurfaceRenderTarget(createInfo); } + /// public virtual IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - var fb = surfaces?.OfType().FirstOrDefault(); - if (fb == null) - throw new Exception("Skia backend currently only supports framebuffer render target"); - return new FramebufferRenderTarget(fb); + foreach (var surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + { + return new FramebufferRenderTarget(framebufferSurface); + } + } + + throw new NotSupportedException( + "Don't know how to create a Skia render target from any of provided surfaces"); } + /// public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { - return new BitmapImpl(width, height, new Vector(96, 96), format); + return new WriteableBitmapImpl(width, height, format); } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs new file mode 100644 index 0000000000..0086671880 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Skia; + +// ReSharper disable once CheckNamespace +namespace Avalonia +{ + /// + /// Skia appication extensions. + /// + public static class SkiaApplicationExtensions + { + /// + /// Enable Skia renderer. + /// + /// Builder type. + /// Builder. + /// Preferred backend type. + /// Configure builder. + public static T UseSkia(this T builder) where T : AppBuilderBase, new() + { + builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia"); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index d3083d3d33..06679478cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -1,47 +1,31 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia -{ - public static class SkiaApplicationExtensions - { - public static T UseSkia(this T builder) where T : AppBuilderBase, new() - { - builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia"); - return builder; - } - } -} namespace Avalonia.Skia { + /// + /// Skia platform initializer. + /// public static class SkiaPlatform { - private static bool s_forceSoftwareRendering; - + /// + /// Initialize Skia platform. + /// public static void Initialize() { var renderInterface = new PlatformRenderInterface(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); } - public static bool ForceSoftwareRendering - { - get { return s_forceSoftwareRendering; } - set - { - s_forceSoftwareRendering = value; - - // TODO: I left this property here as place holder. Do we still need the ability to Force software rendering? - // Is it even possible with SkiaSharp? Perhaps kekekes can answer as part of the HW accel work. - // - throw new NotImplementedException(); - } - } + /// + /// Default DPI. + /// + public static Vector DefaultDpi => new Vector(96.0f, 96.0f); } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 6a1aed8d79..9196ace4d8 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,10 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; - -namespace Avalonia +namespace Avalonia.Skia { public static class SkiaSharpExtensions { diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 935d6d5e5b..c19ff79d87 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,90 +1,103 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl + /// + /// A Skia implementation of a . + /// + public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { - Rect _bounds; - SKPath _path; - - public override SKPath EffectivePath => _path; - - public override Rect GetRenderBounds(Pen pen) + private Rect _bounds; + private readonly SKPath _effectivePath; + + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + /// Precomputed path bounds. + public StreamGeometryImpl(SKPath path, Rect bounds) { - return GetRenderBounds(pen?.Thickness ?? 0); + _effectivePath = path; + _bounds = bounds; } - public override Rect Bounds => _bounds; - - public IStreamGeometryImpl Clone() + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) { - return new StreamGeometryImpl - { - _path = _path?.Clone(), - _bounds = Bounds - }; } - public IStreamGeometryContextImpl Open() + /// + /// Initializes a new instance of the class. + /// + public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) { - _path = new SKPath(); - _path.FillType = SKPathFillType.EvenOdd; - - return new StreamContext(this); } + + /// + public override SKPath EffectivePath => _effectivePath; - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } + /// + public override Rect Bounds => _bounds; - public override IGeometryImpl Intersect(IGeometryImpl geometry) + /// + public IStreamGeometryImpl Clone() { - throw new NotImplementedException(); + return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds); } - public override ITransformedGeometryImpl WithTransform(Matrix transform) + /// + public IStreamGeometryContextImpl Open() { - return new TransformedGeometryImpl(this, transform); + return new StreamContext(this); } - private Rect GetRenderBounds(double strokeThickness) + /// + /// Create new empty . + /// + /// Empty + private static SKPath CreateEmptyPath() { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return new SKPath + { + FillType = SKPathFillType.EvenOdd + }; } - class StreamContext : IStreamGeometryContextImpl + /// + /// A Skia implementation of a . + /// + private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private SKPath _path; + private readonly SKPath _path; - Point _currentPoint; + /// + /// Initializes a new instance of the class. + /// Geometry to operate on. + /// public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._path; + _path = _geometryImpl._effectivePath; } - + + /// + /// Will update bounds of passed geometry. public void Dispose() { - SKRect rc; - _path.GetBounds(out rc); - _geometryImpl._bounds = rc.ToAvaloniaRect(); + _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl.InvalidateCaches(); } + /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _path.ArcTo( @@ -95,33 +108,33 @@ namespace Avalonia.Skia sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, (float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void BeginFigure(Point startPoint, bool isFilled) { _path.MoveTo((float)startPoint.X, (float)startPoint.Y); - _currentPoint = startPoint; } + /// public void CubicBezierTo(Point point1, Point point2, Point point3) { _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); - _currentPoint = point3; } + /// public void QuadraticBezierTo(Point point1, Point point2) { _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); - _currentPoint = point2; } + /// public void LineTo(Point point) { _path.LineTo((float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void EndFigure(bool isClosed) { if (isClosed) @@ -130,6 +143,7 @@ namespace Avalonia.Skia } } + /// public void SetFillRule(FillRule fillRule) { _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs new file mode 100644 index 0000000000..88200dcfbe --- /dev/null +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -0,0 +1,169 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia render target that writes to a surface. + /// + public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl + { + private readonly Vector _dpi; + private readonly SKSurface _surface; + private readonly SKCanvas _canvas; + private readonly bool _disableLcdRendering; + + /// + /// Create new surface render target. + /// + /// Create info. + public SurfaceRenderTarget(CreateInfo createInfo) + { + PixelWidth = createInfo.Width; + PixelHeight = createInfo.Height; + _dpi = createInfo.Dpi; + _disableLcdRendering = createInfo.DisableTextLcdRendering; + + _surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format); + + _canvas = _surface?.Canvas; + + if (_surface == null || _canvas == null) + { + throw new InvalidOperationException("Failed to create Skia render target surface"); + } + } + + /// + /// Create backing Skia surface. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKSurface CreateSurface(int width, int height, PixelFormat? format) + { + var imageInfo = MakeImageInfo(width, height, format); + + return SKSurface.Create(imageInfo); + } + + /// + public void Dispose() + { + _canvas.Dispose(); + _surface.Dispose(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + _canvas.RestoreToCount(-1); + _canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = _canvas, + Dpi = _dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = _disableLcdRendering + }; + + return new DrawingContextImpl(createInfo); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Save(string fileName) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public void Save(Stream stream) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } + } + + /// + /// Create Skia image snapshot from a surface. + /// + /// Image snapshot. + public SKImage SnapshotImage() + { + return _surface.Snapshot(); + } + + /// + /// Create image info for given parameters. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format) + { + var colorType = PixelFormatHelper.ResolveColorType(format); + + return new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + } + + /// + /// Create info of a surface render target. + /// + public struct CreateInfo + { + /// + /// Width of a render target. + /// + public int Width; + + /// + /// Height of a render target. + /// + public int Height; + + /// + /// Dpi used when rendering to a surface. + /// + public Vector Dpi; + + /// + /// Pixel format of a render target. + /// + public PixelFormat? Format; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index e14d3f04be..e95069eef3 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -1,59 +1,43 @@ -using System; -using Avalonia.Media; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl + /// + /// A Skia implementation of a . + /// + public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl { + /// + /// Initializes a new instance of the class. + /// + /// Source geometry. + /// Transform of new geometry. public TransformedGeometryImpl(GeometryImpl source, Matrix transform) { SourceGeometry = source; Transform = transform; - EffectivePath = source.EffectivePath.Clone(); - EffectivePath.Transform(transform.ToSKMatrix()); + + var transformedPath = source.EffectivePath.Clone(); + transformedPath.Transform(transform.ToSKMatrix()); + + EffectivePath = transformedPath; + Bounds = transformedPath.TightBounds.ToAvaloniaRect(); } + /// public override SKPath EffectivePath { get; } + /// public IGeometryImpl SourceGeometry { get; } + /// public Matrix Transform { get; } - public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); - - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override Rect GetRenderBounds(Pen pen) - { - return GetRenderBounds(pen.Thickness); - } - - public override IGeometryImpl Intersect(IGeometryImpl geometry) - { - throw new NotImplementedException(); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override ITransformedGeometryImpl WithTransform(Matrix transform) - { - return new TransformedGeometryImpl(this, transform); - } - - public Rect GetRenderBounds(double strokeThickness) - { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); - } + /// + public override Rect Bounds { get; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 24674f3b22..f85dd84055 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,3 +1,7 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media; @@ -5,7 +9,10 @@ using SkiaSharp; namespace Avalonia.Skia { - static class TypefaceCache + /// + /// Cache for Skia typefaces. + /// + internal static class TypefaceCache { public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name); static readonly Dictionary> Cache = new Dictionary>(); diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs new file mode 100644 index 0000000000..ab6c399ff4 --- /dev/null +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -0,0 +1,151 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia based writeable bitmap. + /// + public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl + { + private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; + private readonly SKBitmap _bitmap; + + /// + /// Create new writeable bitmap. + /// + /// Width. + /// Height. + /// Format. + public WriteableBitmapImpl(int width, int height, PixelFormat? format = null) + { + PixelHeight = height; + PixelWidth = width; + + var colorType = PixelFormatHelper.ResolveColorType(format); + + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + + if (runtimePlatform != null) + { + _bitmap = new SKBitmap(); + + var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + var blob = runtimePlatform.AllocBlob(nfo.BytesSize); + + _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob); + } + else + { + _bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); + } + + _bitmap.Erase(SKColor.Empty); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint); + } + + /// + public void Dispose() + { + _bitmap.Dispose(); + } + + /// + public void Save(Stream stream) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Save(string fileName) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap); + + /// + /// Get snapshot as image. + /// + /// Image snapshot. + public SKImage GetSnapshot() + { + return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + } + + /// + /// Release given unmanaged blob. + /// + /// Blob address. + /// Blob. + private static void ReleaseProc(IntPtr address, object ctx) + { + ((IUnmanagedBlob)ctx).Dispose(); + } + + /// + /// Framebuffer for bitmap. + /// + private class BitmapFramebuffer : ILockedFramebuffer + { + private SKBitmap _bitmap; + + /// + /// Create framebuffer from given bitmap. + /// + /// Bitmap. + public BitmapFramebuffer(SKBitmap bitmap) + { + _bitmap = bitmap; + } + + /// + public void Dispose() + { + _bitmap = null; + } + + /// + public IntPtr Address => _bitmap.GetPixels(); + + /// + public int Width => _bitmap.Width; + + /// + public int Height => _bitmap.Height; + + /// + public int RowBytes => _bitmap.RowBytes; + + /// + public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; + + /// + public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/readme.md b/src/Skia/Avalonia.Skia/readme.md index 3defe9c58c..7ed92c5453 100644 --- a/src/Skia/Avalonia.Skia/readme.md +++ b/src/Skia/Avalonia.Skia/readme.md @@ -1,42 +1,22 @@ -TODO: - -BitmapImpl -- constructor from Width/Height -- Save - -StreamGeometryImpl -- Hit testing in Geometry missing as SkiaSharp does not expose this - DrawingContextImpl - Alpha support missing as SkiaSharp does not expose this - Gradient Shader caching? -- TileBrushes - Pen Dash styles Formatted Text Rendering -- minor polish +- Minor polish -RenderTarget -- Figure out a cleaner implementation across all platforms -- HW acceleration +Linux +- Need gpu platform implementation -App Bootstrapping -- Cleanup the testapplications across all platforms -- Add a cleaner Fluent API for the subsystems - - ie. app.UseDirect2D() (via platform specific extension methods) +macOS +- Need gpu platform implementation Android - Not tested at all yet iOS -- Get GLView working again. See HW above - -Win32 -- Cleanup the unmanaged methods (BITMAPINFO) if possible +- Not tested at all yet General -- Cleanup/eliminate obsolete files -- Finish cleanup of the many Test Applications -- Get Skia Unit Tests passing - - +- Get Skia Unit Tests passing (most of the issues are related to antialiasing) \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b2ffc2cec9..f89086ccb7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -321,6 +321,24 @@ namespace Avalonia.Win32.Interop WS_EX_NOACTIVATE = 0x08000000 } + [Flags] + public enum ClassStyles : uint + { + CS_VREDRAW = 0x0001, + CS_HREDRAW = 0x0002, + CS_DBLCLKS = 0x0008, + CS_OWNDC = 0x0020, + CS_CLASSDC = 0x0040, + CS_PARENTDC = 0x0080, + CS_NOCLOSE = 0x0200, + CS_SAVEBITS = 0x0800, + CS_BYTEALIGNCLIENT = 0x1000, + CS_BYTEALIGNWINDOW = 0x2000, + CS_GLOBALCLASS = 0x4000, + CS_IME = 0x00010000, + CS_DROPSHADOW = 0x00020000 + } + public enum WindowsMessage : uint { WM_NULL = 0x0000, @@ -1194,6 +1212,16 @@ namespace Avalonia.Win32.Interop public IntPtr hIconSm; } + [Flags] + public enum OpenFileNameFlags + { + OFN_ALLOWMULTISELECT = 0x00000200, + OFN_EXPLORER = 0x00080000, + OFN_HIDEREADONLY = 0x00000004, + OFN_NOREADONLYRETURN = 0x00008000, + OFN_OVERWRITEPROMPT = 0x00000002 + } + public enum HRESULT : uint { S_FALSE = 0x0001, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9f67f97252..292d8a8138 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -724,7 +724,7 @@ namespace Avalonia.Win32 UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { cbSize = Marshal.SizeOf(), - style = 0, + style = (int)(ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW), // Unique DC helps with performance when using Gpu based rendering lpfnWndProc = _wndProcDelegate, hInstance = UnmanagedMethods.GetModuleHandle(null), hCursor = DefaultCursor, diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 089579a0a0..4cee05a0d9 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Theory] [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), -#if SKIA +#if AVALONIA_SKIA InlineData(PixelFormat.Rgb565) #endif ] diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index 6381bceadc..0107002274 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs @@ -281,12 +281,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest() { Decorator target = new Decorator @@ -309,12 +305,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index 062c7d88f5..656e77fc31 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Horizontal_Fill() { Decorator target = new Decorator @@ -52,12 +48,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Vertical_Fill() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index b3e214f863..0017feb106 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task RadialGradientBrush_RedBlue() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index f69d336271..3a1f7adb2d 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -270,12 +270,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj new file mode 100644 index 0000000000..0473355fcd --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -0,0 +1,22 @@ + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Skia.UnitTests/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs new file mode 100644 index 0000000000..544a09e50f --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class HitTesting + { + [Fact] + public void Hit_Test_Should_Respect_Fill() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Fill = Brushes.Red, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(10, 10), root, null); + var insideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] {root.Child}, insideResult); + } + } + + [Fact] + public void Hit_Test_Should_Respect_Stroke() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Stroke = Brushes.Red, + StrokeThickness = 5, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + var insideResult = root.Renderer.HitTest(new Point(1, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] { root.Child }, insideResult); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a462e5b079 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Reflection; +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png index 744a314256d8d42397f9d111ab4b83364af647d0..89668d19f40847604c17c2ed55e9f75562e9602b 100644 GIT binary patch literal 642 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZk(GggNypR0F{C2y?PW*4CI=DLgA;$xm%P%V`fWzy@=(r$KPpel0W|@E{j0)# z_P35dzH!{F@A~rh&tHAt{{QaY^*QhEg_~{ueJZUi{^pr?+Ov0mpL+Jqz1h0bH_n+C oI4TIRFpUyJJq*C{&rqMzopr03^|LYXATM literal 652 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZk(Ggg$;{KmF{C2y?PYJSLk1!a7hf>l`R^qA+tDgugSk+86`!tV<(_lzfEt0| zM_~KgJEgTYzxBSy$87&@)TjUa-uLC@_s@lwrG1_{_hwD%^V>g9oy*y`$$0kqH)o!m zFMnQmimmT9=N?4?4yHzhQDSI^0V6mSH0(D%#+!a^OTm^)OpPEBPgg&ebxsLQ0G|$z AQUCw| diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png index 3a3d7df5d47bd2009a1d5613de4af07739416a7b..2b882874dcb11d7c79f3c1b73cc29a68952378ff 100644 GIT binary patch literal 1998 zcmYk74^R_V9>>$rP1$hB!vdU{K}g z{0@Y{VCO}KiQmslnHWgs4$OF(r}wQXS$&*t>Syh(Zh^-cH;;!G1HUR>f8$2*#5Gew z*c;)`noBseIF~_j|*ZJ#O}vOS*r;?Ro7!+(CFPFPJtQA1lYI!zbi zgK`H?U1pW2nlc0qagqnKK^#laJvVj#iM}}kON|>6RSt|bl_D)wzORSL18(;aSk|uX zSk%ckvt+7#aVU)0*0PFY$vne%*8iaH1Gs594O=1TuV?%KR&SQ6nsk(Eb@QsG62$&$ z3CUKg>Wp3BwTfW=H-3@o4VEynpRwcv{WZUdfV_#fLFT#8gMPrqP|JPu?Yf0 z<$J*0u~e!$7w!Q;-?hHOu`nOL1DaK9WvbrBX3+c;S0i@T1dN%uJWpUVH+sP6vy^hg z?mhvU6Iad=m}1ZjkezZ6;TJoBW)OcZ$KvyeKWOq4QdMty2&C8L2|1R(eFBwf3u%S) zVFnHN`*AGMK_2SgY4&4W^nD5K6yze9cSlB32MIS2$_xk9Ah}~s9XnI;J39LeR2{d{2kgiDq zO{D;g7XKFLtj}FhAyq|>tcIY^^Pt4C-yl8Le=!8z=F)J#KgW_c=MI|Ye-K#t)jrUq z%E}SG_6}%1xcdu%$!s5EQCW;TplPIU%`6DySh%rJ%>a@OnioX?z4@>_cEv&w0@ROX zsxm1FnjK|YB*&`|H0yf5Ca~o~x(Lpb2a%k=ghF~kCt{kcO}PehF;Elg-elYW41 z3muON#FYd4W*bwT@197IVupsq`26g3u1e{e=F-`d-c^bZeEggfuluYy|Ls4$@@RUt zYjF=mLYi?UJy<_K$Wy=2P1O@jRSt~KUwhtdUVC&FiIaqk{$3!iJt@(szO6}#obGB% zvT5|E4Z}g3@fT(exL`NNOA=?Y{-p8w(`dmR*D!fgJzj&%aoO>EwkmFkG-A^@R0v~P0D>b12@ts{Jg@^#lfw6FFfz%Tn;+ozX|qc`&Y*(QPJLVc;1I<-O&-3 zO|Y8UzkHJtu@pxE#?>xG)13be&--BdUYxAp!rmM&V0AIwEDrFzO?#y{xtJ>hC}x?M zZWlvon3Io_3uOVYnzS!bBCV_zf{JZksLrb#rgZP4DA64M7A%~X=en4}qE}(eGX2Lm zxgj_m*6LdkW~Q)2L&L_+IC(302f#%UCZ?@x1-Mt(JkgO?R>9s>9!{Y|N_!=wuUWbb z)v>pNW{f@qCp)WQZ;s2AMyAawfS`u#T{sz94e8?_wwswYlY)i{xFJI{oLT{eA5kJF zX%Ph7wYj4@ln0tMmsOM~NN^oA@pdCq`1cQCOo~o{ldDkuKEufYoIbVGft2V|*&!J7AGs&0TQUWjl3oMA9rV$? zPrd}zt(<~%$t5c#lAJPvd%VQN9MtF`=vHGmPLAb6dY0QdNH_68vq!!RO=DLpQ~)0gb_LethIK>A#D1HfN30FUhUKy`jTpxJyp5-0Z%J3(`+yO}9BWxOXw-7FU1+r*`5bs|il6wyGjZbY?q!bo1oSsBS|lt!W;kME59o!we|zK{d-B yz?kz5DL5IF4@)Y`A-B}=y=U*k!z)G|hL~r=y2Tx&{XV>t8Ij?!VTO&{5BvcFr+!@k literal 1959 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3725f2acbc06c0a3f653c1ce22813cfa46fef3b8 GIT binary patch literal 5905 zcmcgwXH*kRmnMJ^sx*N})JQ^m5h00&fYJo%grYCyl_Ca+2uKN4X#!F-RH-HurCE?J zf(l46frKJOAb=o72mz7aLUH-_oc(tH?vLFcbMG@}=G?hu&dfbek`=~Ci2o!%2M33c ziLrsrk?#7}0P-BkH};hUM+$(qF}lK0*#~AFO}KG-7J3{UHK_vouE#hyBtMuK=v@zW zUYew&c-zs*s}Wrq*yk2xnCR7dQS+A?w`R!2pQn}^_b1;^lsbaY--Og17vjLz($I&U3c{o{U2y95?37~&>C^kI z@s4{sYw5de>A;lr$=U;Bu9C@#U-i6U+aH+j7LAM)&-aJy-JK<0m#`J8iXA2r^ln<) zV$@09MFAS}?X9-`YQZ+R=X%ac_B*ilJMY#^<VoZ6e(?6#>?yVUv_A)YIWu5`fK-EH3diF^>;qZW4eZJ_e(t7?4o$3>^1 zjhW;94{DWTTt`a>CZ-y+?fsegISuR2j=wo>N>{f9?f+5#grd<&Mr4an7;(wF1xoAP zY835nHXWTLdU--s0p2Tkl?<=f44dEZlmq#hj~d_Z^auRPHKk|e=h&A{p7D&%tbNF1 z{6A~(-_`g(L6h(PtHf^>J8XXA9UIjm?nE@EJ8}N}!LH8S1Ha~=pX54jpHrGCbV1jY zeub+fQ{~?PyeGLx(V5zp^m@Bjc)FUuidWlDl5#w9<=N$VJzn2WWq%HqtM|8Cal50f zb8D@Coa`Dr92Bhk?UytD?Bgnxax z&!S5?%Q8w+q7A)e5+NZ?mvEb1SnH3%3{C&h7AV}Ixp>@Ills915}onA&n~YfEee!> z?*Xi}Z2Jg{^zF|Z^|$+HS%y)cUEX|&x7uIwO;}rWu^j4;MA43{9*^wwhL4nAKMDb% z8@c_#lRc}7+x@IM$6?aiRebuQMQUj0GvL!&7hs~!p}#QytZ&pMi&pm6XT~v5Z}vX3 ze=SDJe5kJAZHpg&9~hz<);-5D(<8ghXPIlZ_6yg)Mq>8J%E0PmcD{vJ4sEyU(2hr{ zDc>@Pw~U}FXtAm*hkfQw?(X)Nvr;YzXUFiCvO+5utEoDqh%V@vrpW4FLal!eE*#9M z<2nB}EeM$kQw+z&S-2c5c;4&v@(82TMx-!p{(xG)cQg?4&@vKX5%TTgCFrWd2ud+l zgjJ^RWt2xEc$wBE-b~-rD#z#{M_44f?nEKU;fT5 zq);Vxa(j&8ypWmgw)<<$#z@N}X;=yQg7JP)crknq$!zH5IWkkDc#(+T6WC^TJSn_< zW--B`V~slg_I}y{lus~BslztgCOkHEMaax!&juASweB&^P~B(LPlm4tuCx81l=C%H ze-6WvKJ&df;qFu~s`1ni}9kZN~5Nga~*AR{TWv*Lu( z-myPv(NL2YN-8rdRkaHKQ!R{A)%@9qs*ZMn|GFGUPa7|gH*<5HY+RB`WI9d3q;s1e z;Ia?~C|un0VR2CiqoobrZKE=@miX(+TD}o%6r(?`Ws35@{(;eCJ&a6I-kghcGgrc= z1}H5Ue-riARIhzmX??W*jm31Bfhy~eTDF^;E_66X9e|~&Ozn^ehLZiJuh;R^rNcYo z5k->p>3B`zo&28Hs9G}xWUSjr_2rDSvJ!V3HtWlKOCtf$l#A_(DAuSWGG`$Xs|Z>tk^T^*rZ0+|M%*;y z2J&tNKoBWU7z^bs^f)o>x~X_wy%6~c)5FN|$ta0Klv1~aqL^9>+MM&7{m0aF!dK7Z$<%XD+oSr2d8KLGfn(bX8`s-IfQX+53P`V#|HqR3}8 za!H~GU32zL=EmbU74H2#7{z{3Z?3Iuhty4R@|z5x1&VW73(j`$XR7LV3rk$t{fS0r zGBrR(>Q;G9YMqIB+3qk<<~pAsowgnm3yoMO4sB#r_0;9bxftYU^9z=r-h54wc558! zfsD@v#%Q_(EF>V}W%hJ+3QTSa+&Ylez-13eVdT!D%YXYucs*@lf9-(GuXdHFUgE%vvwr1L77VRhR<1vk$ z^ygqO{rqhsMzc}S+A=)3gRidQ_(@1~{kWZt_|?$lc_&Z=+|$MiFM)IQXTD%MU3K#Y962&@K%Qcf>E+ z$8>zBeD7)!?aTVE7J>YY764T<(K?@BDG=MG3u3U+=2cE>5CZjXSpMwfF!(%^uYtn?PGn9Y?++yk1Zdj!r z@(Zgfbmji-G=u}?IrnG>KKTxzaab$(n=8b?+GfIhQUN~-23Ye%tj9iy0>AGU&hU>K zhY~qqTkbN2tkGI(d z2z!CHBlLI7a zm8qlC8lUZ&d_x`+Z4AtgAKBP4>l>KSPIDMU(=(dUA4ch%pBl5V2klaGqf65}=M$L++qpfY#5s6PCVL^tH<3HP{H|==2F0A&@KYmrh(!ffDjmL(`+uZ ziM+~p?1}kT37_B^52oYqn!xVUolhN|_I|?1P~#0HGnJHzsyr?CC>f`=rE!0;h-8LD z;Lkhlr?Z>A***&<`5NRBY1%D;5|9O8d=?0+m;~(AuH=n;!>}=#4LWqr}jZoK=(;{ z=UyLWw^L*En<}~o!U?PEZ+RXi*tZhdL;}TOy{WIT|4MvfoG$L@1T1Z?ZF zNw?>|%pAe#`4snhauYzZQR3pO%C;C;H8lUXFXCuuzA+eV^=*QL+E12Y@4 z|8*IVSBwFxZr$6nXX_s_(M5BLQ(f)x)=n(`w*U37>+-`=>6#Ve2-*i zs|kJY3Mb6P?=FAZInXv-cVF<}(sUkITt{sJ$5>omd#E^s4D}O(c+c&;C_(WgHjL#* za(7w#6ewU$LBN**h(3r!UznoTkYKHf%#r|Knv+MG+hl}p6e1QVG99g;i*m<5_~cAa zfa#9K=Vv3kldc4JkR+@s#U>|H14amKB2KNJcU# zX_3+5YKf>GjA>zhh!)YT5ML!|v_@b|Ryl7X>l?m@I?|AhMQX$yv2(2&0R`(X@k*Z1 zO@%bV#7=RnPj`D*#@|F5hRB6O|=`JTqWZ9+5`T2P})cycG0E6?cVA z+9mlEP##_mQlGxXM}Zd@w=jT#$dmM%201aTt)YTw61LMm{F-GC52DT@Dv+52G~1v@ zblN0f3GdT7KVGk<+p^oAv`k9z{%zDbw%N;nR<_z`{wR(?dzPB!rcDP;P*o*S=wM9Y ziV>%UQ=AXu3rtxIYr`+7bIm3v!#Vqlx|0z?#t_C)A@*fN8XXkvH>GIjX?3`NAu47e zL?g3!eK!vktCsOjH%{!Go(baOOg)vGR!azt=R%fbAh`7mi0rHAmjQv-Wo1$T89m01 zNM}7!>t(TR0z&g0b}1!jo(VKc(&l4U8@S*85z2R?TA%9XkeBHv(Ke$(9PD?RSHg6Z zW#b|fO~6J`p6V$l>WhWm+p#MO5^-x=$`A6~B%XOO)^8=eP`-z?EM#d#-B*S4+qRUr zQRKqUUzz5VUjkWj9ykk#5UlU2b%&Vd)Gda^G) zN%s`;v!gIWp?&RX>5ZDsL&--oCDI#n)P>v`zFmEPOXMt=eX#_ovu zNYs&0=PqURX2JCSvgckBJ7De)yn~pvW^t+merPI~#>=t0gGEvGAFVCp@F}5&8E3_w zN4w^XCF1u~6GdGLi|0JjdCv2{(I+Zz421@PyV#LxX@Sg4aQVH2|;{QEYlV4 zI%`sUy|x#zem?yz+8_?wbsmf^y^o10BEYj?!eTR|h5pRDk~5?{6EqctzLkw}NTrPx zdsFX0vz4)F=fz_Kl3H+1mDed_zJ$@<>b(yEWMEaqt5tK-#xO2v`^herA?lPRPBwdIi#oXdnQE{4vhCRtb{*3#zw1vGF zzCk6^2CZn*N*P~L);zNj2FWVtt;7tW_NPM(Q)O(S=_xnb>SVj~lFyD-Mk;iUP9>A|?T zT6dA@=sQ3JzbvKkGP%Zk2u(pfAywH&f6_%juxN5~uq^0bMgLya zMhB-hkQ>t1E)bTkPJ4z79Imhe8k{>{J;BX;DDCwgku@oAY^d@lA*J5W05(*~{}l+{ zy|{d1G@*2o+34rW9zLS<{>|?GKcj$x+_FDhfU9-VM%xNb!1@J z*w6hZkrl|*^>lFzsfc@fcVS*?s6^XCKHq>2FBQJ-H3}`=2Odm_kdbq4YOq#d;WQ6u zeaIKUa*%^XsIi&l+Wq2LCU3L$Z#AF0F(MRJMfd&p*>N!&ha? z=hHWI+IFi6Mg7dn5j#{X%$a+|edo*RUv9b_T5M$-7r*uXu{ina_n)rr;(DF)=Fuln zo8nTXGk#qr>YTY9EKCy}6ofcjm>NA41UOlg8XZ&w5P}EqdDw6?nSFElbXa1`MpL22 z3+JYJusC_Ip2*R%Elf#}W9?E8mP7U59##C^zpB6JzU`fRpNihhKKxd>bhT%4r8rp;nX#c1#@*LIshHz5UoAYAzM@Ef!yYdbzy~$*_#d4`KxR(>wfeM zX!mA?G+|ENvq?L$l@`v82*=qvRM@vPM5M&m=>;`3Jffudm8s7>Gy@t zTlY)jdoM6-^85Ez?%4bE`Ns47+b>+cp1gVT)2vb^$7sz7vph1gRY7E?DTqw>Rxq6i z^q@nQ(!yT<<7t9RI$O+MowWnGdW#~?Sc5pRT6yJuTOQpvolE}x<-`?vw^$$D+;b;H z{^>R2`t&;a)#?Av{jvP^_n}zTs^uro`%PPYm6IiRDaih{Q$gg?s~~c&E+_)EqnUvb zX!dG<^K5XGtuX(s(_}L7LL0V}gcNLVdY^O~W+eKn?C*1wj#d`rc$?D+47M#&oen_X z1H&$=>&Zptk2!I2(X~^e`I~2~OlPdG*fF=Na*vaDe(zbLT=k75sCt8kUw?Q-*FOns zY3uzpTi5N|x9>et^0&{sNXV%+9xCf)*MGQEWAyFnL$MvFr%gW6AF*YNP^U%4+kGBK v7!T_R6&^6Xt}Mvz6C&}5fuVv&UHw1P$(b*Hr