diff --git a/Avalonia.sln b/Avalonia.sln
index 39dc11f41f..39396f3ab8 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props
build\Moq.props = build\Moq.props
build\NetCore.props = build\NetCore.props
+ build\NetFX.props = build\NetFX.props
build\ReactiveUI.props = build\ReactiveUI.props
build\Rx.props = build\Rx.props
build\SampleApp.props = build\SampleApp.props
@@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -2468,6 +2471,46 @@ Global
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2520,6 +2563,7 @@ Global
{F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2}
{4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/build.cake b/build.cake
index bf3ae41b58..561a33186a 100644
--- a/build.cake
+++ b/build.cake
@@ -207,6 +207,7 @@ Task("Run-Unit-Tests")
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false);
+ RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false);
if (data.Parameters.IsRunningOnWindows)
{
RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true);
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 04e8a3ad4f..35c979a95e 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index b45a93455e..1f53dedc14 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,9 +1,9 @@
using System;
using System.Diagnostics;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Threading;
using Avalonia;
+using Avalonia.Skia;
namespace ControlCatalog.NetCore
{
@@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore
/// This method is needed for IDE previewer infrastructure
///
public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure().UsePlatformDetect().UseReactiveUI();
+ => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI();
static void ConsoleSilencer()
{
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index fd6b149837..a14923b410 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -117,6 +117,9 @@ namespace Avalonia.Rendering
var scene = Interlocked.Exchange(ref _scene, null);
scene?.Dispose();
Stop();
+
+ Layers.Clear();
+ RenderTarget?.Dispose();
}
///
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index e830d5c313..2118b66de2 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -133,6 +133,7 @@ namespace Avalonia.Rendering
///
public void Dispose()
{
+ _renderTarget?.Dispose();
}
///
diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs
index bafd644603..6a45ecd912 100644
--- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs
+++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs
@@ -11,11 +11,7 @@ namespace Avalonia.Rendering
{
private List _inner = new List();
private Dictionary _index = new Dictionary();
-
- public RenderLayers()
- {
- }
-
+
public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
@@ -51,6 +47,16 @@ namespace Avalonia.Rendering
}
}
+ public void Clear()
+ {
+ foreach (var layer in _index.Values)
+ {
+ layer.Bitmap.Dispose();
+ }
+
+ _index.Clear();
+ }
+
public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
{
return _index.TryGetValue(layerRoot, out value);
diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs
index acde49a84a..c2db17cd86 100644
--- a/src/Avalonia.Visuals/Vector.cs
+++ b/src/Avalonia.Visuals/Vector.cs
@@ -3,7 +3,7 @@
using System;
using System.Globalization;
-using System.Xml.Linq;
+using JetBrains.Annotations;
namespace Avalonia
{
@@ -122,6 +122,56 @@ namespace Avalonia
return new Vector(a._x - b._x, a._y - b._y);
}
+ ///
+ /// Check if two vectors are equal (bitwise).
+ ///
+ ///
+ ///
+ public bool Equals(Vector other)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return _x == other._x && _y == other._y;
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ ///
+ /// Check if two vectors are nearly equal (numerically).
+ ///
+ /// The other vector.
+ /// True if vectors are nearly equal.
+ [Pure]
+ public bool NearlyEquals(Vector other)
+ {
+ const float tolerance = float.Epsilon;
+
+ return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+
+ return obj is Vector vector && Equals(vector);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (_x.GetHashCode() * 397) ^ _y.GetHashCode();
+ }
+ }
+
+ public static bool operator ==(Vector left, Vector right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(Vector left, Vector right)
+ {
+ return !left.Equals(right);
+ }
+
///
/// Returns the string representation of the point.
///
diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs
deleted file mode 100644
index ccc5a37105..0000000000
--- a/src/Skia/Avalonia.Skia/BitmapImpl.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using System;
-using System.IO;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
- class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl
- {
- private Vector _dpi;
-
- public SKBitmap Bitmap { get; private set; }
-
- public BitmapImpl(SKBitmap bm)
- {
- Bitmap = bm;
- PixelHeight = bm.Height;
- PixelWidth = bm.Width;
- _dpi = new Vector(96, 96);
- }
-
- static void ReleaseProc(IntPtr address, object ctx)
- {
- ((IUnmanagedBlob) ctx).Dispose();
- }
-
- private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc;
-
- public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null)
- {
- PixelHeight = height;
- PixelWidth = width;
- _dpi = dpi;
- var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
- var runtimePlatform = AvaloniaLocator.Current?.GetService();
- var runtime = runtimePlatform?.GetRuntimeInfo();
- if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux)
- colorType = SKColorType.Bgra8888;
-
- if (runtimePlatform != null)
- {
- Bitmap = new SKBitmap();
- var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
- var plat = AvaloniaLocator.Current.GetService();
- var blob = plat.AllocBlob(nfo.BytesSize);
- Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob);
-
- }
- else
- Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul);
- Bitmap.Erase(SKColor.Empty);
- }
-
- public void Dispose()
- {
- Bitmap.Dispose();
- }
-
- public int PixelWidth { get; private set; }
- public int PixelHeight { get; private set; }
-
- class BitmapDrawingContext : DrawingContextImpl
- {
- private readonly SKSurface _surface;
-
- public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
- : this(CreateSurface(bitmap), dpi, visualBrushRenderer)
- {
- CanUseLcdRendering = false;
- }
-
- private static SKSurface CreateSurface(SKBitmap bitmap)
- {
- IntPtr length;
- var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes);
- if (rv == null)
- throw new Exception("Unable to create Skia surface");
- return rv;
- }
-
- public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
- : base(surface.Canvas, dpi, visualBrushRenderer)
- {
- _surface = surface;
- }
-
- public override void Dispose()
- {
- base.Dispose();
- _surface.Dispose();
- }
- }
-
- public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
- {
- return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer);
- }
-
- public void Save(Stream stream)
- {
- IntPtr length;
- using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes))
- using (var data = image.Encode())
- {
- data.SaveTo(stream);
- }
- }
-
- public void Save(string fileName)
- {
- using (var stream = File.Create(fileName))
- Save(stream);
- }
-
- class BitmapFramebuffer : ILockedFramebuffer
- {
- private SKBitmap _bmp;
-
- public BitmapFramebuffer(SKBitmap bmp)
- {
- _bmp = bmp;
- _bmp.LockPixels();
- }
-
- public void Dispose()
- {
- _bmp.UnlockPixels();
- _bmp = null;
- }
-
- public IntPtr Address => _bmp.GetPixels();
- public int Width => _bmp.Width;
- public int Height => _bmp.Height;
- public int RowBytes => _bmp.RowBytes;
- public Vector Dpi { get; } = new Vector(96, 96);
- public PixelFormat Format => _bmp.ColorType.ToPixelFormat();
- }
-
- public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap);
- }
-}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 22e5652cfb..b7ce6eedc4 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -1,57 +1,114 @@
-using Avalonia.Media;
-using SkiaSharp;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Utilities;
using Avalonia.Utilities;
+using SkiaSharp;
namespace Avalonia.Skia
{
- internal class DrawingContextImpl : IDrawingContextImpl
+ ///
+ /// Skia based drawing context.
+ ///
+ public class DrawingContextImpl : IDrawingContextImpl
{
+ private readonly IDisposable[] _disposables;
private readonly Vector _dpi;
+ private readonly Stack _maskStack = new Stack();
+ private readonly Stack _opacityStack = new Stack();
private readonly Matrix? _postTransform;
- private readonly IDisposable[] _disposables;
private readonly IVisualBrushRenderer _visualBrushRenderer;
- private Stack maskStack = new Stack();
- protected bool CanUseLcdRendering = true;
- public SKCanvas Canvas { get; private set; }
-
- public DrawingContextImpl(
- SKCanvas canvas,
- Vector dpi,
- IVisualBrushRenderer visualBrushRenderer,
- params IDisposable[] disposables)
+ private double _currentOpacity = 1.0f;
+ private readonly bool _canTextUseLcdRendering;
+ private Matrix _currentTransform;
+
+ ///
+ /// Context create info.
+ ///
+ public struct CreateInfo
+ {
+ ///
+ /// Canvas to draw to.
+ ///
+ public SKCanvas Canvas;
+
+ ///
+ /// Dpi of drawings.
+ ///
+ public Vector Dpi;
+
+ ///
+ /// Visual brush renderer.
+ ///
+ public IVisualBrushRenderer VisualBrushRenderer;
+
+ ///
+ /// Render text without Lcd rendering.
+ ///
+ public bool DisableTextLcdRendering;
+ }
+
+ ///
+ /// Create new drawing context.
+ ///
+ /// Create info.
+ /// Array of elements to dispose after drawing has finished.
+ public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables)
{
- _dpi = dpi;
- if (dpi.X != 96 || dpi.Y != 96)
- _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
- _visualBrushRenderer = visualBrushRenderer;
+ _dpi = createInfo.Dpi;
+ _visualBrushRenderer = createInfo.VisualBrushRenderer;
_disposables = disposables;
- Canvas = canvas;
+ _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering;
+
+ Canvas = createInfo.Canvas;
+
+ if (Canvas == null)
+ {
+ throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
+ }
+
+ if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
+ {
+ _postTransform =
+ Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+ }
+
Transform = Matrix.Identity;
}
+
+ ///
+ /// Skia canvas.
+ ///
+ public SKCanvas Canvas { get; }
+ ///
public void Clear(Color color)
{
Canvas.Clear(color.ToSKColor());
}
+ ///
public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect)
{
- var impl = (BitmapImpl)source.Item;
+ var drawableImage = (IDrawableBitmapImpl) source.Item;
var s = sourceRect.ToSKRect();
var d = destRect.ToSKRect();
- using (var paint = new SKPaint()
- { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) })
+
+ using (var paint =
+ new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))})
{
- Canvas.DrawBitmap(impl.Bitmap, s, d, paint);
+ drawableImage.Draw(this, s, d, paint);
}
}
+ ///
public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
PushOpacityMask(opacityMask, opacityMaskRect);
@@ -59,17 +116,19 @@ namespace Avalonia.Skia
PopOpacityMask();
}
+ ///
public void DrawLine(Pen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
- Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint);
+ Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
}
}
+ ///
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{
- var impl = (GeometryImpl)geometry;
+ var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
@@ -79,6 +138,7 @@ namespace Avalonia.Skia
{
Canvas.DrawPath(impl.EffectivePath, fill.Paint);
}
+
if (stroke.Paint != null)
{
Canvas.DrawPath(impl.EffectivePath, stroke.Paint);
@@ -86,227 +146,424 @@ namespace Avalonia.Skia
}
}
- private struct PaintState : IDisposable
+ ///
+ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
{
- private readonly SKColor _color;
- private readonly SKShader _shader;
- private readonly SKPaint _paint;
+ using (var paint = CreatePaint(pen, rect.Size))
+ {
+ var rc = rect.ToSKRect();
- public PaintState(SKPaint paint, SKColor color, SKShader shader)
+ if (Math.Abs(cornerRadius) < float.Epsilon)
+ {
+ Canvas.DrawRect(rc, paint.Paint);
+ }
+ else
+ {
+ Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
+ }
+ }
+ }
+
+ ///
+ public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+ {
+ using (var paint = CreatePaint(brush, rect.Size))
{
- _paint = paint;
- _color = color;
- _shader = shader;
+ var rc = rect.ToSKRect();
+
+ if (Math.Abs(cornerRadius) < float.Epsilon)
+ {
+ Canvas.DrawRect(rc, paint.Paint);
+ }
+ else
+ {
+ Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
+ }
}
+ }
- public void Dispose()
+ ///
+ public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+ {
+ using (var paint = CreatePaint(foreground, text.Size))
{
- _paint.Color = _color;
- _paint.Shader = _shader;
+ var textImpl = (FormattedTextImpl) text;
+ textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
}
}
- internal struct PaintWrapper : IDisposable
+ ///
+ public IRenderTargetBitmapImpl CreateLayer(Size size)
{
- //We are saving memory allocations there
- //TODO: add more disposable fields if needed
- public readonly SKPaint Paint;
+ var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+ var pixelSize = size * normalizedDpi;
- private IDisposable _disposable1;
- private IDisposable _disposable2;
+ return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi);
+ }
- public IDisposable ApplyTo(SKPaint paint)
- {
- var state = new PaintState(paint, paint.Color, paint.Shader);
+ ///
+ public void PushClip(Rect clip)
+ {
+ Canvas.Save();
+ Canvas.ClipRect(clip.ToSKRect());
+ }
- paint.Color = Paint.Color;
- paint.Shader = Paint.Shader;
+ ///
+ public void PopClip()
+ {
+ Canvas.Restore();
+ }
- return state;
- }
+ ///
+ public void PushOpacity(double opacity)
+ {
+ _opacityStack.Push(_currentOpacity);
+ _currentOpacity *= opacity;
+ }
- public void AddDisposable(IDisposable disposable)
- {
- if (_disposable1 == null)
- _disposable1 = disposable;
- else if (_disposable2 == null)
- _disposable2 = disposable;
- else
- throw new InvalidOperationException();
- }
+ ///
+ public void PopOpacity()
+ {
+ _currentOpacity = _opacityStack.Pop();
+ }
- public PaintWrapper(SKPaint paint)
+ ///
+ public virtual void Dispose()
+ {
+ if (_disposables == null)
{
- Paint = paint;
- _disposable1 = null;
- _disposable2 = null;
+ return;
}
- public void Dispose()
+ foreach (var disposable in _disposables)
{
- Paint?.Dispose();
- _disposable1?.Dispose();
- _disposable2?.Dispose();
+ disposable?.Dispose();
}
}
- internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+ ///
+ public void PushGeometryClip(IGeometryImpl clip)
{
- SKPaint paint = new SKPaint();
- var rv = new PaintWrapper(paint);
- paint.IsStroke = false;
+ Canvas.Save();
+ Canvas.ClipPath(((GeometryImpl)clip).EffectivePath);
+ }
-
- double opacity = brush.Opacity * _currentOpacity;
- paint.IsAntialias = true;
+ ///
+ public void PopGeometryClip()
+ {
+ Canvas.Restore();
+ }
+
+ ///
+ public void PushOpacityMask(IBrush mask, Rect bounds)
+ {
+ // TODO: This should be disposed
+ var paint = new SKPaint();
+
+ Canvas.SaveLayer(paint);
+ _maskStack.Push(CreatePaint(mask, bounds.Size));
+ }
- var solid = brush as ISolidColorBrush;
- if (solid != null)
+ ///
+ public void PopOpacityMask()
+ {
+ using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
{
- paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
- return rv;
+ Canvas.SaveLayer(paint);
+ using (var paintWrapper = _maskStack.Pop())
+ {
+ Canvas.DrawPaint(paintWrapper.Paint);
+ }
+ Canvas.Restore();
}
- paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity)));
- var gradient = brush as IGradientBrush;
- if (gradient != null)
+ Canvas.Restore();
+ }
+
+ ///
+ public Matrix Transform
+ {
+ get { return _currentTransform; }
+ set
{
- var tileMode = gradient.SpreadMethod.ToSKShaderTileMode();
- var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
- var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray();
+ if (_currentTransform == value)
+ return;
+
+ _currentTransform = value;
+
+ var transform = value;
- var linearGradient = brush as ILinearGradientBrush;
- if (linearGradient != null)
+ if (_postTransform.HasValue)
+ {
+ transform *= _postTransform.Value;
+ }
+
+ Canvas.SetMatrix(transform.ToSKMatrix());
+ }
+ }
+
+ ///
+ /// Configure paint wrapper for using gradient brush.
+ ///
+ /// Paint wrapper.
+ /// Target size.
+ /// Gradient brush.
+ private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
+ {
+ var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
+ var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
+ var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
+
+ switch (gradientBrush)
+ {
+ case ILinearGradientBrush linearGradient:
{
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
// would be nice to cache these shaders possibly?
- using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
- paint.Shader = shader;
+ using (var shader =
+ SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
+ {
+ paintWrapper.Paint.Shader = shader;
+ }
+ break;
}
- else
+ case IRadialGradientBrush radialGradient:
{
- var radialGradient = brush as IRadialGradientBrush;
- if (radialGradient != null)
- {
- var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
- var radius = (float)radialGradient.Radius;
+ var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
+ var radius = (float)(radialGradient.Radius * targetSize.Width);
- // TODO: There is no SetAlpha in SkiaSharp
- //paint.setAlpha(128);
-
- // would be nice to cache these shaders possibly?
- using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
- paint.Shader = shader;
+ // TODO: There is no SetAlpha in SkiaSharp
+ //paint.setAlpha(128);
+ // would be nice to cache these shaders possibly?
+ using (var shader =
+ SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
+ {
+ paintWrapper.Paint.Shader = shader;
}
+
+ break;
}
+ }
+ }
+
+ ///
+ /// Configure paint wrapper for using tile brush.
+ ///
+ /// Paint wrapper.
+ /// Target size.
+ /// Tile brush to use.
+ /// Tile brush image.
+ private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
+ {
+ var calc = new TileBrushCalculator(tileBrush,
+ new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
+
+ var intermediate = CreateRenderTarget(
+ (int)calc.IntermediateSize.Width,
+ (int)calc.IntermediateSize.Height, _dpi);
+
+ paintWrapper.AddDisposable(intermediate);
+
+ using (var context = intermediate.CreateDrawingContext(null))
+ {
+ var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
- return rv;
+ context.Clear(Colors.Transparent);
+ context.PushClip(calc.IntermediateClip);
+ context.Transform = calc.IntermediateTransform;
+ context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
+ context.PopClip();
}
- var tileBrush = brush as ITileBrush;
- var visualBrush = brush as IVisualBrush;
- var tileBrushImage = default(BitmapImpl);
+ var tileTransform =
+ tileBrush.TileMode != TileMode.None
+ ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
+ : SKMatrix.MakeIdentity();
- if (visualBrush != null)
+ SKShaderTileMode tileX =
+ tileBrush.TileMode == TileMode.None
+ ? SKShaderTileMode.Clamp
+ : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
+ ? SKShaderTileMode.Mirror
+ : SKShaderTileMode.Repeat;
+
+ SKShaderTileMode tileY =
+ tileBrush.TileMode == TileMode.None
+ ? SKShaderTileMode.Clamp
+ : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
+ ? SKShaderTileMode.Mirror
+ : SKShaderTileMode.Repeat;
+
+
+ var image = intermediate.SnapshotImage();
+ paintWrapper.AddDisposable(image);
+
+ using (var shader = image.ToShader(tileX, tileY, tileTransform))
{
- if (_visualBrushRenderer != null)
- {
- var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+ paintWrapper.Paint.Shader = shader;
+ }
+ }
- if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
- {
- var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+ ///
+ /// Configure paint wrapper to use visual brush.
+ ///
+ /// Paint wrapper.
+ /// Visual brush.
+ /// Visual brush renderer.
+ /// Tile brush image.
+ private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage)
+ {
+ if (_visualBrushRenderer == null)
+ {
+ throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+ }
- using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
- {
- ctx.Clear(Colors.Transparent);
- _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
- }
+ var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
- tileBrushImage = intermediate;
- rv.AddDisposable(tileBrushImage);
- }
- }
- else
+ if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
+ {
+ var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+
+ using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer))
{
- throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+ ctx.Clear(Colors.Transparent);
+
+ visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
}
+
+ tileBrushImage = intermediate;
+ paintWrapper.AddDisposable(tileBrushImage);
}
- else
+ }
+
+ ///
+ /// Creates paint wrapper for given brush.
+ ///
+ /// Source brush.
+ /// Target size.
+ /// Paint wrapper for given brush.
+ internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+ {
+ var paint = new SKPaint
+ {
+ IsStroke = false,
+ IsAntialias = true
+ };
+
+ var paintWrapper = new PaintWrapper(paint);
+
+ double opacity = brush.Opacity * _currentOpacity;
+
+ if (brush is ISolidColorBrush solid)
{
- tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item);
+ paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
+
+ return paintWrapper;
}
- if (tileBrush != null && tileBrushImage != null)
+ paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity));
+
+ if (brush is IGradientBrush gradient)
{
- var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
- var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi);
- rv.AddDisposable(bitmap);
- using (var context = bitmap.CreateDrawingContext(null))
- {
- var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
+ ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
- context.Clear(Colors.Transparent);
- context.PushClip(calc.IntermediateClip);
- context.Transform = calc.IntermediateTransform;
- context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
- context.PopClip();
- }
+ return paintWrapper;
+ }
+
+ var tileBrush = brush as ITileBrush;
+ var visualBrush = brush as IVisualBrush;
+ var tileBrushImage = default(IDrawableBitmapImpl);
- SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y);
- SKShaderTileMode tileX =
- tileBrush.TileMode == TileMode.None
- ? SKShaderTileMode.Clamp
- : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
- ? SKShaderTileMode.Mirror
- : SKShaderTileMode.Repeat;
-
- SKShaderTileMode tileY =
- tileBrush.TileMode == TileMode.None
- ? SKShaderTileMode.Clamp
- : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
- ? SKShaderTileMode.Mirror
- : SKShaderTileMode.Repeat;
- using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation))
- paint.Shader = shader;
+ if (visualBrush != null)
+ {
+ ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage);
+ }
+ else
+ {
+ tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
}
- return rv;
+ if (tileBrush != null && tileBrushImage != null)
+ {
+ ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
+ }
+
+ return paintWrapper;
}
+ ///
+ /// Creates paint wrapper for given pen.
+ ///
+ /// Source pen.
+ /// Target size.
+ ///
private PaintWrapper CreatePaint(Pen pen, Size targetSize)
{
var rv = CreatePaint(pen.Brush, targetSize);
var paint = rv.Paint;
paint.IsStroke = true;
- paint.StrokeWidth = (float)pen.Thickness;
+ paint.StrokeWidth = (float) pen.Thickness;
- if (pen.StartLineCap == PenLineCap.Round)
- paint.StrokeCap = SKStrokeCap.Round;
- else if (pen.StartLineCap == PenLineCap.Square)
- paint.StrokeCap = SKStrokeCap.Square;
- else
- paint.StrokeCap = SKStrokeCap.Butt;
+ // Need to modify dashes due to Skia modifying their lengths
+ // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
+ // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
+ float dashLengthModifier;
+ float gapLengthModifier;
- if (pen.LineJoin == PenLineJoin.Miter)
- paint.StrokeJoin = SKStrokeJoin.Miter;
- else if (pen.LineJoin == PenLineJoin.Round)
- paint.StrokeJoin = SKStrokeJoin.Round;
- else
- paint.StrokeJoin = SKStrokeJoin.Bevel;
+ switch (pen.StartLineCap)
+ {
+ case PenLineCap.Round:
+ paint.StrokeCap = SKStrokeCap.Round;
+ dashLengthModifier = -paint.StrokeWidth;
+ gapLengthModifier = paint.StrokeWidth;
+ break;
+ case PenLineCap.Square:
+ paint.StrokeCap = SKStrokeCap.Square;
+ dashLengthModifier = -paint.StrokeWidth;
+ gapLengthModifier = paint.StrokeWidth;
+ break;
+ default:
+ paint.StrokeCap = SKStrokeCap.Butt;
+ dashLengthModifier = 0.0f;
+ gapLengthModifier = 0.0f;
+ break;
+ }
+
+ switch (pen.LineJoin)
+ {
+ case PenLineJoin.Miter:
+ paint.StrokeJoin = SKStrokeJoin.Miter;
+ break;
+ case PenLineJoin.Round:
+ paint.StrokeJoin = SKStrokeJoin.Round;
+ break;
+ default:
+ paint.StrokeJoin = SKStrokeJoin.Bevel;
+ break;
+ }
- paint.StrokeMiter = (float)pen.MiterLimit;
+ paint.StrokeMiter = (float) pen.MiterLimit;
if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
{
- var pe = SKPathEffect.CreateDash(
- pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(),
- (float)pen.DashStyle.Offset);
+ var srcDashes = pen.DashStyle.Dashes;
+ var dashesArray = new float[srcDashes.Count];
+
+ for (var i = 0; i < srcDashes.Count; ++i)
+ {
+ var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier;
+
+ // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale
+ dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier;
+ }
+
+ var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset);
+
paint.PathEffect = pe;
rv.AddDisposable(pe);
}
@@ -314,128 +571,118 @@ namespace Avalonia.Skia
return rv;
}
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+ ///
+ /// Create new render target compatible with this drawing context.
+ ///
+ /// Width.
+ /// Height.
+ /// Drawing dpi.
+ /// Pixel format.
+ ///
+ private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null)
{
- using (var paint = CreatePaint(pen, rect.Size))
+ var createInfo = new SurfaceRenderTarget.CreateInfo
{
- var rc = rect.ToSKRect();
- if (cornerRadius == 0)
- {
- Canvas.DrawRect(rc, paint.Paint);
- }
- else
- {
- Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
- }
- }
+ Width = width,
+ Height = height,
+ Dpi = dpi,
+ Format = format,
+ DisableTextLcdRendering = !_canTextUseLcdRendering
+ };
+
+ return new SurfaceRenderTarget(createInfo);
}
- public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+ ///
+ /// Skia cached paint state.
+ ///
+ private struct PaintState : IDisposable
{
- using (var paint = CreatePaint(brush, rect.Size))
+ private readonly SKColor _color;
+ private readonly SKShader _shader;
+ private readonly SKPaint _paint;
+
+ public PaintState(SKPaint paint, SKColor color, SKShader shader)
{
- var rc = rect.ToSKRect();
- if (cornerRadius == 0)
- {
- Canvas.DrawRect(rc, paint.Paint);
- }
- else
- {
- Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
- }
+ _paint = paint;
+ _color = color;
+ _shader = shader;
}
- }
- public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
- {
- using (var paint = CreatePaint(foreground, text.Size))
+ ///
+ public void Dispose()
{
- var textImpl = (FormattedTextImpl)text;
- textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering);
+ _paint.Color = _color;
+ _paint.Shader = _shader;
}
}
- public IRenderTargetBitmapImpl CreateLayer(Size size)
- {
- var pixelSize = size * (_dpi / 96);
- return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi);
- }
-
- public void PushClip(Rect clip)
- {
- Canvas.Save();
- Canvas.ClipRect(clip.ToSKRect());
- }
-
- public void PopClip()
- {
- Canvas.Restore();
- }
-
- private double _currentOpacity = 1.0f;
- private readonly Stack _opacityStack = new Stack();
-
- public void PushOpacity(double opacity)
+ ///
+ /// Skia paint wrapper.
+ ///
+ internal struct PaintWrapper : IDisposable
{
- _opacityStack.Push(_currentOpacity);
- _currentOpacity *= opacity;
- }
+ //We are saving memory allocations there
+ public readonly SKPaint Paint;
- public void PopOpacity()
- {
- _currentOpacity = _opacityStack.Pop();
- }
+ private IDisposable _disposable1;
+ private IDisposable _disposable2;
+ private IDisposable _disposable3;
- public virtual void Dispose()
- {
- if(_disposables!=null)
- foreach (var disposable in _disposables)
- disposable?.Dispose();
- }
+ public PaintWrapper(SKPaint paint)
+ {
+ Paint = paint;
- public void PushGeometryClip(IGeometryImpl clip)
- {
- Canvas.Save();
- Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath);
- }
+ _disposable1 = null;
+ _disposable2 = null;
+ _disposable3 = null;
+ }
- public void PopGeometryClip()
- {
- Canvas.Restore();
- }
+ public IDisposable ApplyTo(SKPaint paint)
+ {
+ var state = new PaintState(paint, paint.Color, paint.Shader);
- public void PushOpacityMask(IBrush mask, Rect bounds)
- {
- Canvas.SaveLayer(new SKPaint());
- maskStack.Push(CreatePaint(mask, bounds.Size));
- }
+ paint.Color = Paint.Color;
+ paint.Shader = Paint.Shader;
- public void PopOpacityMask()
- {
- Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn });
- using (var paintWrapper = maskStack.Pop())
- {
- Canvas.DrawPaint(paintWrapper.Paint);
+ return state;
}
- Canvas.Restore();
- Canvas.Restore();
- }
- private Matrix _currentTransform;
-
- public Matrix Transform
- {
- get { return _currentTransform; }
- set
+ ///
+ /// Add new disposable to a wrapper.
+ ///
+ /// Disposable to add.
+ public void AddDisposable(IDisposable disposable)
{
- if (_currentTransform == value)
- return;
+ if (_disposable1 == null)
+ {
+ _disposable1 = disposable;
+ }
+ else if (_disposable2 == null)
+ {
+ _disposable2 = disposable;
+ }
+ else if (_disposable3 == null)
+ {
+ _disposable3 = disposable;
+ }
+ else
+ {
+ Debug.Assert(false);
- _currentTransform = value;
- var transform = value;
- if (_postTransform.HasValue)
- transform *= _postTransform.Value;
- Canvas.SetMatrix(transform.ToSKMatrix());
+ // ReSharper disable once HeuristicUnreachableCode
+ throw new InvalidOperationException(
+ "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables.");
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Paint?.Dispose();
+ _disposable1?.Dispose();
+ _disposable2?.Dispose();
+ _disposable3?.Dispose();
}
}
}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 9c18d52cea..d835c83aa6 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -10,6 +10,9 @@ using System.Linq;
namespace Avalonia.Skia
{
+ ///
+ /// Skia formatted text implementation.
+ ///
public class FormattedTextImpl : IFormattedTextImpl
{
public FormattedTextImpl(
@@ -21,7 +24,7 @@ namespace Avalonia.Skia
IReadOnlyList spans)
{
Text = text ?? string.Empty;
-
+
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
@@ -352,7 +355,7 @@ namespace Avalonia.Skia
{
float measuredWidth;
string subText = textInput.Substring(textIndex, stop - textIndex);
- lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2;
+ lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth);
}
//Check for white space or line breakers before the lengthBreak
@@ -451,7 +454,6 @@ namespace Avalonia.Skia
private void BuildRects()
{
// Build character rects
- var fm = _paint.FontMetrics;
SKTextAlign align = _paint.TextAlign;
for (int li = 0; li < _skiaLines.Count; li++)
@@ -559,18 +561,16 @@ namespace Avalonia.Skia
string subString;
- float widthConstraint = (_constraint.Width != double.PositiveInfinity)
- ? (float)_constraint.Width
- : -1;
-
- for (int c = 0; curOff < length; c++)
+ float widthConstraint = double.IsPositiveInfinity(_constraint.Width)
+ ? -1
+ : (float)_constraint.Width;
+
+ while(curOff < length)
{
float lineWidth = -1;
int measured;
int trailingnumber = 0;
-
- subString = Text.Substring(curOff);
-
+
float constraint = -1;
if (_wrapping == TextWrapping.Wrap)
diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
index 1956f02d1b..99dbbefd4d 100644
--- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
+++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
@@ -1,82 +1,199 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Disposables;
using Avalonia.Controls.Platform.Surfaces;
-using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
+using Avalonia.Skia.Helpers;
using SkiaSharp;
namespace Avalonia.Skia
{
+ ///
+ /// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
+ ///
public class FramebufferRenderTarget : IRenderTarget
{
- private readonly IFramebufferPlatformSurface _surface;
+ private readonly IFramebufferPlatformSurface _platformSurface;
+ private SKImageInfo _currentImageInfo;
+ private IntPtr _currentFramebufferAddress;
+ private SKSurface _framebufferSurface;
+ private PixelFormatConversionShim _conversionShim;
+ private IDisposable _preFramebufferCopyHandler;
- public FramebufferRenderTarget(IFramebufferPlatformSurface surface)
+ ///
+ /// Create new framebuffer render target using a target surface.
+ ///
+ /// Target surface.
+ public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface)
{
- _surface = surface;
+ _platformSurface = platformSurface;
}
+ ///
public void Dispose()
{
- //Nothing to do here, since we don't own framebuffer
+ FreeSurface();
+ }
+
+ ///
+ public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+ {
+ var framebuffer = _platformSurface.Lock();
+ var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height,
+ framebuffer.Format.ToSkColorType(), SKAlphaType.Premul);
+
+ CreateSurface(framebufferImageInfo, framebuffer);
+
+ var canvas = _framebufferSurface.Canvas;
+
+ canvas.RestoreToCount(-1);
+ canvas.Save();
+ canvas.ResetMatrix();
+
+ var createInfo = new DrawingContextImpl.CreateInfo
+ {
+ Canvas = canvas,
+ Dpi = framebuffer.Dpi,
+ VisualBrushRenderer = visualBrushRenderer,
+ DisableTextLcdRendering = true
+ };
+
+ return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer);
+ }
+
+ ///
+ /// Check if two images info are compatible.
+ ///
+ /// Current.
+ /// Desired.
+ /// True, if images are compatible.
+ private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo)
+ {
+ return currentImageInfo.Width == desiredImageInfo.Width &&
+ currentImageInfo.Height == desiredImageInfo.Height &&
+ currentImageInfo.ColorType == desiredImageInfo.ColorType;
+ }
+
+ ///
+ /// Create Skia surface backed by given framebuffer.
+ ///
+ /// Desired image info.
+ /// Backing framebuffer.
+ private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer)
+ {
+ if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address)
+ {
+ return;
+ }
+
+ FreeSurface();
+
+ _currentFramebufferAddress = framebuffer.Address;
+
+ var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes);
+
+ // If surface cannot be created - try to create a compatibilty shim first
+ if (surface == null)
+ {
+ _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address);
+ _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler;
+
+ surface = _conversionShim.Surface;
+ }
+
+ _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " +
+ framebuffer.Format +
+ " or pixel format translator");
+ _currentImageInfo = desiredImageInfo;
+ }
+
+ ///
+ /// Free Skia surface.
+ ///
+ private void FreeSurface()
+ {
+ _conversionShim?.Dispose();
+ _conversionShim = null;
+ _preFramebufferCopyHandler = null;
+
+ if (_conversionShim != null)
+ {
+ _framebufferSurface?.Dispose();
+ }
+
+ _framebufferSurface = null;
+ _currentFramebufferAddress = IntPtr.Zero;
}
- class PixelFormatShim : IDisposable
+ ///
+ /// Converts non-compatible pixel formats using bitmap copies.
+ ///
+ private class PixelFormatConversionShim : IDisposable
{
- private readonly SKImageInfo _nfo;
- private readonly IntPtr _fb;
- private readonly int _rowBytes;
- private SKBitmap _bitmap;
+ private readonly SKBitmap _bitmap;
+ private readonly SKImageInfo _destinationInfo;
+ private readonly IntPtr _framebufferAddress;
- public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes)
+ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress)
{
- _nfo = nfo;
- _fb = fb;
- _rowBytes = rowBytes;
+ _destinationInfo = destinationInfo;
+ _framebufferAddress = framebufferAddress;
+
+ // Create bitmap using default platform settings
+ _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height);
+
+ if (!_bitmap.CanCopyTo(destinationInfo.ColorType))
+ {
+ _bitmap.Dispose();
+
+ throw new Exception(
+ $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}");
+ }
+
+ Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
-
- _bitmap = new SKBitmap(nfo.Width, nfo.Height);
- if (!_bitmap.CanCopyTo(nfo.ColorType))
+ if (Surface == null)
{
_bitmap.Dispose();
+
throw new Exception(
- $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}");
+ $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}");
}
+
+ SurfaceCopyHandler = Disposable.Create(CopySurface);
}
- public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
+ ///
+ /// Skia surface.
+ ///
+ public SKSurface Surface { get; }
+ ///
+ /// Handler to start conversion via surface copy.
+ ///
+ public IDisposable SurfaceCopyHandler { get; }
+
+ ///
public void Dispose()
{
- using (var tmp = _bitmap.Copy(_nfo.ColorType))
- tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes);
+ Surface.Dispose();
_bitmap.Dispose();
}
-
- }
-
- public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
- {
- var fb = _surface.Lock();
- PixelFormatShim shim = null;
- SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(),
- SKAlphaType.Premul);
- var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ??
- (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes))
- .CreateSurface();
- if (surface == null)
- throw new Exception("Unable to create a surface for pixel format " + fb.Format +
- " or pixel format translator");
- var canvas = surface.Canvas;
-
-
- canvas.RestoreToCount(0);
- canvas.Save();
- canvas.ResetMatrix();
- return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb);
+ ///
+ /// Convert and copy surface to a framebuffer.
+ ///
+ private void CopySurface()
+ {
+ using (var snapshot = Surface.Snapshot())
+ {
+ snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0,
+ SKImageCachingHint.Disallow);
+ }
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index fb134b728c..af4cdb8056 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -1,3 +1,6 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
using System;
using Avalonia.Media;
using Avalonia.Platform;
@@ -5,14 +8,169 @@ using SkiaSharp;
namespace Avalonia.Skia
{
- abstract class GeometryImpl : IGeometryImpl
+ ///
+ /// A Skia implementation of .
+ ///
+ public abstract class GeometryImpl : IGeometryImpl
{
+ private PathCache _pathCache;
+
+ ///
public abstract Rect Bounds { get; }
public abstract SKPath EffectivePath { get; }
- public abstract bool FillContains(Point point);
- public abstract Rect GetRenderBounds(Pen pen);
- public abstract IGeometryImpl Intersect(IGeometryImpl geometry);
- public abstract bool StrokeContains(Pen pen, Point point);
- public abstract ITransformedGeometryImpl WithTransform(Matrix transform);
+
+ ///
+ public bool FillContains(Point point)
+ {
+ return PathContainsCore(EffectivePath, point);
+ }
+
+ ///
+ public bool StrokeContains(Pen pen, Point point)
+ {
+ // Skia requires to compute stroke path to check for point containment.
+ // Due to that we are caching using stroke width.
+ // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
+
+ var strokeWidth = (float)(pen?.Thickness ?? 0);
+
+ if (!_pathCache.HasCacheFor(strokeWidth))
+ {
+ UpdatePathCache(strokeWidth);
+ }
+
+ return PathContainsCore(_pathCache.CachedStrokePath, point);
+ }
+
+ ///
+ /// Update path cache for given stroke width.
+ ///
+ /// Stroke width.
+ private void UpdatePathCache(float strokeWidth)
+ {
+ var strokePath = new SKPath();
+
+ // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
+ if (Math.Abs(strokeWidth) < float.Epsilon)
+ {
+ _pathCache.Cache(strokePath, strokeWidth, Bounds);
+ }
+ else
+ {
+ using (var paint = new SKPaint())
+ {
+ paint.IsStroke = true;
+ paint.StrokeWidth = strokeWidth;
+
+ paint.GetFillPath(EffectivePath, strokePath);
+
+ _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
+ }
+ }
+ }
+
+ ///
+ /// Check Skia path if it contains a point.
+ ///
+ /// Path to check.
+ /// Point.
+ /// True, if point is contained in a path.
+ private static bool PathContainsCore(SKPath path, Point point)
+ {
+ return path.Contains((float)point.X, (float)point.Y);
+ }
+
+ ///
+ public IGeometryImpl Intersect(IGeometryImpl geometry)
+ {
+ var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect);
+
+ return result == null ? null : new StreamGeometryImpl(result);
+ }
+
+ ///
+ public Rect GetRenderBounds(Pen pen)
+ {
+ var strokeWidth = (float)(pen?.Thickness ?? 0);
+
+ if (!_pathCache.HasCacheFor(strokeWidth))
+ {
+ UpdatePathCache(strokeWidth);
+ }
+
+ return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
+ }
+
+ ///
+ public ITransformedGeometryImpl WithTransform(Matrix transform)
+ {
+ return new TransformedGeometryImpl(this, transform);
+ }
+
+ ///
+ /// Invalidate all caches. Call after chaning path contents.
+ ///
+ protected void InvalidateCaches()
+ {
+ _pathCache.Invalidate();
+ }
+
+ private struct PathCache
+ {
+ private float _cachedStrokeWidth;
+
+ ///
+ /// Tolerance for two stroke widths to be deemed equal
+ ///
+ public const float Tolerance = float.Epsilon;
+
+ ///
+ /// Cached contour path.
+ ///
+ public SKPath CachedStrokePath { get; private set; }
+
+ ///
+ /// Cached geometry render bounds.
+ ///
+ public Rect CachedGeometryRenderBounds { get; private set; }
+
+ ///
+ /// Is cached valid for given stroke width.
+ ///
+ /// Stroke width to check.
+ /// True, if CachedStrokePath can be used for given stroke width.
+ public bool HasCacheFor(float strokeWidth)
+ {
+ return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
+ }
+
+ ///
+ /// Cache path for given stroke width. Takes ownership of a passed path.
+ ///
+ /// Path to cache.
+ /// Stroke width to cache.
+ /// Render bounds to use.
+ public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
+ {
+ if (CachedStrokePath != path)
+ {
+ CachedStrokePath?.Dispose();
+ }
+
+ CachedStrokePath = path;
+ CachedGeometryRenderBounds = geometryRenderBounds;
+ _cachedStrokeWidth = strokeWidth;
+ }
+
+ ///
+ /// Invalidate cache state.
+ ///
+ public void Invalidate()
+ {
+ CachedStrokePath?.Dispose();
+ CachedGeometryRenderBounds = Rect.Empty;
+ _cachedStrokeWidth = default(float);
+ }
+ }
}
}
diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
new file mode 100644
index 0000000000..d587a989cc
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
@@ -0,0 +1,47 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers
+{
+ ///
+ /// Helps with saving images to stream/file.
+ ///
+ public static class ImageSavingHelper
+ {
+ ///
+ /// Save Skia image to a file.
+ ///
+ /// Image to save
+ /// Target file.
+ public static void SaveImage(SKImage image, string fileName)
+ {
+ if (image == null) throw new ArgumentNullException(nameof(image));
+ if (fileName == null) throw new ArgumentNullException(nameof(fileName));
+
+ using (var stream = File.Create(fileName))
+ {
+ SaveImage(image, stream);
+ }
+ }
+
+ ///
+ /// Save Skia image to a stream.
+ ///
+ /// Image to save
+ /// Target stream.
+ public static void SaveImage(SKImage image, Stream stream)
+ {
+ if (image == null) throw new ArgumentNullException(nameof(image));
+ if (stream == null) throw new ArgumentNullException(nameof(stream));
+
+ using (var data = image.Encode())
+ {
+ data.SaveTo(stream);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
new file mode 100644
index 0000000000..307af708af
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
@@ -0,0 +1,35 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers
+{
+ ///
+ /// Helps with resolving pixel formats to Skia color types.
+ ///
+ public static class PixelFormatHelper
+ {
+ ///
+ /// Resolve given format to Skia color type.
+ ///
+ /// Format to resolve.
+ /// Resolved color type.
+ public static SKColorType ResolveColorType(PixelFormat? format)
+ {
+ var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
+
+ // TODO: This looks like some leftover hack
+ var runtimePlatform = AvaloniaLocator.Current?.GetService();
+ var runtime = runtimePlatform?.GetRuntimeInfo();
+
+ if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux)
+ {
+ colorType = SKColorType.Bgra8888;
+ }
+
+ return colorType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
new file mode 100644
index 0000000000..5aa5de2abc
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
@@ -0,0 +1,23 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ ///
+ /// Extended bitmap implementation that allows for drawing it's contents.
+ ///
+ internal interface IDrawableBitmapImpl : IBitmapImpl
+ {
+ ///
+ /// Draw bitmap to a drawing context.
+ ///
+ /// Drawing context.
+ /// Source rect.
+ /// Destination rect.
+ /// Paint to use.
+ void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint);
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
new file mode 100644
index 0000000000..332b8547bf
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
@@ -0,0 +1,92 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using Avalonia.Platform;
+using Avalonia.Skia.Helpers;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ ///
+ /// Immutable Skia bitmap.
+ ///
+ public class ImmutableBitmap : IDrawableBitmapImpl
+ {
+ private readonly SKImage _image;
+
+ ///
+ /// Create immutable bitmap from given stream.
+ ///
+ /// Stream containing encoded data.
+ public ImmutableBitmap(Stream stream)
+ {
+ using (var skiaStream = new SKManagedStream(stream))
+ {
+ _image = SKImage.FromEncodedData(SKData.Create(skiaStream));
+
+ if (_image == null)
+ {
+ throw new ArgumentException("Unable to load bitmap from provided data");
+ }
+
+ PixelWidth = _image.Width;
+ PixelHeight = _image.Height;
+ }
+ }
+
+ ///
+ /// Create immutable bitmap from given pixel data copy.
+ ///
+ /// Width of data pixels.
+ /// Height of data pixels.
+ /// Stride of data pixels.
+ /// Format of data pixels.
+ /// Data pixels.
+ public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data)
+ {
+ var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul);
+
+ _image = SKImage.FromPixelCopy(imageInfo, data, stride);
+
+ if (_image == null)
+ {
+ throw new ArgumentException("Unable to create bitmap from provided data");
+ }
+
+ PixelWidth = width;
+ PixelHeight = height;
+ }
+
+ ///
+ public int PixelWidth { get; }
+
+ ///
+ public int PixelHeight { get; }
+
+ ///
+ public void Dispose()
+ {
+ _image.Dispose();
+ }
+
+ ///
+ public void Save(string fileName)
+ {
+ ImageSavingHelper.SaveImage(_image, fileName);
+ }
+
+ ///
+ public void Save(Stream stream)
+ {
+ ImageSavingHelper.SaveImage(_image, stream);
+ }
+
+ ///
+ public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
+ {
+ context.Canvas.DrawImage(_image, sourceRect, destRect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 50e65f45dc..d4e6403dc9 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -1,21 +1,21 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Platform;
-using SkiaSharp;
namespace Avalonia.Skia
{
- public partial class PlatformRenderInterface : IPlatformRenderInterface
+ ///
+ /// Skia platform render interface.
+ ///
+ public class PlatformRenderInterface : IPlatformRenderInterface
{
- public IBitmapImpl CreateBitmap(int width, int height)
- {
- return CreateRenderTargetBitmap(width, height, 96, 96);
- }
-
+ ///
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
@@ -27,27 +27,19 @@ namespace Avalonia.Skia
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
}
+ ///
public IStreamGeometryImpl CreateStreamGeometry()
{
return new StreamGeometryImpl();
}
- public IBitmapImpl LoadBitmap(System.IO.Stream stream)
+ ///
+ public IBitmapImpl LoadBitmap(Stream stream)
{
- using (var s = new SKManagedStream(stream))
- {
- var bitmap = SKBitmap.Decode(s);
- if (bitmap != null)
- {
- return new BitmapImpl(bitmap);
- }
- else
- {
- throw new ArgumentException("Unable to load bitmap from provided data");
- }
- }
+ return new ImmutableBitmap(stream);
}
+ ///
public IBitmapImpl LoadBitmap(string fileName)
{
using (var stream = File.OpenRead(fileName))
@@ -56,16 +48,13 @@ namespace Avalonia.Skia
}
}
+ ///
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
{
- using (var tmp = new SKBitmap())
- {
- tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul)
- , data, stride);
- return new BitmapImpl(tmp.Copy());
- }
+ return new ImmutableBitmap(width, height, stride, format, data);
}
+ ///
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
int width,
int height,
@@ -73,24 +62,47 @@ namespace Avalonia.Skia
double dpiY)
{
if (width < 1)
+ {
throw new ArgumentException("Width can't be less than 1", nameof(width));
+ }
+
if (height < 1)
+ {
throw new ArgumentException("Height can't be less than 1", nameof(height));
+ }
+
+ var dpi = new Vector(dpiX, dpiY);
- return new BitmapImpl(width, height, new Vector(dpiX, dpiY));
+ var createInfo = new SurfaceRenderTarget.CreateInfo
+ {
+ Width = width,
+ Height = height,
+ Dpi = dpi,
+ DisableTextLcdRendering = false
+ };
+
+ return new SurfaceRenderTarget(createInfo);
}
+ ///
public virtual IRenderTarget CreateRenderTarget(IEnumerable