Browse Source

Skia backend cleanup.

pull/1679/head
Nelson Carrillo 8 years ago
parent
commit
88bfdf87ea
  1. 44
      Avalonia.sln
  2. 1
      build.cake
  3. 4
      build/SkiaSharp.props
  4. 4
      samples/ControlCatalog.NetCore/Program.cs
  5. 3
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  6. 1
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  7. 16
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  8. 52
      src/Avalonia.Visuals/Vector.cs
  9. 142
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  10. 783
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  11. 22
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  12. 211
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  13. 170
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  14. 47
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  15. 35
      src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
  16. 23
      src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
  17. 92
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  18. 80
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  19. 28
      src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs
  20. 46
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  21. 6
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  22. 124
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  23. 169
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  24. 62
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  25. 9
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  26. 151
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  27. 34
      src/Skia/Avalonia.Skia/readme.md
  28. 28
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  29. 2
      src/Windows/Avalonia.Win32/WindowImpl.cs
  30. 2
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  31. 12
      tests/Avalonia.RenderTests/Media/ImageBrushTests.cs
  32. 12
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  33. 6
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  34. 6
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  35. 22
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  36. 79
      tests/Avalonia.Skia.UnitTests/HitTesting.cs
  37. 8
      tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs
  38. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png
  39. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png
  40. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png
  41. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  42. BIN
      tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png

44
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\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props
build\Moq.props = build\Moq.props build\Moq.props = build\Moq.props
build\NetCore.props = build\NetCore.props build\NetCore.props = build\NetCore.props
build\NetFX.props = build\NetFX.props
build\ReactiveUI.props = build\ReactiveUI.props build\ReactiveUI.props = build\ReactiveUI.props
build\Rx.props = build\Rx.props build\Rx.props = build\Rx.props
build\SampleApp.props = build\SampleApp.props build\SampleApp.props = build\SampleApp.props
@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX
EndProject 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}" 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 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 Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 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|NetCoreOnly.ActiveCfg = Release|Any CPU
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -2520,6 +2563,7 @@ Global
{F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2} {CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2}
{4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
build.cake

@ -207,6 +207,7 @@ Task("Run-Unit-Tests")
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false);
if (data.Parameters.IsRunningOnWindows) if (data.Parameters.IsRunningOnWindows)
{ {
RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true);

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.57.1" /> <PackageReference Include="SkiaSharp" Version="1.60.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.57.1.3" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.60.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

4
samples/ControlCatalog.NetCore/Program.cs

@ -1,9 +1,9 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Avalonia; using Avalonia;
using Avalonia.Skia;
namespace ControlCatalog.NetCore namespace ControlCatalog.NetCore
{ {
@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore
/// This method is needed for IDE previewer infrastructure /// This method is needed for IDE previewer infrastructure
/// </summary> /// </summary>
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>().UsePlatformDetect().UseReactiveUI(); => AppBuilder.Configure<App>().UsePlatformDetect().UseSkia().UseReactiveUI();
static void ConsoleSilencer() static void ConsoleSilencer()
{ {

3
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -117,6 +117,9 @@ namespace Avalonia.Rendering
var scene = Interlocked.Exchange(ref _scene, null); var scene = Interlocked.Exchange(ref _scene, null);
scene?.Dispose(); scene?.Dispose();
Stop(); Stop();
Layers.Clear();
RenderTarget?.Dispose();
} }
/// <inheritdoc/> /// <inheritdoc/>

1
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -133,6 +133,7 @@ namespace Avalonia.Rendering
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
_renderTarget?.Dispose();
} }
/// <inheritdoc/> /// <inheritdoc/>

16
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@ -11,11 +11,7 @@ namespace Avalonia.Rendering
{ {
private List<RenderLayer> _inner = new List<RenderLayer>(); private List<RenderLayer> _inner = new List<RenderLayer>();
private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>(); private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
public RenderLayers()
{
}
public int Count => _inner.Count; public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; 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) public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
{ {
return _index.TryGetValue(layerRoot, out value); return _index.TryGetValue(layerRoot, out value);

52
src/Avalonia.Visuals/Vector.cs

@ -3,7 +3,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Xml.Linq; using JetBrains.Annotations;
namespace Avalonia namespace Avalonia
{ {
@ -122,6 +122,56 @@ namespace Avalonia
return new Vector(a._x - b._x, a._y - b._y); return new Vector(a._x - b._x, a._y - b._y);
} }
/// <summary>
/// Check if two vectors are equal (bitwise).
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(Vector other)
{
// ReSharper disable CompareOfFloatsByEqualityOperator
return _x == other._x && _y == other._y;
// ReSharper restore CompareOfFloatsByEqualityOperator
}
/// <summary>
/// Check if two vectors are nearly equal (numerically).
/// </summary>
/// <param name="other">The other vector.</param>
/// <returns>True if vectors are nearly equal.</returns>
[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);
}
/// <summary> /// <summary>
/// Returns the string representation of the point. /// Returns the string representation of the point.
/// </summary> /// </summary>

142
src/Skia/Avalonia.Skia/BitmapImpl.cs

@ -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<IRuntimePlatform>();
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<IRuntimePlatform>();
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);
}
}

783
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -1,57 +1,114 @@
using Avalonia.Media; // Copyright (c) The Avalonia Project. All rights reserved.
using SkiaSharp; // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Utilities; using Avalonia.Rendering.Utilities;
using Avalonia.Utilities; using Avalonia.Utilities;
using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
internal class DrawingContextImpl : IDrawingContextImpl /// <summary>
/// Skia based drawing context.
/// </summary>
public class DrawingContextImpl : IDrawingContextImpl
{ {
private readonly IDisposable[] _disposables;
private readonly Vector _dpi; private readonly Vector _dpi;
private readonly Stack<PaintWrapper> _maskStack = new Stack<PaintWrapper>();
private readonly Stack<double> _opacityStack = new Stack<double>();
private readonly Matrix? _postTransform; private readonly Matrix? _postTransform;
private readonly IDisposable[] _disposables;
private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly IVisualBrushRenderer _visualBrushRenderer;
private Stack<PaintWrapper> maskStack = new Stack<PaintWrapper>(); private double _currentOpacity = 1.0f;
protected bool CanUseLcdRendering = true; private readonly bool _canTextUseLcdRendering;
public SKCanvas Canvas { get; private set; } private Matrix _currentTransform;
public DrawingContextImpl( /// <summary>
SKCanvas canvas, /// Context create info.
Vector dpi, /// </summary>
IVisualBrushRenderer visualBrushRenderer, public struct CreateInfo
params IDisposable[] disposables) {
/// <summary>
/// Canvas to draw to.
/// </summary>
public SKCanvas Canvas;
/// <summary>
/// Dpi of drawings.
/// </summary>
public Vector Dpi;
/// <summary>
/// Visual brush renderer.
/// </summary>
public IVisualBrushRenderer VisualBrushRenderer;
/// <summary>
/// Render text without Lcd rendering.
/// </summary>
public bool DisableTextLcdRendering;
}
/// <summary>
/// Create new drawing context.
/// </summary>
/// <param name="createInfo">Create info.</param>
/// <param name="disposables">Array of elements to dispose after drawing has finished.</param>
public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables)
{ {
_dpi = dpi; _dpi = createInfo.Dpi;
if (dpi.X != 96 || dpi.Y != 96) _visualBrushRenderer = createInfo.VisualBrushRenderer;
_postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
_visualBrushRenderer = visualBrushRenderer;
_disposables = disposables; _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; Transform = Matrix.Identity;
} }
/// <summary>
/// Skia canvas.
/// </summary>
public SKCanvas Canvas { get; }
/// <inheritdoc />
public void Clear(Color color) public void Clear(Color color)
{ {
Canvas.Clear(color.ToSKColor()); Canvas.Clear(color.ToSKColor());
} }
/// <inheritdoc />
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{ {
var impl = (BitmapImpl)source.Item; var drawableImage = (IDrawableBitmapImpl) source.Item;
var s = sourceRect.ToSKRect(); var s = sourceRect.ToSKRect();
var d = destRect.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);
} }
} }
/// <inheritdoc />
public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{ {
PushOpacityMask(opacityMask, opacityMaskRect); PushOpacityMask(opacityMask, opacityMaskRect);
@ -59,17 +116,19 @@ namespace Avalonia.Skia
PopOpacityMask(); PopOpacityMask();
} }
/// <inheritdoc />
public void DrawLine(Pen pen, Point p1, Point p2) 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)))) 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);
} }
} }
/// <inheritdoc />
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{ {
var impl = (GeometryImpl)geometry; var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size; var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
@ -79,6 +138,7 @@ namespace Avalonia.Skia
{ {
Canvas.DrawPath(impl.EffectivePath, fill.Paint); Canvas.DrawPath(impl.EffectivePath, fill.Paint);
} }
if (stroke.Paint != null) if (stroke.Paint != null)
{ {
Canvas.DrawPath(impl.EffectivePath, stroke.Paint); Canvas.DrawPath(impl.EffectivePath, stroke.Paint);
@ -86,227 +146,424 @@ namespace Avalonia.Skia
} }
} }
private struct PaintState : IDisposable /// <inheritdoc />
public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
{ {
private readonly SKColor _color; using (var paint = CreatePaint(pen, rect.Size))
private readonly SKShader _shader; {
private readonly SKPaint _paint; 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);
}
}
}
/// <inheritdoc />
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
{
using (var paint = CreatePaint(brush, rect.Size))
{ {
_paint = paint; var rc = rect.ToSKRect();
_color = color;
_shader = shader; if (Math.Abs(cornerRadius) < float.Epsilon)
{
Canvas.DrawRect(rc, paint.Paint);
}
else
{
Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
}
} }
}
public void Dispose() /// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
using (var paint = CreatePaint(foreground, text.Size))
{ {
_paint.Color = _color; var textImpl = (FormattedTextImpl) text;
_paint.Shader = _shader; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
} }
} }
internal struct PaintWrapper : IDisposable /// <inheritdoc />
public IRenderTargetBitmapImpl CreateLayer(Size size)
{ {
//We are saving memory allocations there var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
//TODO: add more disposable fields if needed var pixelSize = size * normalizedDpi;
public readonly SKPaint Paint;
private IDisposable _disposable1; return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi);
private IDisposable _disposable2; }
public IDisposable ApplyTo(SKPaint paint) /// <inheritdoc />
{ public void PushClip(Rect clip)
var state = new PaintState(paint, paint.Color, paint.Shader); {
Canvas.Save();
Canvas.ClipRect(clip.ToSKRect());
}
paint.Color = Paint.Color; /// <inheritdoc />
paint.Shader = Paint.Shader; public void PopClip()
{
Canvas.Restore();
}
return state; /// <inheritdoc />
} public void PushOpacity(double opacity)
{
_opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity;
}
public void AddDisposable(IDisposable disposable) /// <inheritdoc />
{ public void PopOpacity()
if (_disposable1 == null) {
_disposable1 = disposable; _currentOpacity = _opacityStack.Pop();
else if (_disposable2 == null) }
_disposable2 = disposable;
else
throw new InvalidOperationException();
}
public PaintWrapper(SKPaint paint) /// <inheritdoc />
public virtual void Dispose()
{
if (_disposables == null)
{ {
Paint = paint; return;
_disposable1 = null;
_disposable2 = null;
} }
public void Dispose() foreach (var disposable in _disposables)
{ {
Paint?.Dispose(); disposable?.Dispose();
_disposable1?.Dispose();
_disposable2?.Dispose();
} }
} }
internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) /// <inheritdoc />
public void PushGeometryClip(IGeometryImpl clip)
{ {
SKPaint paint = new SKPaint(); Canvas.Save();
var rv = new PaintWrapper(paint); Canvas.ClipPath(((GeometryImpl)clip).EffectivePath);
paint.IsStroke = false; }
/// <inheritdoc />
double opacity = brush.Opacity * _currentOpacity; public void PopGeometryClip()
paint.IsAntialias = true; {
Canvas.Restore();
}
/// <inheritdoc />
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; /// <inheritdoc />
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)); Canvas.SaveLayer(paint);
return rv; 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; Canvas.Restore();
if (gradient != null) }
/// <inheritdoc />
public Matrix Transform
{
get { return _currentTransform; }
set
{ {
var tileMode = gradient.SpreadMethod.ToSKShaderTileMode(); if (_currentTransform == value)
var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); return;
var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray();
_currentTransform = value;
var transform = value;
var linearGradient = brush as ILinearGradientBrush; if (_postTransform.HasValue)
if (linearGradient != null) {
transform *= _postTransform.Value;
}
Canvas.SetMatrix(transform.ToSKMatrix());
}
}
/// <summary>
/// Configure paint wrapper for using gradient brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="gradientBrush">Gradient brush.</param>
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 start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
// would be nice to cache these shaders possibly? // would be nice to cache these shaders possibly?
using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) using (var shader =
paint.Shader = shader; SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
{
paintWrapper.Paint.Shader = shader;
}
break;
} }
else case IRadialGradientBrush radialGradient:
{ {
var radialGradient = brush as IRadialGradientBrush; var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
if (radialGradient != null) var radius = (float)(radialGradient.Radius * targetSize.Width);
{
var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
var radius = (float)radialGradient.Radius;
// TODO: There is no SetAlpha in SkiaSharp // TODO: There is no SetAlpha in SkiaSharp
//paint.setAlpha(128); //paint.setAlpha(128);
// would be nice to cache these shaders possibly?
using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
paint.Shader = shader;
// would be nice to cache these shaders possibly?
using (var shader =
SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
{
paintWrapper.Paint.Shader = shader;
} }
break;
} }
}
}
/// <summary>
/// Configure paint wrapper for using tile brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="tileBrush">Tile brush to use.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
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 tileTransform =
var visualBrush = brush as IVisualBrush; tileBrush.TileMode != TileMode.None
var tileBrushImage = default(BitmapImpl); ? 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) paintWrapper.Paint.Shader = shader;
{ }
var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); }
if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) /// <summary>
{ /// Configure paint wrapper to use visual brush.
var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); /// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="visualBrush">Visual brush.</param>
/// <param name="visualBrushRenderer">Visual brush renderer.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
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)) var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
{
ctx.Clear(Colors.Transparent);
_visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
}
tileBrushImage = intermediate; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
rv.AddDisposable(tileBrushImage); {
} var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
}
else 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 }
/// <summary>
/// Creates paint wrapper for given brush.
/// </summary>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <returns>Paint wrapper for given brush.</returns>
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); ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
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);
context.Clear(Colors.Transparent); return paintWrapper;
context.PushClip(calc.IntermediateClip); }
context.Transform = calc.IntermediateTransform;
context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); var tileBrush = brush as ITileBrush;
context.PopClip(); var visualBrush = brush as IVisualBrush;
} var tileBrushImage = default(IDrawableBitmapImpl);
SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y); if (visualBrush != null)
SKShaderTileMode tileX = {
tileBrush.TileMode == TileMode.None ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage);
? SKShaderTileMode.Clamp }
: tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY else
? SKShaderTileMode.Mirror {
: SKShaderTileMode.Repeat; tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
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;
} }
return rv; if (tileBrush != null && tileBrushImage != null)
{
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
}
return paintWrapper;
} }
/// <summary>
/// Creates paint wrapper for given pen.
/// </summary>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <returns></returns>
private PaintWrapper CreatePaint(Pen pen, Size targetSize) private PaintWrapper CreatePaint(Pen pen, Size targetSize)
{ {
var rv = CreatePaint(pen.Brush, targetSize); var rv = CreatePaint(pen.Brush, targetSize);
var paint = rv.Paint; var paint = rv.Paint;
paint.IsStroke = true; paint.IsStroke = true;
paint.StrokeWidth = (float)pen.Thickness; paint.StrokeWidth = (float) pen.Thickness;
if (pen.StartLineCap == PenLineCap.Round) // Need to modify dashes due to Skia modifying their lengths
paint.StrokeCap = SKStrokeCap.Round; // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
else if (pen.StartLineCap == PenLineCap.Square) // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
paint.StrokeCap = SKStrokeCap.Square; float dashLengthModifier;
else float gapLengthModifier;
paint.StrokeCap = SKStrokeCap.Butt;
if (pen.LineJoin == PenLineJoin.Miter) switch (pen.StartLineCap)
paint.StrokeJoin = SKStrokeJoin.Miter; {
else if (pen.LineJoin == PenLineJoin.Round) case PenLineCap.Round:
paint.StrokeJoin = SKStrokeJoin.Round; paint.StrokeCap = SKStrokeCap.Round;
else dashLengthModifier = -paint.StrokeWidth;
paint.StrokeJoin = SKStrokeJoin.Bevel; 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) if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
{ {
var pe = SKPathEffect.CreateDash( var srcDashes = pen.DashStyle.Dashes;
pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(), var dashesArray = new float[srcDashes.Count];
(float)pen.DashStyle.Offset);
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; paint.PathEffect = pe;
rv.AddDisposable(pe); rv.AddDisposable(pe);
} }
@ -314,128 +571,118 @@ namespace Avalonia.Skia
return rv; return rv;
} }
public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) /// <summary>
/// Create new render target compatible with this drawing context.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="dpi">Drawing dpi.</param>
/// <param name="format">Pixel format.</param>
/// <returns></returns>
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(); Width = width,
if (cornerRadius == 0) Height = height,
{ Dpi = dpi,
Canvas.DrawRect(rc, paint.Paint); Format = format,
} DisableTextLcdRendering = !_canTextUseLcdRendering
else };
{
Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); return new SurfaceRenderTarget(createInfo);
}
}
} }
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) /// <summary>
/// Skia cached paint state.
/// </summary>
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(); _paint = paint;
if (cornerRadius == 0) _color = color;
{ _shader = shader;
Canvas.DrawRect(rc, paint.Paint);
}
else
{
Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
}
} }
}
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) /// <inheritdoc />
{ public void Dispose()
using (var paint = CreatePaint(foreground, text.Size))
{ {
var textImpl = (FormattedTextImpl)text; _paint.Color = _color;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering); _paint.Shader = _shader;
} }
} }
public IRenderTargetBitmapImpl CreateLayer(Size size) /// <summary>
{ /// Skia paint wrapper.
var pixelSize = size * (_dpi / 96); /// </summary>
return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi); internal struct PaintWrapper : IDisposable
}
public void PushClip(Rect clip)
{
Canvas.Save();
Canvas.ClipRect(clip.ToSKRect());
}
public void PopClip()
{
Canvas.Restore();
}
private double _currentOpacity = 1.0f;
private readonly Stack<double> _opacityStack = new Stack<double>();
public void PushOpacity(double opacity)
{ {
_opacityStack.Push(_currentOpacity); //We are saving memory allocations there
_currentOpacity *= opacity; public readonly SKPaint Paint;
}
public void PopOpacity() private IDisposable _disposable1;
{ private IDisposable _disposable2;
_currentOpacity = _opacityStack.Pop(); private IDisposable _disposable3;
}
public virtual void Dispose() public PaintWrapper(SKPaint paint)
{ {
if(_disposables!=null) Paint = paint;
foreach (var disposable in _disposables)
disposable?.Dispose();
}
public void PushGeometryClip(IGeometryImpl clip) _disposable1 = null;
{ _disposable2 = null;
Canvas.Save(); _disposable3 = null;
Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath); }
}
public void PopGeometryClip() public IDisposable ApplyTo(SKPaint paint)
{ {
Canvas.Restore(); var state = new PaintState(paint, paint.Color, paint.Shader);
}
public void PushOpacityMask(IBrush mask, Rect bounds) paint.Color = Paint.Color;
{ paint.Shader = Paint.Shader;
Canvas.SaveLayer(new SKPaint());
maskStack.Push(CreatePaint(mask, bounds.Size));
}
public void PopOpacityMask() return state;
{
Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn });
using (var paintWrapper = maskStack.Pop())
{
Canvas.DrawPaint(paintWrapper.Paint);
} }
Canvas.Restore();
Canvas.Restore();
}
private Matrix _currentTransform; /// <summary>
/// Add new disposable to a wrapper.
public Matrix Transform /// </summary>
{ /// <param name="disposable">Disposable to add.</param>
get { return _currentTransform; } public void AddDisposable(IDisposable disposable)
set
{ {
if (_currentTransform == value) if (_disposable1 == null)
return; {
_disposable1 = disposable;
}
else if (_disposable2 == null)
{
_disposable2 = disposable;
}
else if (_disposable3 == null)
{
_disposable3 = disposable;
}
else
{
Debug.Assert(false);
_currentTransform = value; // ReSharper disable once HeuristicUnreachableCode
var transform = value; throw new InvalidOperationException(
if (_postTransform.HasValue) "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables.");
transform *= _postTransform.Value; }
Canvas.SetMatrix(transform.ToSKMatrix()); }
/// <inheritdoc />
public void Dispose()
{
Paint?.Dispose();
_disposable1?.Dispose();
_disposable2?.Dispose();
_disposable3?.Dispose();
} }
} }
} }

22
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -10,6 +10,9 @@ using System.Linq;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
/// <summary>
/// Skia formatted text implementation.
/// </summary>
public class FormattedTextImpl : IFormattedTextImpl public class FormattedTextImpl : IFormattedTextImpl
{ {
public FormattedTextImpl( public FormattedTextImpl(
@ -21,7 +24,7 @@ namespace Avalonia.Skia
IReadOnlyList<FormattedTextStyleSpan> spans) IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Text = text ?? string.Empty; Text = text ?? string.Empty;
// Replace 0 characters with zero-width spaces (200B) // Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B); Text = Text.Replace((char)0, (char)0x200B);
@ -352,7 +355,7 @@ namespace Avalonia.Skia
{ {
float measuredWidth; float measuredWidth;
string subText = textInput.Substring(textIndex, stop - textIndex); 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 //Check for white space or line breakers before the lengthBreak
@ -451,7 +454,6 @@ namespace Avalonia.Skia
private void BuildRects() private void BuildRects()
{ {
// Build character rects // Build character rects
var fm = _paint.FontMetrics;
SKTextAlign align = _paint.TextAlign; SKTextAlign align = _paint.TextAlign;
for (int li = 0; li < _skiaLines.Count; li++) for (int li = 0; li < _skiaLines.Count; li++)
@ -559,18 +561,16 @@ namespace Avalonia.Skia
string subString; string subString;
float widthConstraint = (_constraint.Width != double.PositiveInfinity) float widthConstraint = double.IsPositiveInfinity(_constraint.Width)
? (float)_constraint.Width ? -1
: -1; : (float)_constraint.Width;
for (int c = 0; curOff < length; c++) while(curOff < length)
{ {
float lineWidth = -1; float lineWidth = -1;
int measured; int measured;
int trailingnumber = 0; int trailingnumber = 0;
subString = Text.Substring(curOff);
float constraint = -1; float constraint = -1;
if (_wrapping == TextWrapping.Wrap) if (_wrapping == TextWrapping.Wrap)

211
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@ -1,82 +1,199 @@
using System; // Copyright (c) The Avalonia Project. All rights reserved.
using System.Collections.Generic; // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Text;
using System;
using System.Reactive.Disposables;
using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Skia.Helpers;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
/// <summary>
/// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
/// </summary>
public class FramebufferRenderTarget : IRenderTarget 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) /// <summary>
/// Create new framebuffer render target using a target surface.
/// </summary>
/// <param name="platformSurface">Target surface.</param>
public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface)
{ {
_surface = surface; _platformSurface = platformSurface;
} }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
//Nothing to do here, since we don't own framebuffer FreeSurface();
}
/// <inheritdoc />
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);
}
/// <summary>
/// Check if two images info are compatible.
/// </summary>
/// <param name="currentImageInfo">Current.</param>
/// <param name="desiredImageInfo">Desired.</param>
/// <returns>True, if images are compatible.</returns>
private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo)
{
return currentImageInfo.Width == desiredImageInfo.Width &&
currentImageInfo.Height == desiredImageInfo.Height &&
currentImageInfo.ColorType == desiredImageInfo.ColorType;
}
/// <summary>
/// Create Skia surface backed by given framebuffer.
/// </summary>
/// <param name="desiredImageInfo">Desired image info.</param>
/// <param name="framebuffer">Backing framebuffer.</param>
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;
}
/// <summary>
/// Free Skia surface.
/// </summary>
private void FreeSurface()
{
_conversionShim?.Dispose();
_conversionShim = null;
_preFramebufferCopyHandler = null;
if (_conversionShim != null)
{
_framebufferSurface?.Dispose();
}
_framebufferSurface = null;
_currentFramebufferAddress = IntPtr.Zero;
} }
class PixelFormatShim : IDisposable /// <summary>
/// Converts non-compatible pixel formats using bitmap copies.
/// </summary>
private class PixelFormatConversionShim : IDisposable
{ {
private readonly SKImageInfo _nfo; private readonly SKBitmap _bitmap;
private readonly IntPtr _fb; private readonly SKImageInfo _destinationInfo;
private readonly int _rowBytes; private readonly IntPtr _framebufferAddress;
private SKBitmap _bitmap;
public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes) public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress)
{ {
_nfo = nfo; _destinationInfo = destinationInfo;
_fb = fb; _framebufferAddress = framebufferAddress;
_rowBytes = rowBytes;
// 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);
if (Surface == null)
_bitmap = new SKBitmap(nfo.Width, nfo.Height);
if (!_bitmap.CanCopyTo(nfo.ColorType))
{ {
_bitmap.Dispose(); _bitmap.Dispose();
throw new Exception( 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); /// <summary>
/// Skia surface.
/// </summary>
public SKSurface Surface { get; }
/// <summary>
/// Handler to start conversion via surface copy.
/// </summary>
public IDisposable SurfaceCopyHandler { get; }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
using (var tmp = _bitmap.Copy(_nfo.ColorType)) Surface.Dispose();
tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes);
_bitmap.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;
/// <summary>
canvas.RestoreToCount(0); /// Convert and copy surface to a framebuffer.
canvas.Save(); /// </summary>
canvas.ResetMatrix(); private void CopySurface()
return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb); {
using (var snapshot = Surface.Snapshot())
{
snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0,
SKImageCachingHint.Disallow);
}
}
} }
} }
} }

170
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 System;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -5,14 +8,169 @@ using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
abstract class GeometryImpl : IGeometryImpl /// <summary>
/// A Skia implementation of <see cref="IGeometryImpl"/>.
/// </summary>
public abstract class GeometryImpl : IGeometryImpl
{ {
private PathCache _pathCache;
/// <inheritdoc />
public abstract Rect Bounds { get; } public abstract Rect Bounds { get; }
public abstract SKPath EffectivePath { get; } public abstract SKPath EffectivePath { get; }
public abstract bool FillContains(Point point);
public abstract Rect GetRenderBounds(Pen pen); /// <inheritdoc />
public abstract IGeometryImpl Intersect(IGeometryImpl geometry); public bool FillContains(Point point)
public abstract bool StrokeContains(Pen pen, Point point); {
public abstract ITransformedGeometryImpl WithTransform(Matrix transform); return PathContainsCore(EffectivePath, point);
}
/// <inheritdoc />
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);
}
/// <summary>
/// Update path cache for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width.</param>
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());
}
}
}
/// <summary>
/// Check Skia path if it contains a point.
/// </summary>
/// <param name="path">Path to check.</param>
/// <param name="point">Point.</param>
/// <returns>True, if point is contained in a path.</returns>
private static bool PathContainsCore(SKPath path, Point point)
{
return path.Contains((float)point.X, (float)point.Y);
}
/// <inheritdoc />
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect);
return result == null ? null : new StreamGeometryImpl(result);
}
/// <inheritdoc />
public Rect GetRenderBounds(Pen pen)
{
var strokeWidth = (float)(pen?.Thickness ?? 0);
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
}
/// <inheritdoc />
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new TransformedGeometryImpl(this, transform);
}
/// <summary>
/// Invalidate all caches. Call after chaning path contents.
/// </summary>
protected void InvalidateCaches()
{
_pathCache.Invalidate();
}
private struct PathCache
{
private float _cachedStrokeWidth;
/// <summary>
/// Tolerance for two stroke widths to be deemed equal
/// </summary>
public const float Tolerance = float.Epsilon;
/// <summary>
/// Cached contour path.
/// </summary>
public SKPath CachedStrokePath { get; private set; }
/// <summary>
/// Cached geometry render bounds.
/// </summary>
public Rect CachedGeometryRenderBounds { get; private set; }
/// <summary>
/// Is cached valid for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width to check.</param>
/// <returns>True, if CachedStrokePath can be used for given stroke width.</returns>
public bool HasCacheFor(float strokeWidth)
{
return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
}
/// <summary>
/// Cache path for given stroke width. Takes ownership of a passed path.
/// </summary>
/// <param name="path">Path to cache.</param>
/// <param name="strokeWidth">Stroke width to cache.</param>
/// <param name="geometryRenderBounds">Render bounds to use.</param>
public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
{
if (CachedStrokePath != path)
{
CachedStrokePath?.Dispose();
}
CachedStrokePath = path;
CachedGeometryRenderBounds = geometryRenderBounds;
_cachedStrokeWidth = strokeWidth;
}
/// <summary>
/// Invalidate cache state.
/// </summary>
public void Invalidate()
{
CachedStrokePath?.Dispose();
CachedGeometryRenderBounds = Rect.Empty;
_cachedStrokeWidth = default(float);
}
}
} }
} }

47
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
{
/// <summary>
/// Helps with saving images to stream/file.
/// </summary>
public static class ImageSavingHelper
{
/// <summary>
/// Save Skia image to a file.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="fileName">Target file.</param>
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);
}
}
/// <summary>
/// Save Skia image to a stream.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="stream">Target stream.</param>
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);
}
}
}
}

35
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
{
/// <summary>
/// Helps with resolving pixel formats to Skia color types.
/// </summary>
public static class PixelFormatHelper
{
/// <summary>
/// Resolve given format to Skia color type.
/// </summary>
/// <param name="format">Format to resolve.</param>
/// <returns>Resolved color type.</returns>
public static SKColorType ResolveColorType(PixelFormat? format)
{
var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
// TODO: This looks like some leftover hack
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
var runtime = runtimePlatform?.GetRuntimeInfo();
if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux)
{
colorType = SKColorType.Bgra8888;
}
return colorType;
}
}
}

23
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
{
/// <summary>
/// Extended bitmap implementation that allows for drawing it's contents.
/// </summary>
internal interface IDrawableBitmapImpl : IBitmapImpl
{
/// <summary>
/// Draw bitmap to a drawing context.
/// </summary>
/// <param name="context">Drawing context.</param>
/// <param name="sourceRect">Source rect.</param>
/// <param name="destRect">Destination rect.</param>
/// <param name="paint">Paint to use.</param>
void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint);
}
}

92
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
{
/// <summary>
/// Immutable Skia bitmap.
/// </summary>
public class ImmutableBitmap : IDrawableBitmapImpl
{
private readonly SKImage _image;
/// <summary>
/// Create immutable bitmap from given stream.
/// </summary>
/// <param name="stream">Stream containing encoded data.</param>
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;
}
}
/// <summary>
/// Create immutable bitmap from given pixel data copy.
/// </summary>
/// <param name="width">Width of data pixels.</param>
/// <param name="height">Height of data pixels.</param>
/// <param name="stride">Stride of data pixels.</param>
/// <param name="format">Format of data pixels.</param>
/// <param name="data">Data pixels.</param>
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;
}
/// <inheritdoc />
public int PixelWidth { get; }
/// <inheritdoc />
public int PixelHeight { get; }
/// <inheritdoc />
public void Dispose()
{
_image.Dispose();
}
/// <inheritdoc />
public void Save(string fileName)
{
ImageSavingHelper.SaveImage(_image, fileName);
}
/// <inheritdoc />
public void Save(Stream stream)
{
ImageSavingHelper.SaveImage(_image, stream);
}
/// <inheritdoc />
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
context.Canvas.DrawImage(_image, sourceRect, destRect, paint);
}
}
}

80
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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
public partial class PlatformRenderInterface : IPlatformRenderInterface /// <summary>
/// Skia platform render interface.
/// </summary>
public class PlatformRenderInterface : IPlatformRenderInterface
{ {
public IBitmapImpl CreateBitmap(int width, int height) /// <inheritdoc />
{
return CreateRenderTargetBitmap(width, height, 96, 96);
}
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
@ -27,27 +27,19 @@ namespace Avalonia.Skia
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
} }
/// <inheritdoc />
public IStreamGeometryImpl CreateStreamGeometry() public IStreamGeometryImpl CreateStreamGeometry()
{ {
return new StreamGeometryImpl(); return new StreamGeometryImpl();
} }
public IBitmapImpl LoadBitmap(System.IO.Stream stream) /// <inheritdoc />
public IBitmapImpl LoadBitmap(Stream stream)
{ {
using (var s = new SKManagedStream(stream)) return new ImmutableBitmap(stream);
{
var bitmap = SKBitmap.Decode(s);
if (bitmap != null)
{
return new BitmapImpl(bitmap);
}
else
{
throw new ArgumentException("Unable to load bitmap from provided data");
}
}
} }
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName) public IBitmapImpl LoadBitmap(string fileName)
{ {
using (var stream = File.OpenRead(fileName)) using (var stream = File.OpenRead(fileName))
@ -56,16 +48,13 @@ namespace Avalonia.Skia
} }
} }
/// <inheritdoc />
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
{ {
using (var tmp = new SKBitmap()) return new ImmutableBitmap(width, height, stride, format, data);
{
tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul)
, data, stride);
return new BitmapImpl(tmp.Copy());
}
} }
/// <inheritdoc />
public IRenderTargetBitmapImpl CreateRenderTargetBitmap( public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
int width, int width,
int height, int height,
@ -73,24 +62,47 @@ namespace Avalonia.Skia
double dpiY) double dpiY)
{ {
if (width < 1) if (width < 1)
{
throw new ArgumentException("Width can't be less than 1", nameof(width)); throw new ArgumentException("Width can't be less than 1", nameof(width));
}
if (height < 1) if (height < 1)
{
throw new ArgumentException("Height can't be less than 1", nameof(height)); 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);
} }
/// <inheritdoc />
public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{ {
var fb = surfaces?.OfType<IFramebufferPlatformSurface>().FirstOrDefault(); foreach (var surface in surfaces)
if (fb == null) {
throw new Exception("Skia backend currently only supports framebuffer render target"); if (surface is IFramebufferPlatformSurface framebufferSurface)
return new FramebufferRenderTarget(fb); {
return new FramebufferRenderTarget(framebufferSurface);
}
}
throw new NotSupportedException(
"Don't know how to create a Skia render target from any of provided surfaces");
} }
/// <inheritdoc />
public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) 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);
} }
} }
} }

28
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
{
/// <summary>
/// Skia appication extensions.
/// </summary>
public static class SkiaApplicationExtensions
{
/// <summary>
/// Enable Skia renderer.
/// </summary>
/// <typeparam name="T">Builder type.</typeparam>
/// <param name="builder">Builder.</param>
/// <param name="preferredBackendType">Preferred backend type.</param>
/// <returns>Configure builder.</returns>
public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new()
{
builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia");
return builder;
}
}
}

46
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;
using System.Collections.Generic; using Avalonia.Logging;
using System.Text;
using Avalonia.Controls;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia
{
public static class SkiaApplicationExtensions
{
public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new()
{
builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia");
return builder;
}
}
}
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
/// <summary>
/// Skia platform initializer.
/// </summary>
public static class SkiaPlatform public static class SkiaPlatform
{ {
private static bool s_forceSoftwareRendering; /// <summary>
/// Initialize Skia platform.
/// </summary>
public static void Initialize() public static void Initialize()
{ {
var renderInterface = new PlatformRenderInterface(); var renderInterface = new PlatformRenderInterface();
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface); .Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
} }
public static bool ForceSoftwareRendering /// <summary>
{ /// Default DPI.
get { return s_forceSoftwareRendering; } /// </summary>
set public static Vector DefaultDpi => new Vector(96.0f, 96.0f);
{
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();
}
}
} }
} }

6
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 System;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia
namespace Avalonia
{ {
public static class SkiaSharpExtensions public static class SkiaSharpExtensions
{ {

124
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.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl /// <summary>
/// A Skia implementation of a <see cref="IStreamGeometryImpl"/>.
/// </summary>
public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{ {
Rect _bounds; private Rect _bounds;
SKPath _path; private readonly SKPath _effectivePath;
public override SKPath EffectivePath => _path; /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
public override Rect GetRenderBounds(Pen pen) /// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
/// <param name="bounds">Precomputed path bounds.</param>
public StreamGeometryImpl(SKPath path, Rect bounds)
{ {
return GetRenderBounds(pen?.Thickness ?? 0); _effectivePath = path;
_bounds = bounds;
} }
public override Rect Bounds => _bounds; /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
public IStreamGeometryImpl Clone() /// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect())
{ {
return new StreamGeometryImpl
{
_path = _path?.Clone(),
_bounds = Bounds
};
} }
public IStreamGeometryContextImpl Open() /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty)
{ {
_path = new SKPath();
_path.FillType = SKPathFillType.EvenOdd;
return new StreamContext(this);
} }
/// <inheritdoc />
public override SKPath EffectivePath => _effectivePath;
public override bool FillContains(Point point) /// <inheritdoc />
{ public override Rect Bounds => _bounds;
// 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 IGeometryImpl Intersect(IGeometryImpl geometry) /// <inheritdoc />
public IStreamGeometryImpl Clone()
{ {
throw new NotImplementedException(); return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds);
} }
public override ITransformedGeometryImpl WithTransform(Matrix transform) /// <inheritdoc />
public IStreamGeometryContextImpl Open()
{ {
return new TransformedGeometryImpl(this, transform); return new StreamContext(this);
} }
private Rect GetRenderBounds(double strokeThickness) /// <summary>
/// Create new empty <see cref="SKPath"/>.
/// </summary>
/// <returns>Empty <see cref="SKPath"/></returns>
private static SKPath CreateEmptyPath()
{ {
// TODO: Calculate properly. return new SKPath
return Bounds.Inflate(strokeThickness); {
FillType = SKPathFillType.EvenOdd
};
} }
class StreamContext : IStreamGeometryContextImpl /// <summary>
/// A Skia implementation of a <see cref="IStreamGeometryContextImpl"/>.
/// </summary>
private class StreamContext : IStreamGeometryContextImpl
{ {
private readonly StreamGeometryImpl _geometryImpl; private readonly StreamGeometryImpl _geometryImpl;
private SKPath _path; private readonly SKPath _path;
Point _currentPoint; /// <summary>
/// Initializes a new instance of the <see cref="StreamContext"/> class.
/// <param name="geometryImpl">Geometry to operate on.</param>
/// </summary>
public StreamContext(StreamGeometryImpl geometryImpl) public StreamContext(StreamGeometryImpl geometryImpl)
{ {
_geometryImpl = geometryImpl; _geometryImpl = geometryImpl;
_path = _geometryImpl._path; _path = _geometryImpl._effectivePath;
} }
/// <inheritdoc />
/// <remarks>Will update bounds of passed geometry.</remarks>
public void Dispose() public void Dispose()
{ {
SKRect rc; _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect();
_path.GetBounds(out rc); _geometryImpl.InvalidateCaches();
_geometryImpl._bounds = rc.ToAvaloniaRect();
} }
/// <inheritdoc />
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{ {
_path.ArcTo( _path.ArcTo(
@ -95,33 +108,33 @@ namespace Avalonia.Skia
sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise,
(float)point.X, (float)point.X,
(float)point.Y); (float)point.Y);
_currentPoint = point;
} }
/// <inheritdoc />
public void BeginFigure(Point startPoint, bool isFilled) public void BeginFigure(Point startPoint, bool isFilled)
{ {
_path.MoveTo((float)startPoint.X, (float)startPoint.Y); _path.MoveTo((float)startPoint.X, (float)startPoint.Y);
_currentPoint = startPoint;
} }
/// <inheritdoc />
public void CubicBezierTo(Point point1, Point point2, Point point3) 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); _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
_currentPoint = point3;
} }
/// <inheritdoc />
public void QuadraticBezierTo(Point point1, Point point2) public void QuadraticBezierTo(Point point1, Point point2)
{ {
_path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
_currentPoint = point2;
} }
/// <inheritdoc />
public void LineTo(Point point) public void LineTo(Point point)
{ {
_path.LineTo((float)point.X, (float)point.Y); _path.LineTo((float)point.X, (float)point.Y);
_currentPoint = point;
} }
/// <inheritdoc />
public void EndFigure(bool isClosed) public void EndFigure(bool isClosed)
{ {
if (isClosed) if (isClosed)
@ -130,6 +143,7 @@ namespace Avalonia.Skia
} }
} }
/// <inheritdoc />
public void SetFillRule(FillRule fillRule) public void SetFillRule(FillRule fillRule)
{ {
_path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;

169
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
{
/// <summary>
/// Skia render target that writes to a surface.
/// </summary>
public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl
{
private readonly Vector _dpi;
private readonly SKSurface _surface;
private readonly SKCanvas _canvas;
private readonly bool _disableLcdRendering;
/// <summary>
/// Create new surface render target.
/// </summary>
/// <param name="createInfo">Create info.</param>
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");
}
}
/// <summary>
/// Create backing Skia surface.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="format">Format.</param>
/// <returns></returns>
private static SKSurface CreateSurface(int width, int height, PixelFormat? format)
{
var imageInfo = MakeImageInfo(width, height, format);
return SKSurface.Create(imageInfo);
}
/// <inheritdoc />
public void Dispose()
{
_canvas.Dispose();
_surface.Dispose();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public int PixelWidth { get; }
/// <inheritdoc />
public int PixelHeight { get; }
/// <inheritdoc />
public void Save(string fileName)
{
using (var image = SnapshotImage())
{
ImageSavingHelper.SaveImage(image, fileName);
}
}
/// <inheritdoc />
public void Save(Stream stream)
{
using (var image = SnapshotImage())
{
ImageSavingHelper.SaveImage(image, stream);
}
}
/// <inheritdoc />
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
using (var image = SnapshotImage())
{
context.Canvas.DrawImage(image, sourceRect, destRect, paint);
}
}
/// <summary>
/// Create Skia image snapshot from a surface.
/// </summary>
/// <returns>Image snapshot.</returns>
public SKImage SnapshotImage()
{
return _surface.Snapshot();
}
/// <summary>
/// Create image info for given parameters.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="format">Format.</param>
/// <returns></returns>
private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format)
{
var colorType = PixelFormatHelper.ResolveColorType(format);
return new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
}
/// <summary>
/// Create info of a surface render target.
/// </summary>
public struct CreateInfo
{
/// <summary>
/// Width of a render target.
/// </summary>
public int Width;
/// <summary>
/// Height of a render target.
/// </summary>
public int Height;
/// <summary>
/// Dpi used when rendering to a surface.
/// </summary>
public Vector Dpi;
/// <summary>
/// Pixel format of a render target.
/// </summary>
public PixelFormat? Format;
/// <summary>
/// Render text without Lcd rendering.
/// </summary>
public bool DisableTextLcdRendering;
}
}
}

62
src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs

@ -1,59 +1,43 @@
using System; // Copyright (c) The Avalonia Project. All rights reserved.
using Avalonia.Media; // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl /// <summary>
/// A Skia implementation of a <see cref="ITransformedGeometryImpl"/>.
/// </summary>
public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{ {
/// <summary>
/// Initializes a new instance of the <see cref="TransformedGeometryImpl"/> class.
/// </summary>
/// <param name="source">Source geometry.</param>
/// <param name="transform">Transform of new geometry.</param>
public TransformedGeometryImpl(GeometryImpl source, Matrix transform) public TransformedGeometryImpl(GeometryImpl source, Matrix transform)
{ {
SourceGeometry = source; SourceGeometry = source;
Transform = transform; 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();
} }
/// <inheritdoc />
public override SKPath EffectivePath { get; } public override SKPath EffectivePath { get; }
/// <inheritdoc />
public IGeometryImpl SourceGeometry { get; } public IGeometryImpl SourceGeometry { get; }
/// <inheritdoc />
public Matrix Transform { get; } public Matrix Transform { get; }
public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); /// <inheritdoc />
public override Rect Bounds { get; }
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);
}
} }
} }

9
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.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
@ -5,7 +9,10 @@ using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
static class TypefaceCache /// <summary>
/// Cache for Skia typefaces.
/// </summary>
internal static class TypefaceCache
{ {
public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name); public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name);
static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> Cache = new Dictionary<string, Dictionary<FontKey, SKTypeface>>(); static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> Cache = new Dictionary<string, Dictionary<FontKey, SKTypeface>>();

151
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
{
/// <summary>
/// Skia based writeable bitmap.
/// </summary>
public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl
{
private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc;
private readonly SKBitmap _bitmap;
/// <summary>
/// Create new writeable bitmap.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="format">Format.</param>
public WriteableBitmapImpl(int width, int height, PixelFormat? format = null)
{
PixelHeight = height;
PixelWidth = width;
var colorType = PixelFormatHelper.ResolveColorType(format);
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
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);
}
/// <inheritdoc />
public int PixelWidth { get; }
/// <inheritdoc />
public int PixelHeight { get; }
/// <inheritdoc />
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
}
/// <inheritdoc />
public void Dispose()
{
_bitmap.Dispose();
}
/// <inheritdoc />
public void Save(Stream stream)
{
using (var image = GetSnapshot())
{
ImageSavingHelper.SaveImage(image, stream);
}
}
/// <inheritdoc />
public void Save(string fileName)
{
using (var image = GetSnapshot())
{
ImageSavingHelper.SaveImage(image, fileName);
}
}
/// <inheritdoc />
public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap);
/// <summary>
/// Get snapshot as image.
/// </summary>
/// <returns>Image snapshot.</returns>
public SKImage GetSnapshot()
{
return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
}
/// <summary>
/// Release given unmanaged blob.
/// </summary>
/// <param name="address">Blob address.</param>
/// <param name="ctx">Blob.</param>
private static void ReleaseProc(IntPtr address, object ctx)
{
((IUnmanagedBlob)ctx).Dispose();
}
/// <summary>
/// Framebuffer for bitmap.
/// </summary>
private class BitmapFramebuffer : ILockedFramebuffer
{
private SKBitmap _bitmap;
/// <summary>
/// Create framebuffer from given bitmap.
/// </summary>
/// <param name="bitmap">Bitmap.</param>
public BitmapFramebuffer(SKBitmap bitmap)
{
_bitmap = bitmap;
}
/// <inheritdoc />
public void Dispose()
{
_bitmap = null;
}
/// <inheritdoc />
public IntPtr Address => _bitmap.GetPixels();
/// <inheritdoc />
public int Width => _bitmap.Width;
/// <inheritdoc />
public int Height => _bitmap.Height;
/// <inheritdoc />
public int RowBytes => _bitmap.RowBytes;
/// <inheritdoc />
public Vector Dpi { get; } = SkiaPlatform.DefaultDpi;
/// <inheritdoc />
public PixelFormat Format => _bitmap.ColorType.ToPixelFormat();
}
}
}

34
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 DrawingContextImpl
- Alpha support missing as SkiaSharp does not expose this - Alpha support missing as SkiaSharp does not expose this
- Gradient Shader caching? - Gradient Shader caching?
- TileBrushes
- Pen Dash styles - Pen Dash styles
Formatted Text Rendering Formatted Text Rendering
- minor polish - Minor polish
RenderTarget Linux
- Figure out a cleaner implementation across all platforms - Need gpu platform implementation
- HW acceleration
App Bootstrapping macOS
- Cleanup the testapplications across all platforms - Need gpu platform implementation
- Add a cleaner Fluent API for the subsystems
- ie. app.UseDirect2D() (via platform specific extension methods)
Android Android
- Not tested at all yet - Not tested at all yet
iOS iOS
- Get GLView working again. See HW above - Not tested at all yet
Win32
- Cleanup the unmanaged methods (BITMAPINFO) if possible
General General
- Cleanup/eliminate obsolete files - Get Skia Unit Tests passing (most of the issues are related to antialiasing)
- Finish cleanup of the many Test Applications
- Get Skia Unit Tests passing

28
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -321,6 +321,24 @@ namespace Avalonia.Win32.Interop
WS_EX_NOACTIVATE = 0x08000000 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 public enum WindowsMessage : uint
{ {
WM_NULL = 0x0000, WM_NULL = 0x0000,
@ -1194,6 +1212,16 @@ namespace Avalonia.Win32.Interop
public IntPtr hIconSm; 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 public enum HRESULT : uint
{ {
S_FALSE = 0x0001, S_FALSE = 0x0001,

2
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -724,7 +724,7 @@ namespace Avalonia.Win32
UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
{ {
cbSize = Marshal.SizeOf<UnmanagedMethods.WNDCLASSEX>(), cbSize = Marshal.SizeOf<UnmanagedMethods.WNDCLASSEX>(),
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, lpfnWndProc = _wndProcDelegate,
hInstance = UnmanagedMethods.GetModuleHandle(null), hInstance = UnmanagedMethods.GetModuleHandle(null),
hCursor = DefaultCursor, hCursor = DefaultCursor,

2
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@ -67,7 +67,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[Theory] [Theory]
[InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888),
#if SKIA #if AVALONIA_SKIA
InlineData(PixelFormat.Rgb565) InlineData(PixelFormat.Rgb565)
#endif #endif
] ]

12
tests/Avalonia.RenderTests/Media/ImageBrushTests.cs

@ -281,12 +281,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
} }
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact] [Fact]
#endif
public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest() public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest()
{ {
Decorator target = new Decorator Decorator target = new Decorator
@ -309,12 +305,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
} }
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact] [Fact]
#endif
public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest()
{ {
Decorator target = new Decorator Decorator target = new Decorator

12
tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs

@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush") public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush")
{ {
} }
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact] [Fact]
#endif
public async Task LinearGradientBrush_RedBlue_Horizontal_Fill() public async Task LinearGradientBrush_RedBlue_Horizontal_Fill()
{ {
Decorator target = new Decorator Decorator target = new Decorator
@ -52,12 +48,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
} }
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact] [Fact]
#endif
public async Task LinearGradientBrush_RedBlue_Vertical_Fill() public async Task LinearGradientBrush_RedBlue_Vertical_Fill()
{ {
Decorator target = new Decorator Decorator target = new Decorator

6
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush")
{ {
} }
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact] [Fact]
#endif
public async Task RadialGradientBrush_RedBlue() public async Task RadialGradientBrush_RedBlue()
{ {
Decorator target = new Decorator Decorator target = new Decorator

6
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -270,12 +270,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
} }
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact] [Fact]
#endif
public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest()
{ {
Decorator target = new Decorator Decorator target = new Decorator

22
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
</Project>

79
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);
}
}
}
}

8
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)]

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 642 B

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Loading…
Cancel
Save