diff --git a/source/FFImageLoading.Common/Cache/SimpleLRUCache.cs b/source/FFImageLoading.Common/Cache/SimpleLRUCache.cs new file mode 100644 index 000000000..b1d8a2e86 --- /dev/null +++ b/source/FFImageLoading.Common/Cache/SimpleLRUCache.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; + +namespace FFImageLoading.Cache +{ + public class SimpleLRUCache + { + private readonly object _lock = new object(); + private readonly int _capacity; + private readonly LinkedList> _lru = new LinkedList>(); + private readonly Dictionary>> _cache = new Dictionary>>(); + + public SimpleLRUCache(int capacity) + { + if (capacity < 1) + throw new ArgumentException("Capacity must be greater than zero."); + + _capacity = capacity; + } + + public void AddOrReplace(TKey key, TValue value) + { + lock(_lock) + { + if (_cache.TryGetValue(key, out var node)) + { + node.Value.Value = value; + _lru.Remove(node); + } + else + { + if (_capacity == _lru.Count) + { + var lastNode = _lru.First; + _cache.Remove(lastNode.Value.Key); + _lru.RemoveFirst(); + } + + node = new LinkedListNode>(new CacheItem(key, value)); + _cache.Add(key, node); + } + + _lru.AddLast(node); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (_lock) + { + if (_cache.TryGetValue(key, out var node)) + { + _lru.Remove(node); + _lru.AddLast(node); + + value = node.Value.Value; + return true; + } + + value = default; + return false; + } + } + + private class CacheItem + { + public K Key { get; } + public V Value { get; set; } + + public CacheItem(K key, V value) + { + this.Key = key; + this.Value = value; + } + } + } +} diff --git a/source/FFImageLoading.Svg.Shared/SkSvg.cs b/source/FFImageLoading.Svg.Shared/SkSvg.cs index 3c6a7ff48..11a41d004 100644 --- a/source/FFImageLoading.Svg.Shared/SkSvg.cs +++ b/source/FFImageLoading.Svg.Shared/SkSvg.cs @@ -63,6 +63,8 @@ public SKSvg(float pixelsPerInch, SKSize canvasSize) ThrowOnUnsupportedElement = DefaultThrowOnUnsupportedElement; } + public bool HasRasterImage { get; private set; } + public float PixelsPerInch { get; set; } public bool ThrowOnUnsupportedElement { get; set; } @@ -207,7 +209,6 @@ private SKPicture Load(XDocument xdoc, CancellationToken token = default) // read elements LoadElements(svg.Elements(), canvas, stroke, fill, token); - Picture = recorder.EndRecording(); } @@ -315,6 +316,7 @@ private void ReadElement(XElement e, SKCanvas canvas, SKPaint stroke, SKPaint fi { if (bitmap != null) { + HasRasterImage = true; canvas.DrawBitmap(bitmap, image.Rect); } } diff --git a/source/FFImageLoading.Svg.Shared/SvgDataResolver.cs b/source/FFImageLoading.Svg.Shared/SvgDataResolver.cs index 9b98b5982..cb0b2ef82 100644 --- a/source/FFImageLoading.Svg.Shared/SvgDataResolver.cs +++ b/source/FFImageLoading.Svg.Shared/SvgDataResolver.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Text.RegularExpressions; using FFImageLoading.Helpers; +using FFImageLoading.Cache; #if __IOS__ using Foundation; @@ -43,6 +44,7 @@ public class SvgDataResolver : IVectorDataResolver { #pragma warning disable RECS0108 // Warns about static fields in generic types private static readonly object _encodingLock = new object(); + private static readonly SimpleLRUCache _cache = new SimpleLRUCache(50); #pragma warning restore RECS0108 // Warns about static fields in generic types #if __IOS__ @@ -205,62 +207,81 @@ private async Task Decode(SKPicture picture, SKBitmap bitmap public async Task Resolve(string identifier, TaskParameter parameters, CancellationToken token) { - var source = parameters.Source; + var replaceStringMapKey = (ReplaceStringMap == null || ReplaceStringMap.Count == 0) + ? null : string.Join(",", ReplaceStringMap.Select(x => string.Format("{0}/{1}", x.Key, x.Value)).OrderBy(v => v)); + var key = replaceStringMapKey == null ? identifier : $"{identifier};{replaceStringMapKey}"; - if (!string.IsNullOrWhiteSpace(parameters.LoadingPlaceholderPath) && parameters.LoadingPlaceholderPath == identifier) - source = parameters.LoadingPlaceholderSource; - else if (!string.IsNullOrWhiteSpace(parameters.ErrorPlaceholderPath) && parameters.ErrorPlaceholderPath == identifier) - source = parameters.ErrorPlaceholderSource; + DataResolverResult resolvedData; - var resolvedData = await (Configuration.DataResolverFactory ?? new DataResolverFactory()) - .GetResolver(identifier, source, parameters, Configuration) - .Resolve(identifier, parameters, token).ConfigureAwait(false); + if (_cache.TryGetValue(key, out var picture)) + { + resolvedData = new DataResolverResult() + { + LoadingResult = LoadingResult.MemoryCache, + ImageInformation = new ImageInformation() + }; + } + else + { + var source = parameters.Source; - if (resolvedData?.Stream == null) - throw new FileNotFoundException(identifier); + if (!string.IsNullOrWhiteSpace(parameters.LoadingPlaceholderPath) && parameters.LoadingPlaceholderPath == identifier) + source = parameters.LoadingPlaceholderSource; + else if (!string.IsNullOrWhiteSpace(parameters.ErrorPlaceholderPath) && parameters.ErrorPlaceholderPath == identifier) + source = parameters.ErrorPlaceholderSource; - var svg = new SKSvg() - { - ThrowOnUnsupportedElement = false, - }; - SKPicture picture; + resolvedData = await (Configuration.DataResolverFactory ?? new DataResolverFactory()) + .GetResolver(identifier, source, parameters, Configuration) + .Resolve(identifier, parameters, token).ConfigureAwait(false); - if (ReplaceStringMap == null || ReplaceStringMap.Count == 0) - { - using (var svgStream = resolvedData.Stream) - { - picture = svg.Load(svgStream, token); - } - } - else - { - using (var svgStream = resolvedData.Stream) - using (var reader = new StreamReader(svgStream)) - { - var inputString = await reader.ReadToEndAsync().ConfigureAwait(false); + if (resolvedData?.Stream == null) + throw new FileNotFoundException(identifier); + + var svg = new SKSvg() + { + ThrowOnUnsupportedElement = false, + }; + + if (ReplaceStringMap == null || ReplaceStringMap.Count == 0) + { + using (var svgStream = resolvedData.Stream) + { + picture = svg.Load(svgStream, token); + } + } + else + { + using (var svgStream = resolvedData.Stream) + using (var reader = new StreamReader(svgStream)) + { + var inputString = await reader.ReadToEndAsync().ConfigureAwait(false); - foreach (var map in ReplaceStringMap - .Where(v => v.Key.StartsWith("regex:", StringComparison.OrdinalIgnoreCase))) - { - inputString = Regex.Replace(inputString, map.Key.Substring(6), map.Value); - } + foreach (var map in ReplaceStringMap + .Where(v => v.Key.StartsWith("regex:", StringComparison.OrdinalIgnoreCase))) + { + inputString = Regex.Replace(inputString, map.Key.Substring(6), map.Value); + } - var builder = new StringBuilder(inputString); + var builder = new StringBuilder(inputString); - foreach (var map in ReplaceStringMap - .Where(v => !v.Key.StartsWith("regex:", StringComparison.OrdinalIgnoreCase))) - { - builder.Replace(map.Key, map.Value); - } + foreach (var map in ReplaceStringMap + .Where(v => !v.Key.StartsWith("regex:", StringComparison.OrdinalIgnoreCase))) + { + builder.Replace(map.Key, map.Value); + } - token.ThrowIfCancellationRequested(); + token.ThrowIfCancellationRequested(); - using (var svgFinalStream = new MemoryStream(Encoding.UTF8.GetBytes(builder.ToString()))) - { - picture = svg.Load(svgFinalStream); - } - } - } + using (var svgFinalStream = new MemoryStream(Encoding.UTF8.GetBytes(builder.ToString()))) + { + picture = svg.Load(svgFinalStream); + } + } + } + + if (!svg.HasRasterImage) + _cache.AddOrReplace(key, picture); + } token.ThrowIfCancellationRequested(); @@ -294,8 +315,6 @@ public async Task Resolve(string identifier, TaskParameter p sizeX = (int)(sizeY / picture.CullRect.Height * picture.CullRect.Width); } - resolvedData.ImageInformation.SetType(ImageInformation.ImageType.SVG); - using (var bitmap = new SKBitmap(new SKImageInfo((int)sizeX, (int)sizeY))) using (var canvas = new SKCanvas(bitmap)) using (var paint = new SKPaint()) @@ -309,6 +328,10 @@ public async Task Resolve(string identifier, TaskParameter p token.ThrowIfCancellationRequested(); + resolvedData.ImageInformation.SetType(ImageInformation.ImageType.SVG); + resolvedData.ImageInformation.SetCurrentSize((int)sizeX, (int)sizeY); + resolvedData.ImageInformation.SetOriginalSize((int)picture.CullRect.Width, (int)picture.CullRect.Height); + return await Decode(picture, bitmap, resolvedData).ConfigureAwait(false); } }