diff --git a/Directory.Build.props b/Directory.Build.props index 06c97ff996..cb1123bc28 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,8 @@  - 3.3.0 + 3.4.0 - 11.3.9 + 11.3.9.1 $(VersionSuffix) Wiesław Šoltés Wiesław Šoltés diff --git a/samples/AvaloniaSvgSample/MainWindow.axaml b/samples/AvaloniaSvgSample/MainWindow.axaml index 96c8866237..5aa7c14328 100644 --- a/samples/AvaloniaSvgSample/MainWindow.axaml +++ b/samples/AvaloniaSvgSample/MainWindow.axaml @@ -85,7 +85,7 @@ HorizontalAlignment="Stretch"> - + diff --git a/samples/AvaloniaSvgSample/MainWindow.axaml.cs b/samples/AvaloniaSvgSample/MainWindow.axaml.cs index 908ed656f1..e43538d58c 100644 --- a/samples/AvaloniaSvgSample/MainWindow.axaml.cs +++ b/samples/AvaloniaSvgSample/MainWindow.axaml.cs @@ -141,11 +141,15 @@ private void InitializeModelSample() var assemblyName = typeof(MainWindow).Assembly.GetName().Name ?? "AvaloniaSvgSample"; var resourcePath = $"avares://{assemblyName}/Assets/__tiger.svg"; var originalSource = SvgSource.Load(resourcePath, null); - svgModelOriginal.Source = new SvgImage { Source = originalSource }; + var originalImage = new SvgImage { Source = originalSource }; + svgModelOriginal.Source = originalImage; - var modifiedSource = SvgSource.Load(resourcePath, null); - ApplyGrayscale(modifiedSource); - svgModelModified.Source = new SvgImage { Source = modifiedSource }; + var cloneImage = originalImage.Clone(); + if (cloneImage.Source is { } cloneSource) + { + ApplyGrayscale(cloneSource); + } + svgModelModified.Source = cloneImage; } private static void ApplyGrayscale(SvgSource source) diff --git a/samples/AvaloniaSvgSkiaSample/MainWindow.axaml b/samples/AvaloniaSvgSkiaSample/MainWindow.axaml index 157e0afa56..af4e2a329a 100644 --- a/samples/AvaloniaSvgSkiaSample/MainWindow.axaml +++ b/samples/AvaloniaSvgSkiaSample/MainWindow.axaml @@ -104,7 +104,7 @@ HorizontalAlignment="Stretch"> - + diff --git a/samples/AvaloniaSvgSkiaSample/MainWindow.axaml.cs b/samples/AvaloniaSvgSkiaSample/MainWindow.axaml.cs index a8a6afad03..ad72030ba5 100644 --- a/samples/AvaloniaSvgSkiaSample/MainWindow.axaml.cs +++ b/samples/AvaloniaSvgSkiaSample/MainWindow.axaml.cs @@ -153,11 +153,15 @@ private void InitializeModelSample() var assemblyName = typeof(MainWindow).Assembly.GetName().Name ?? "AvaloniaSvgSkiaSample"; var resourcePath = $"avares://{assemblyName}/Assets/__tiger.svg"; var originalSource = SvgSource.Load(resourcePath, null); - svgModelOriginal.Source = new SvgImage { Source = originalSource }; + var originalImage = new SvgImage { Source = originalSource }; + svgModelOriginal.Source = originalImage; - var modifiedSource = SvgSource.Load(resourcePath, null); - ApplyGrayscale(modifiedSource); - svgModelModified.Source = new SvgImage { Source = modifiedSource }; + var cloneImage = originalImage.Clone(); + if (cloneImage.Source is { } cloneSource) + { + ApplyGrayscale(cloneSource); + } + svgModelModified.Source = cloneImage; } private static void ApplyGrayscale(SvgSource source) diff --git a/samples/TestApp/TestApp.json b/samples/TestApp/TestApp.json new file mode 100644 index 0000000000..ac8d3a81e7 --- /dev/null +++ b/samples/TestApp/TestApp.json @@ -0,0 +1 @@ +{"Paths":["/Users/wieslawsoltes/Downloads/svg/demos/__AJ_Digital_Camera.svg","/Users/wieslawsoltes/Downloads/svg/demos/__issue-134-01.svg","/Users/wieslawsoltes/Downloads/svg/demos/__issue-227-02.svg","/Users/wieslawsoltes/Downloads/svg/demos/__issue-252-01.svg","/Users/wieslawsoltes/Downloads/svg/demos/__Telefunken_FuBK_test_pattern.svg","/Users/wieslawsoltes/Downloads/svg/demos/__tiger.svg","/Users/wieslawsoltes/Downloads/svg/demos/!opacity.svg","/Users/wieslawsoltes/Downloads/svg/demos/bunny.svg","/Users/wieslawsoltes/Downloads/svg/demos/compuserver_msn_Ford_Focus.svg","/Users/wieslawsoltes/Downloads/svg/demos/Example.svg","/Users/wieslawsoltes/Downloads/svg/demos/ExportFile_16x.svg","/Users/wieslawsoltes/Downloads/svg/demos/feGaussianBlur.svg","/Users/wieslawsoltes/Downloads/svg/demos/gallardo.svg","/Users/wieslawsoltes/Downloads/svg/demos/ios_app_buttons.svg","/Users/wieslawsoltes/Downloads/svg/demos/KOMPAS-Graphic.svg","/Users/wieslawsoltes/Downloads/svg/demos/logo.svg","/Users/wieslawsoltes/Downloads/svg/demos/marker.svg","/Users/wieslawsoltes/Downloads/svg/demos/opacity-groups.svg","/Users/wieslawsoltes/Downloads/svg/demos/pattern.svg","/Users/wieslawsoltes/Downloads/svg/demos/stroke-miterlimit.svg","/Users/wieslawsoltes/Downloads/svg/demos/SVG_logo.svg","/Users/wieslawsoltes/Downloads/svg/demos/svglogo_.svg","/Users/wieslawsoltes/Downloads/svg/demos/svglogo.svg","/Users/wieslawsoltes/Downloads/svg/demos/symbol.svg","/Users/wieslawsoltes/Downloads/svg/demos/tommek_Car.svg","/Users/wieslawsoltes/Downloads/svg/demos/use.svg"],"Query":null} \ No newline at end of file diff --git a/src/Svg.Controls.Avalonia/SvgImage.cs b/src/Svg.Controls.Avalonia/SvgImage.cs index 7461d0393d..2a8d8fdf39 100644 --- a/src/Svg.Controls.Avalonia/SvgImage.cs +++ b/src/Svg.Controls.Avalonia/SvgImage.cs @@ -107,6 +107,21 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } + /// + /// Creates a deep clone of this with an independent source. + /// + /// A new instance. + public SvgImage Clone() + { + var clone = new SvgImage(); + if (Source is { } source) + { + clone.Source = source.Clone(); + } + + return clone; + } + /// /// Raises the event. /// diff --git a/src/Svg.Controls.Avalonia/SvgSource.cs b/src/Svg.Controls.Avalonia/SvgSource.cs index c6e0e529d1..d9137ef0d2 100644 --- a/src/Svg.Controls.Avalonia/SvgSource.cs +++ b/src/Svg.Controls.Avalonia/SvgSource.cs @@ -145,4 +145,16 @@ public void RebuildFromModel() Picture = picture.DeepClone(); } + + /// + /// Creates a deep clone of this . + /// + /// A new instance. + public SvgSource Clone() + { + return new SvgSource + { + Picture = Picture?.DeepClone() + }; + } } diff --git a/src/Svg.Controls.Skia.Avalonia/SvgCustomDrawOperation.cs b/src/Svg.Controls.Skia.Avalonia/SvgCustomDrawOperation.cs index 95726137b5..5e871309d8 100644 --- a/src/Svg.Controls.Skia.Avalonia/SvgCustomDrawOperation.cs +++ b/src/Svg.Controls.Skia.Avalonia/SvgCustomDrawOperation.cs @@ -48,9 +48,6 @@ public void Render(ImmediateDrawingContext context) return; } - lock (_svg.Sync) - { - _svg.Draw(canvas); - } + _svg.Draw(canvas); } } diff --git a/src/Svg.Controls.Skia.Avalonia/SvgImage.cs b/src/Svg.Controls.Skia.Avalonia/SvgImage.cs index 351a93e852..0158fe1c81 100644 --- a/src/Svg.Controls.Skia.Avalonia/SvgImage.cs +++ b/src/Svg.Controls.Skia.Avalonia/SvgImage.cs @@ -114,9 +114,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { var css = string.Concat(change.GetNewValue(), ' ', CurrentCss); - if (Source?.Css != css) + if (Source is { } source && source.Css != css) { - Source?.ReLoad(new SvgParameters(null, css)); + source.ReLoad(new SvgParameters(source.Parameters?.Entities, css)); RaiseInvalidated(EventArgs.Empty); } } @@ -125,14 +125,28 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { var css = string.Concat(Css, ' ', change.GetNewValue()); - if (Source?.Css != css) + if (Source is { } source && source.Css != css) { - Source?.ReLoad(new SvgParameters(null, css)); + source.ReLoad(new SvgParameters(source.Parameters?.Entities, css)); RaiseInvalidated(EventArgs.Empty); } } } + /// + /// Creates a deep clone of this with an independent source. + /// + /// A new instance. + public SvgImage Clone() + { + return new SvgImage + { + Source = Source?.Clone(), + Css = Css, + CurrentCss = CurrentCss + }; + } + /// /// Raises the event. /// diff --git a/src/Svg.Controls.Skia.Avalonia/SvgSource.cs b/src/Svg.Controls.Skia.Avalonia/SvgSource.cs index 53ed71a06f..3d67b04833 100644 --- a/src/Svg.Controls.Skia.Avalonia/SvgSource.cs +++ b/src/Svg.Controls.Skia.Avalonia/SvgSource.cs @@ -6,6 +6,8 @@ using System.Diagnostics; using System.IO; using System.Net.Http; +using System.Text; +using System.Threading; using Avalonia.Metadata; using Avalonia.Platform; using SkiaSharp; @@ -32,6 +34,11 @@ public sealed class SvgSource : IDisposable private SvgParameters? _originalParameters; private string? _originalPath; private Stream? _originalStream; + private Uri? _originalBaseUri; + private int _activeRenders; + private readonly ThreadLocal _renderDepth = new(() => 0); + private bool _disposePending; + private bool _disposed; [Content] public string? Path { get; init; } @@ -40,16 +47,7 @@ public sealed class SvgSource : IDisposable public string? Css { get; init; } - public SKSvg? Svg - { - get - { - lock (Sync) - { - return _skSvg; - } - } - } + public SKSvg? Svg => Volatile.Read(ref _skSvg); public SvgParameters? Parameters => _originalParameters; @@ -57,13 +55,31 @@ public SKPicture? Picture { get { - if (_picture is null && Path is not null) + var picture = _picture; + if (picture is not null) + { + return picture; + } + + var skSvg = Volatile.Read(ref _skSvg); + if (skSvg is { }) + { + picture = skSvg.Picture; + lock (Sync) + { + _picture ??= picture; + } + return picture; + } + + var path = Path; + if (path is null) { - var entitiesCopy = Entities is null ? null : new Dictionary(Entities); - _picture = LoadImpl(this, Path, _baseUri, new SvgParameters(entitiesCopy, Css)); + return null; } - return _picture; + var entitiesCopy = Entities is null ? null : new Dictionary(Entities); + return LoadImpl(this, path, _baseUri, new SvgParameters(entitiesCopy, Css)); } set => _picture = value; } @@ -88,14 +104,41 @@ public SvgSource(IServiceProvider serviceProvider) public void Dispose() { + SKPicture? picture = null; + SKSvg? skSvg = null; + Stream? originalStream = null; + lock (Sync) { - _picture?.Dispose(); + if (_disposed) + { + return; + } + + _disposePending = true; + + if (_activeRenders > 0) + { + if (_renderDepth.Value > 0) + { + return; + } + + while (_activeRenders > 0) + { + Monitor.Wait(Sync); + } + + if (_disposed) + { + return; + } + } - _originalPath = null; - _originalStream?.Dispose(); - _originalStream = null; + DisposeCoreLocked(out picture, out skSvg, out originalStream); } + + DisposeResources(picture, skSvg, originalStream); } /// @@ -113,88 +156,78 @@ static SvgSource() private static SKPicture? Load(SvgSource source, string? path, SvgParameters? parameters) { - source._originalPath = path; - source._originalStream?.Dispose(); - source._originalStream = null; - if (path is null) { + lock (source.Sync) + { + source._originalPath = null; + source._originalStream?.Dispose(); + source._originalStream = null; + source._originalParameters = parameters; + source._originalBaseUri = null; + source._skSvg = null; + source._picture = null; + } return null; } var skSvg = new SKSvg(); skSvg.Load(path, parameters); - lock (source.Sync) - { - source._skSvg = skSvg; - } - return skSvg.Picture; - } - - private static SKPicture? Load(SvgSource source, Stream stream, SvgParameters? parameters = null) - { - if (source._originalStream != stream) - { - source._originalStream?.Dispose(); - source._originalStream = new MemoryStream(); - stream.CopyTo(source._originalStream); - } - - source._originalPath = null; - source._originalParameters = parameters; - source._originalStream.Position = 0; + var picture = skSvg.Picture; - var skSvg = new SKSvg(); - skSvg.Load(source._originalStream, parameters); lock (source.Sync) { + source._originalPath = path; + source._originalStream?.Dispose(); + source._originalStream = null; + source._originalParameters = parameters; + source._originalBaseUri = null; source._skSvg = skSvg; + source._picture = picture; } - return skSvg.Picture; + + return picture; } - private static SKPicture? FromSvg(string svg) + private static SKPicture? Load(SvgSource source, Stream stream, SvgParameters? parameters = null, Uri? baseUri = null) { - var skSvg = new SKSvg(); - skSvg.FromSvg(svg); - return skSvg.Picture; + var cachedStream = new MemoryStream(); + stream.CopyTo(cachedStream); + return LoadFromCachedStream(source, cachedStream, parameters, baseUri); } - private static SKPicture? FromSvg(SvgSource source, string svg) + private static SKPicture? LoadFromCachedStream(SvgSource source, MemoryStream cachedStream, SvgParameters? parameters, Uri? baseUri) { + cachedStream.Position = 0; var skSvg = new SKSvg(); - skSvg.FromSvg(svg); + skSvg.Load(cachedStream, parameters, baseUri); + var picture = skSvg.Picture; + lock (source.Sync) { + source._originalStream?.Dispose(); + source._originalStream = cachedStream; + source._originalPath = null; + source._originalParameters = parameters; + source._originalBaseUri = baseUri; source._skSvg = skSvg; + source._picture = picture; } - return skSvg.Picture; + + return picture; } - private static SKPicture? FromSvgDocument(SvgDocument? svgDocument) + private static MemoryStream CreateStream(string svg) { - if (svgDocument is { }) - { - var skSvg = new SKSvg(); - skSvg.FromSvgDocument(svgDocument); - return skSvg.Picture; - } - return null; + return new MemoryStream(Encoding.UTF8.GetBytes(svg)); } - private static SKPicture? FromSvgDocument(SvgSource source, SvgDocument? svgDocument) + private static MemoryStream CreateStream(SvgDocument document) { - if (svgDocument is { }) - { - var skSvg = new SKSvg(); - skSvg.FromSvgDocument(svgDocument); - lock (source.Sync) - { - source._skSvg = skSvg; - } - return skSvg.Picture; - } - return null; + var stream = new MemoryStream(); + document.Write(stream, useBom: false); + stream.Position = 0; + return stream; } private static SvgSource? ThrowOnMissingResource(string path) @@ -219,7 +252,7 @@ static SvgSource() if (response.IsSuccessStatusCode) { var stream = response.Content.ReadAsStreamAsync().Result; - return Load(source, stream, parameters); + return Load(source, stream, parameters, uriHttp); } } catch (HttpRequestException e) @@ -245,7 +278,7 @@ static SvgSource() ThrowOnMissingResource(path); return null; } - return Load(source, stream, parameters); + return Load(source, stream, parameters, baseUri); } } @@ -259,7 +292,7 @@ static SvgSource() public static SvgSource Load(string path, Uri? baseUri = default, SvgParameters? parameters = null) { var source = new SvgSource(baseUri); - source._picture = LoadImpl(source, path, baseUri, parameters); + LoadImpl(source, path, baseUri, parameters); return source; } @@ -269,9 +302,21 @@ public static SvgSource Load(string path, Uri? baseUri = default, SvgParameters? /// The svg source. /// The svg source. public static SvgSource LoadFromSvg(string svg) + { + return LoadFromSvg(svg, null); + } + + /// + /// Loads svg source from svg source. + /// + /// The svg source. + /// The svg parameters. + /// The svg source. + public static SvgSource LoadFromSvg(string svg, SvgParameters? parameters) { var source = new SvgSource(default(Uri)); - source._picture = FromSvg(source, svg); + using var stream = CreateStream(svg); + Load(source, stream, parameters); return source; } @@ -284,7 +329,7 @@ public static SvgSource LoadFromSvg(string svg) public static SvgSource LoadFromStream(Stream stream, SvgParameters? parameters = null) { var source = new SvgSource(default(Uri)); - source._picture = Load(source, stream, parameters); + Load(source, stream, parameters); return source; } @@ -294,9 +339,41 @@ public static SvgSource LoadFromStream(Stream stream, SvgParameters? parameters /// The svg document. /// The svg source. public static SvgSource LoadFromSvgDocument(SvgDocument document) + { + return LoadFromSvgDocument(document, null); + } + + /// t + /// Loads svg source from svg document. + /// + /// The svg document. + /// The svg parameters. + /// The svg source. + public static SvgSource LoadFromSvgDocument(SvgDocument document, SvgParameters? parameters) { var source = new SvgSource(default(Uri)); - source._picture = FromSvgDocument(source, document); + if (parameters is null) + { + var originalStream = CreateStream(document); + var skSvg = new SKSvg(); + skSvg.FromSvgDocument(document); + var picture = skSvg.Picture; + + lock (source.Sync) + { + source._originalStream?.Dispose(); + source._originalStream = originalStream; + source._originalPath = null; + source._originalParameters = null; + source._originalBaseUri = document.BaseUri; + source._skSvg = skSvg; + source._picture = picture; + } + return source; + } + + using var stream = CreateStream(document); + Load(source, stream, parameters, document.BaseUri); return source; } @@ -306,36 +383,244 @@ public static SvgSource LoadFromSvgDocument(SvgDocument document) /// public void RebuildFromModel() { + SKSvg? skSvg; lock (Sync) { - if (_skSvg is null) + skSvg = _skSvg; + } + + if (skSvg is null) + { + return; + } + + skSvg.RebuildFromModel(); + + lock (Sync) + { + if (ReferenceEquals(_skSvg, skSvg)) { - return; + _picture = skSvg.Picture; + } + } + } + + /// + /// Creates a deep clone of this with independent model data. + /// + /// A new instance. + public SvgSource Clone() + { + var clone = new SvgSource(_baseUri) + { + Path = Path, + Entities = Entities is null ? null : new Dictionary(Entities), + Css = Css + }; + + SvgParameters? originalParameters; + string? originalPath; + Uri? originalBaseUri; + SKSvg? skSvg; + SKPicture? picture; + MemoryStream? originalStreamCopy = null; + SKPicture? clonedPicture = null; + var canClonePicture = false; + + lock (Sync) + { + originalParameters = _originalParameters; + originalPath = _originalPath; + originalBaseUri = _originalBaseUri; + skSvg = _skSvg; + picture = _picture; + + if (_originalStream is { } originalStream) + { + originalStreamCopy = new MemoryStream(); + var position = originalStream.Position; + originalStream.Position = 0; + originalStream.CopyTo(originalStreamCopy); + originalStreamCopy.Position = 0; + originalStream.Position = position; } - _skSvg.RebuildFromModel(); - _picture = _skSvg.Picture; + canClonePicture = picture is { } + && _originalStream is null + && _originalPath is null + && Path is null; + + if (canClonePicture && picture is { }) + { + clonedPicture = ClonePicture(picture); + } } + + clone._originalParameters = CloneParameters(originalParameters); + clone._originalPath = originalPath; + clone._originalBaseUri = originalBaseUri; + clone._originalStream = originalStreamCopy; + + if (skSvg is { }) + { + clone._skSvg = skSvg.Clone(); + } + else if (canClonePicture) + { + clone._picture = clonedPicture; + } + + return clone; + } + + private static SvgParameters? CloneParameters(SvgParameters? parameters) + { + if (parameters is null) + { + return null; + } + + var entities = parameters.Value.Entities; + var entitiesCopy = entities is null ? null : new Dictionary(entities); + return new SvgParameters(entitiesCopy, parameters.Value.Css); + } + + private static SKPicture? ClonePicture(SKPicture? picture) + { + if (picture is null) + { + return null; + } + + using var recorder = new SKPictureRecorder(); + var canvas = recorder.BeginRecording(picture.CullRect); + canvas.DrawPicture(picture); + return recorder.EndRecording(); } public void ReLoad(SvgParameters? parameters) { + MemoryStream? streamCopy = null; + string? originalPath; + string? path; + Uri? originalBaseUri; + Uri? baseUri; + lock (Sync) { - _picture = null; - _skSvg = null; + if (_originalStream is null && _originalPath is null && Path is null) + { + return; + } + if (_originalStream is { } originalStream) + { + streamCopy = new MemoryStream(); + originalStream.Position = 0; + originalStream.CopyTo(streamCopy); + streamCopy.Position = 0; + } + originalPath = _originalPath; + originalBaseUri = _originalBaseUri; + path = Path; + baseUri = _baseUri; _originalParameters = parameters; + } + + if (streamCopy is { }) + { + LoadFromCachedStream(this, streamCopy, parameters, originalBaseUri); + return; + } + + if (originalPath is { }) + { + Load(this, originalPath, parameters); + return; + } + + if (path is { }) + { + LoadImpl(this, path, baseUri, parameters); + } + } - if (_originalStream == null) + internal bool BeginRender() + { + lock (Sync) + { + if (_disposed || _disposePending) { - _picture = Load(this, _originalPath, parameters); - return; + return false; } - _originalStream.Position = 0; + _activeRenders++; + _renderDepth.Value++; + return true; + } + } + + internal void EndRender() + { + SKPicture? picture = null; + SKSvg? skSvg = null; + Stream? originalStream = null; - _picture = Load(this, _originalStream, parameters); + lock (Sync) + { + if (_renderDepth.Value > 0) + { + _renderDepth.Value--; + } + + if (_activeRenders > 0 && --_activeRenders == 0) + { + if (_disposePending) + { + DisposeCoreLocked(out picture, out skSvg, out originalStream); + } + + Monitor.PulseAll(Sync); + } + } + + if (picture is not null || skSvg is not null || originalStream is not null) + { + DisposeResources(picture, skSvg, originalStream); } } + + private void DisposeCoreLocked(out SKPicture? picture, out SKSvg? skSvg, out Stream? originalStream) + { + picture = _picture; + skSvg = _skSvg; + originalStream = _originalStream; + + _picture = null; + _skSvg = null; + _originalPath = null; + _originalParameters = null; + _originalBaseUri = null; + _originalStream = null; + _disposePending = false; + _disposed = true; + } + + private static void DisposeResources(SKPicture? picture, SKSvg? skSvg, Stream? originalStream) + { + SKPicture? skSvgPicture = null; + if (skSvg is { } && picture is { }) + { + skSvgPicture = skSvg.Picture; + } + + skSvg?.Dispose(); + + if (picture is { } && !ReferenceEquals(picture, skSvgPicture)) + { + picture.Dispose(); + } + + originalStream?.Dispose(); + } } diff --git a/src/Svg.Controls.Skia.Avalonia/SvgSourceCustomDrawOperation.cs b/src/Svg.Controls.Skia.Avalonia/SvgSourceCustomDrawOperation.cs index 76037d1804..88b3982e86 100644 --- a/src/Svg.Controls.Skia.Avalonia/SvgSourceCustomDrawOperation.cs +++ b/src/Svg.Controls.Skia.Avalonia/SvgSourceCustomDrawOperation.cs @@ -34,37 +34,45 @@ public void Render(ImmediateDrawingContext context) return; } - var leaseFeature = context.TryGetFeature(); - if (leaseFeature is null) + if (!_svg.BeginRender()) { return; } - using var lease = leaseFeature.Lease(); - var canvas = lease?.SkCanvas; - if (canvas is null) + try { - return; - } + var leaseFeature = context.TryGetFeature(); + if (leaseFeature is null) + { + return; + } + + using var lease = leaseFeature.Lease(); + var canvas = lease?.SkCanvas; + if (canvas is null) + { + return; + } - lock (_svg.Sync) - { if (_svg.Svg is { } skSvg) { skSvg.Draw(canvas); + return; } - else - { - var picture = _svg.Picture; - if (picture is null) - { - return; - } - canvas.Save(); - canvas.DrawPicture(picture); - canvas.Restore(); + var picture = _svg.Picture; + if (picture is null) + { + return; } + + canvas.Save(); + canvas.DrawPicture(picture); + canvas.Restore(); + } + finally + { + _svg.EndRender(); } } } diff --git a/src/Svg.Skia/SKSvg.Model.cs b/src/Svg.Skia/SKSvg.Model.cs index 27c8d47fe6..a5c417b6bc 100644 --- a/src/Svg.Skia/SKSvg.Model.cs +++ b/src/Svg.Skia/SKSvg.Model.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System; using System.Collections.Generic; +using System.IO; +using System.Threading; using System.Xml; using ShimSkiaSharp; using Svg.Model; @@ -85,6 +87,8 @@ public static void Draw(SkiaSharp.SKCanvas skCanvas, string path, SkiaModel skia private SvgParameters? _originalParameters; private string? _originalPath; private System.IO.Stream? _originalStream; + private Uri? _originalBaseUri; + private int _activeDraws; public object Sync { get; } = new(); @@ -103,24 +107,45 @@ public virtual SkiaSharp.SKPicture? Picture { get { - if (_picture is { }) + SkiaSharp.SKPicture? picture; + SKPicture? model; + lock (Sync) { - return _picture; + picture = _picture; + if (picture is { }) + { + return picture; + } + + model = Model; + if (model is null) + { + return null; + } } - if (Model is null) + var newPicture = SkiaModel.ToSKPicture(model); + if (newPicture is null) { return null; } lock (Sync) { - if (_picture is null && Model is { } model) + if (!ReferenceEquals(Model, model)) { - _picture = SkiaModel.ToSKPicture(model); + newPicture.Dispose(); + return _picture; } - return _picture; + if (_picture is { } existing) + { + newPicture.Dispose(); + return existing; + } + + _picture = newPicture; + return newPicture; } } protected set => _picture = value; @@ -148,8 +173,12 @@ public DrawAttributes IgnoreAttributes public void ClearWireframePicture() { - WireframePicture?.Dispose(); - WireframePicture = null; + lock (Sync) + { + WaitForDrawsLocked(); + WireframePicture?.Dispose(); + WireframePicture = null; + } } public event EventHandler? OnDraw; @@ -174,73 +203,97 @@ public SKSvg() /// The rebuilt SkiaSharp picture, or null when no model exists. public SkiaSharp.SKPicture? RebuildFromModel() { - lock (Sync) + var model = Model; + if (model is null) { - var previous = _picture; - if (Model is null) + lock (Sync) { + WaitForDrawsLocked(); + _picture?.Dispose(); _picture = null; - previous?.Dispose(); - ClearWireframePicture(); - return null; + WireframePicture?.Dispose(); + WireframePicture = null; } + return null; + } - _picture = SkiaModel.ToSKPicture(Model); - if (!ReferenceEquals(previous, _picture)) + var rebuilt = SkiaModel.ToSKPicture(model); + lock (Sync) + { + WaitForDrawsLocked(); + if (!ReferenceEquals(Model, model)) { - previous?.Dispose(); + rebuilt?.Dispose(); + return _picture; } - ClearWireframePicture(); - return _picture; + + _picture?.Dispose(); + _picture = rebuilt; + WireframePicture?.Dispose(); + WireframePicture = null; } + return rebuilt; } - public SkiaSharp.SKPicture? Load(System.IO.Stream stream, SvgParameters? parameters = null) + /// + /// Creates a deep clone of the current , including model and reload data. + /// + /// A new instance with independent state. + public SKSvg Clone() { - SvgDocument? svgDocument; + var clone = new SKSvg(); - if (CacheOriginalStream) - { - if (_originalStream != stream) - { - _originalStream?.Dispose(); - _originalStream = new System.IO.MemoryStream(); - stream.CopyTo(_originalStream); - } + clone.Settings.AlphaType = Settings.AlphaType; + clone.Settings.ColorType = Settings.ColorType; + clone.Settings.SrgbLinear = Settings.SrgbLinear; + clone.Settings.Srgb = Settings.Srgb; + clone.Settings.TypefaceProviders = Settings.TypefaceProviders is null + ? null + : new List(Settings.TypefaceProviders); - _originalPath = null; - _originalParameters = parameters; - _originalStream.Position = 0; + clone.Wireframe = Wireframe; + clone.IgnoreAttributes = IgnoreAttributes; - svgDocument = SvgService.Open(_originalStream, parameters); - if (svgDocument is null) - { - return null; - } + clone._originalParameters = _originalParameters; + clone._originalPath = _originalPath; + clone._originalBaseUri = _originalBaseUri; + + if (_originalStream is { } originalStream) + { + clone._originalStream = new MemoryStream(); + var position = originalStream.Position; + originalStream.Position = 0; + originalStream.CopyTo(clone._originalStream); + clone._originalStream.Position = 0; + originalStream.Position = position; } - else + + if (Model is { } model) { - svgDocument = SvgService.Open(stream, parameters); - if (svgDocument is null) - { - return null; - } + clone.Model = model.DeepClone(); + clone.Drawable = Drawable?.DeepClone(); } - Model = SvgService.ToModel(svgDocument, AssetLoader, out var drawable, out _, _ignoreAttributes); - Drawable = drawable; - Picture = SkiaModel.ToSKPicture(Model); - WireframePicture?.Dispose(); - WireframePicture = null; + return clone; + } - return Picture; + public SkiaSharp.SKPicture? Load(System.IO.Stream stream, SvgParameters? parameters = null) + { + return LoadInternal(stream, parameters, null); } public SkiaSharp.SKPicture? Load(System.IO.Stream stream) => Load(stream, null); + public SkiaSharp.SKPicture? Load(System.IO.Stream stream, SvgParameters? parameters, Uri? baseUri) + { + return LoadInternal(stream, parameters, baseUri); + } + public SkiaSharp.SKPicture? Load(string? path, SvgParameters? parameters = null) { _originalPath = path; + _originalParameters = parameters; + _originalBaseUri = null; _originalStream?.Dispose(); _originalStream = null; @@ -266,6 +319,7 @@ public SKSvg() var svgDocument = SvgService.Open(reader); if (svgDocument is { }) { + _originalBaseUri = null; Model = SvgService.ToModel(svgDocument, AssetLoader, out var drawable, out _, _ignoreAttributes); Drawable = drawable; Picture = SkiaModel.ToSKPicture(Model); @@ -273,31 +327,84 @@ public SKSvg() WireframePicture = null; return Picture; } + return null; } - public SkiaSharp.SKPicture? ReLoad(SvgParameters? parameters) + private SkiaSharp.SKPicture? LoadInternal(System.IO.Stream stream, SvgParameters? parameters, Uri? baseUri) { - lock (Sync) + SvgDocument? svgDocument; + + if (CacheOriginalStream) { - if (!CacheOriginalStream) + if (_originalStream != stream) { - throw new ArgumentException($"Enable {nameof(CacheOriginalStream)} feature toggle to enable reload feature."); + _originalStream?.Dispose(); + _originalStream = new System.IO.MemoryStream(); + stream.CopyTo(_originalStream); } - Reset(); - + _originalPath = null; _originalParameters = parameters; + _originalBaseUri = baseUri; + _originalStream.Position = 0; - if (_originalStream == null) - { - return Load(_originalPath, parameters); - } + svgDocument = SvgService.Open(_originalStream, parameters); + } + else + { + _originalBaseUri = baseUri; + svgDocument = SvgService.Open(stream, parameters); + } - _originalStream.Position = 0; + if (svgDocument is null) + { + return null; + } + + if (baseUri is { }) + { + svgDocument.BaseUri = baseUri; + } + + Model = SvgService.ToModel(svgDocument, AssetLoader, out var drawable, out _, _ignoreAttributes); + Drawable = drawable; + Picture = SkiaModel.ToSKPicture(Model); + WireframePicture?.Dispose(); + WireframePicture = null; + + return Picture; + } + + public SkiaSharp.SKPicture? ReLoad(SvgParameters? parameters) + { + if (!CacheOriginalStream) + { + throw new ArgumentException($"Enable {nameof(CacheOriginalStream)} feature toggle to enable reload feature."); + } + + string? originalPath; + System.IO.Stream? originalStream; + Uri? originalBaseUri; - return Load(_originalStream, parameters); + lock (Sync) + { + _originalParameters = parameters; + originalPath = _originalPath; + originalStream = _originalStream; + originalBaseUri = _originalBaseUri; + } + + Reset(); + + if (originalStream == null) + { + return Load(originalPath, parameters); } + + originalStream.Position = 0; + + return Load(originalStream, parameters, originalBaseUri); } public SkiaSharp.SKPicture? FromSvg(string svg) @@ -350,6 +457,9 @@ public bool Save(string path, SkiaSharp.SKColor background, SkiaSharp.SKEncodedI public void Draw(SkiaSharp.SKCanvas canvas) { + BeginDraw(); + try + { var picture = Picture; if (picture is null) { @@ -359,17 +469,39 @@ public void Draw(SkiaSharp.SKCanvas canvas) canvas.Save(); if (Wireframe && Model is { }) { - WireframePicture ??= SkiaModel.ToWireframePicture(Model); - if (WireframePicture is { }) + var wireframePicture = WireframePicture; + if (wireframePicture is null && Model is { } model) { - canvas.DrawPicture(WireframePicture); + var newWireframe = SkiaModel.ToWireframePicture(model); + lock (Sync) + { + if (WireframePicture is null) + { + WireframePicture = newWireframe; + } + else + { + newWireframe?.Dispose(); + } + wireframePicture = WireframePicture; + } + } + + if (wireframePicture is { }) + { + canvas.DrawPicture(wireframePicture); } } else { canvas.DrawPicture(picture); } - canvas.Restore(); + canvas.Restore(); + } + finally + { + EndDraw(); + } RaiseOnDraw(new SKSvgDrawEventArgs(canvas)); } @@ -378,6 +510,7 @@ private void Reset() { lock (Sync) { + WaitForDrawsLocked(); Model = null; Drawable = null; @@ -394,4 +527,31 @@ public void Dispose() Reset(); _originalStream?.Dispose(); } + + private void BeginDraw() + { + lock (Sync) + { + _activeDraws++; + } + } + + private void EndDraw() + { + lock (Sync) + { + if (_activeDraws > 0 && --_activeDraws == 0) + { + Monitor.PulseAll(Sync); + } + } + } + + private void WaitForDrawsLocked() + { + while (_activeDraws > 0) + { + Monitor.Wait(Sync); + } + } } diff --git a/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs b/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs index a25803e2b1..e3ee2406ae 100644 --- a/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs +++ b/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs @@ -10,6 +10,8 @@ namespace Avalonia.Svg.UnitTests; public class SvgImageTests { + private const string SampleSvg = ""; + [AvaloniaFact] public void SvgImageExtension_Returns_VisualBrush_For_Brush_Property() { @@ -53,4 +55,16 @@ public void SvgBrushResource_Returns_VisualBrush() Assert.Equal(0.25, brush.TransformOrigin.Point.X, 6); Assert.Equal(0.75, brush.TransformOrigin.Point.Y, 6); } + + [AvaloniaFact] + public void SvgImage_Clone_Creates_Independent_Source() + { + var source = SvgSource.LoadFromSvg(SampleSvg); + var svgImage = new SvgImage { Source = source }; + + var clone = svgImage.Clone(); + + Assert.NotSame(svgImage, clone); + Assert.NotSame(svgImage.Source, clone.Source); + } } diff --git a/tests/Svg.Controls.Avalonia.UnitTests/SvgSourceTests.cs b/tests/Svg.Controls.Avalonia.UnitTests/SvgSourceTests.cs index 437dae5442..7a8beb4c5c 100644 --- a/tests/Svg.Controls.Avalonia.UnitTests/SvgSourceTests.cs +++ b/tests/Svg.Controls.Avalonia.UnitTests/SvgSourceTests.cs @@ -30,4 +30,14 @@ public void RebuildFromModel_RefreshesPicture() Assert.NotNull(source.Picture); Assert.NotSame(original, source.Picture); } + + [AvaloniaFact] + public void Clone_DeepClonesPicture() + { + var source = SvgSource.LoadFromSvg(SampleSvg); + var clone = source.Clone(); + + Assert.NotSame(source, clone); + Assert.NotSame(source.Picture, clone.Picture); + } } diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs index 1c56ad3eb5..75fce09a2a 100644 --- a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs @@ -10,6 +10,8 @@ namespace Avalonia.Svg.Skia.UnitTests; public class SvgImageTests { + private const string SampleSvg = ""; + [AvaloniaFact] public void SvgImage_Load() { @@ -63,4 +65,23 @@ public void SvgBrushResource_Returns_VisualBrush() Assert.Equal(0.25, brush.TransformOrigin.Point.X, 6); Assert.Equal(0.75, brush.TransformOrigin.Point.Y, 6); } + + [AvaloniaFact] + public void SvgImage_Clone_Creates_Independent_Source() + { + var source = SvgSource.LoadFromSvg(SampleSvg); + var svgImage = new SvgImage + { + Source = source, + Css = ".Red { fill: #FF0000; }", + CurrentCss = ".Blue { fill: #0000FF; }" + }; + + var clone = svgImage.Clone(); + + Assert.NotSame(svgImage, clone); + Assert.NotSame(svgImage.Source, clone.Source); + Assert.Equal(svgImage.Css, clone.Css); + Assert.Equal(svgImage.CurrentCss, clone.CurrentCss); + } } diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgSourceTests.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgSourceTests.cs index 9cc464886a..758a48ed7d 100644 --- a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgSourceTests.cs +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgSourceTests.cs @@ -1,7 +1,11 @@ +using System; using System.Linq; +using System.Reflection; +using System.Threading.Tasks; using Avalonia.Headless.XUnit; using Avalonia.Svg.Skia; using ShimSkiaSharp; +using Svg.Model; using Svg.Model.Services; using Xunit; @@ -52,4 +56,57 @@ public void RebuildFromModel_RefreshesPicture() Assert.NotNull(source.Picture); Assert.NotSame(original, source.Picture); } + + [AvaloniaFact] + public void LoadFromSvg_ReLoad_PreservesPicture() + { + var source = SvgSource.LoadFromSvg(SampleSvg); + + source.ReLoad(new SvgParameters(null, ".Black { fill: #000000; }")); + + Assert.NotNull(source.Svg); + Assert.NotNull(source.Picture); + } + + [AvaloniaFact] + public void Clone_DeepClonesModel() + { + var source = SvgSource.LoadFromSvg(SampleSvg); + var clone = source.Clone(); + + Assert.NotSame(source, clone); + Assert.NotNull(source.Svg); + Assert.NotNull(clone.Svg); + Assert.NotSame(source.Svg, clone.Svg); + Assert.NotSame(source.Svg?.Model, clone.Svg?.Model); + Assert.NotSame(source.Picture, clone.Picture); + } + + [AvaloniaFact] + public void Dispose_DuringRender_DoesNotDeadlock() + { + var source = SvgSource.LoadFromSvg(SampleSvg); + var beginRender = typeof(SvgSource).GetMethod("BeginRender", BindingFlags.Instance | BindingFlags.NonPublic); + var endRender = typeof(SvgSource).GetMethod("EndRender", BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.NotNull(beginRender); + Assert.NotNull(endRender); + + var task = Task.Run(() => + { + var started = (bool)(beginRender!.Invoke(source, null) ?? false); + if (!started) + { + return false; + } + + source.Dispose(); + endRender!.Invoke(source, null); + + return source.Svg is null && source.Picture is null; + }); + + Assert.True(task.Wait(TimeSpan.FromSeconds(2))); + Assert.True(task.Result); + } }