Skip to content

Commit 89c074f

Browse files
committed
feat: svg rendering
Special thanks to @AuroraZiling
1 parent ea4514b commit 89c074f

File tree

5 files changed

+213
-32
lines changed

5 files changed

+213
-32
lines changed

Directory.Packages.props

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
<Project>
2-
<PropertyGroup>
3-
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4-
<AvaloniaVersion>11.2.0</AvaloniaVersion>
5-
</PropertyGroup>
6-
<ItemGroup>
7-
<!-- Base packages -->
8-
<PackageVersion Include="PolySharp" Version="1.15.0"/>
9-
<!-- Avalonia packages -->
10-
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
11-
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)"/>
12-
<PackageVersion Include="Avalonia.Themes.Simple" Version="$(AvaloniaVersion)"/>
13-
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
14-
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)"/>
15-
<PackageVersion Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
16-
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersion)"/>
17-
<!-- Markdown packages -->
18-
<PackageVersion Include="ColorCode.Core" Version="2.0.15"/>
19-
<PackageVersion Include="Markdig" Version="0.41.3"/>
20-
<!-- NUnit packages -->
21-
<PackageVersion Include="BenchmarkDotNet" Version="0.15.1"/>
22-
<PackageVersion Include="coverlet.collector" Version="6.0.3"/>
23-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
24-
<PackageVersion Include="NUnit" Version="4.3.2"/>
25-
<PackageVersion Include="NUnit.Analyzers" Version="4.5.0"/>
26-
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0"/>
27-
</ItemGroup>
2+
<PropertyGroup>
3+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
<AvaloniaVersion>11.3.0</AvaloniaVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<!-- Base packages -->
8+
<PackageVersion Include="Avalonia.Svg" Version="11.3.0" />
9+
<PackageVersion Include="PolySharp" Version="1.15.0" />
10+
<!-- Avalonia packages -->
11+
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)" />
12+
<PackageVersion Include="Avalonia.Themes.Simple" Version="$(AvaloniaVersion)" />
13+
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
14+
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
15+
<PackageVersion Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
16+
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
17+
<!-- Markdown packages -->
18+
<PackageVersion Include="ColorCode.Core" Version="2.0.15" />
19+
<PackageVersion Include="Markdig" Version="0.41.3" />
20+
<!-- NUnit packages -->
21+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.1" />
22+
<PackageVersion Include="coverlet.collector" Version="6.0.3" />
23+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
24+
<PackageVersion Include="NUnit" Version="4.3.2" />
25+
<PackageVersion Include="NUnit.Analyzers" Version="4.5.0" />
26+
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
27+
</ItemGroup>
2828
</Project>

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ It supports **real-time rendering** of Markdown content, so it's ideal for appli
4545
- [x] Table support
4646
- [x] Code block syntax highlighting
4747
- [x] Image support
48+
- [x] Bitmap
49+
- [x] SVG
4850
- [x] Selectable text across elements
4951
- [ ] LaTeX support
5052
- [ ] HTML rendering
@@ -137,9 +139,9 @@ Distributed under the Apache 2.0 License. See [LICENSE](LICENSE) for more inform
137139
- **markdig** - [BSD-2-Clause License](https://github.com/xoofx/markdig/blob/master/license.txt)
138140
- Markdown parser for Everywhere.Markdown rendering
139141
- Source repo: https://github.com/xoofx/markdig
140-
- **AsyncImageLoader.Avalonia** - [MIT License](https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/LICENSE)
141-
- Asynchronous image loading for Avalonia
142-
- Source repo: https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia
142+
- **Svg.Skia** - [MIT License](https://github.com/wieslawsoltes/Svg.Skia/blob/master/LICENSE.TXT)
143+
- Svg rendering for images
144+
- Source repo: https://github.com/wieslawsoltes/Svg.Skia
143145
- **ColorCode** - [MIT License](https://github.com/CommunityToolkit/ColorCode-Universal/blob/main/license.md)
144146
- Syntax highlighting for code blocks
145147
- Source repo: https://github.com/CommunityToolkit/ColorCode-Universal
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Media;
4+
using Avalonia.Media.Imaging;
5+
using Avalonia.Svg;
6+
using Avalonia.Threading;
7+
using Svg.Model;
8+
9+
namespace LiveMarkdown.Avalonia;
10+
11+
public class AsyncImageLoader
12+
{
13+
public static readonly AttachedProperty<string?> SourceProperty =
14+
AvaloniaProperty.RegisterAttached<AsyncImageLoader, Image, string?>("Source");
15+
16+
public static void SetSource(Image obj, string? value) => obj.SetValue(SourceProperty, value);
17+
18+
public static string? GetSource(Image obj) => obj.GetValue(SourceProperty);
19+
20+
public static HttpClient HttpClient { get; set; } = new();
21+
22+
public static AsyncImageLoaderCache Cache { get; set; } = new RamBasedAsyncImageLoaderCache();
23+
24+
private readonly static Dictionary<Image, (Task task, CancellationTokenSource cts)> ImageLoadTasks = new();
25+
26+
static AsyncImageLoader()
27+
{
28+
SourceProperty.Changed.AddClassHandler<Image>(HandleSourceChanged);
29+
}
30+
31+
private static void HandleSourceChanged(Image sender, AvaloniaPropertyChangedEventArgs args)
32+
{
33+
// This method is always called on the UI thread, so we can safely access the UI elements.
34+
35+
if (ImageLoadTasks.TryGetValue(sender, out var pair))
36+
{
37+
pair.cts.Cancel(); // Cancel the previous loading task if it exists
38+
ImageLoadTasks.Remove(sender);
39+
}
40+
41+
var newSource = args.NewValue as string;
42+
if (string.IsNullOrEmpty(newSource))
43+
{
44+
sender.Source = null; // Clear the image source if the new value is null or empty
45+
return;
46+
}
47+
48+
if (Cache.GetImage(newSource!) is { } cachedImage)
49+
{
50+
sender.Source = cachedImage; // Use the cached image if available
51+
return;
52+
}
53+
54+
var newPair = CreateLoadPair(sender, newSource!);
55+
ImageLoadTasks.Add(sender, newPair);
56+
}
57+
58+
private static (Task task, CancellationTokenSource cts) CreateLoadPair(Image image, string source)
59+
{
60+
var cts = new CancellationTokenSource();
61+
var task = Task.Run(
62+
async () =>
63+
{
64+
try
65+
{
66+
using var response = await HttpClient.GetAsync(source, cts.Token);
67+
response.EnsureSuccessStatusCode();
68+
using var stream = await response.Content.ReadAsStreamAsync();
69+
70+
var buffer = new byte[16];
71+
// check if the stream is svg
72+
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
73+
if (bytesRead == 0)
74+
{
75+
return null;
76+
}
77+
78+
stream.Seek(0, SeekOrigin.Begin); // Reset the stream position
79+
80+
var isBinary = buffer.Take(bytesRead).Any(b => b == 0); // check for null bytes, which indicate binary data
81+
if (isBinary)
82+
{
83+
// If the stream is binary, treat it as a Bitmap
84+
return (object)WriteableBitmap.Decode(stream);
85+
}
86+
87+
return SvgSource.Load(stream, new SvgParameters
88+
{
89+
// Handle rm size
90+
Css = ":nth-child(0) { font-size: 16px; font-family: Arial, sans-serif; }"
91+
});
92+
}
93+
catch (OperationCanceledException)
94+
{
95+
// Task was cancelled, do nothing
96+
throw;
97+
}
98+
catch
99+
{
100+
// Handle other exceptions as needed
101+
return null; // Clear the image source on error
102+
}
103+
},
104+
cts.Token)
105+
.ContinueWith(
106+
t =>
107+
{
108+
Dispatcher.UIThread.Invoke(() =>
109+
{
110+
if (ImageLoadTasks.TryGetValue(image, out var pair) && pair.cts == cts)
111+
{
112+
ImageLoadTasks.Remove(image); // Remove the task from the dictionary
113+
}
114+
115+
if (t.Exception is not null) return; // Operation was cancelled or failed, do nothing
116+
117+
IImage? result = t.Result switch
118+
{
119+
Bitmap bitmap => bitmap,
120+
SvgSource svgSource => new SvgImage { Source = svgSource },
121+
_ => null // Clear the image source if the result is not a valid image
122+
};
123+
124+
if (result is not null) Cache.SetImage(source, result);
125+
126+
image.Source = result;
127+
});
128+
},
129+
cts.Token);
130+
131+
return (task, cts);
132+
}
133+
}
134+
135+
public abstract class AsyncImageLoaderCache
136+
{
137+
public abstract IImage? GetImage(string source);
138+
139+
public abstract void SetImage(string source, IImage image);
140+
}
141+
142+
public class RamBasedAsyncImageLoaderCache : AsyncImageLoaderCache
143+
{
144+
private readonly Dictionary<string, WeakReference<IImage>> _cache = new();
145+
146+
private int _checkThreshold = 16;
147+
148+
public override IImage? GetImage(string source)
149+
{
150+
lock (_cache)
151+
{
152+
if (_cache.TryGetValue(source, out var weakRef) && weakRef.TryGetTarget(out var image))
153+
{
154+
return image;
155+
}
156+
157+
return null;
158+
}
159+
}
160+
161+
public override void SetImage(string source, IImage image)
162+
{
163+
lock (_cache)
164+
{
165+
_cache[source] = new WeakReference<IImage>(image);
166+
167+
if (_cache.Count <= _checkThreshold) return;
168+
169+
// Clean up weak references that are no longer alive
170+
var keysToRemove = _cache.Where(kvp => !kvp.Value.TryGetTarget(out _)).Select(kvp => kvp.Key).ToList();
171+
foreach (var key in keysToRemove)
172+
{
173+
_cache.Remove(key);
174+
}
175+
176+
if (_cache.Count > _checkThreshold) _checkThreshold *= 2;
177+
else if (_cache.Count < _checkThreshold / 4) _checkThreshold /= 2;
178+
}
179+
}
180+
}

src/LiveMarkdown.Avalonia/LiveMarkdown.Avalonia.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
</PropertyGroup>
1414

1515
<ItemGroup>
16-
<PackageReference Include="AsyncImageLoader.Avalonia"/>
1716
<PackageReference Include="Avalonia"/>
17+
<PackageReference Include="Avalonia.Svg" />
1818
<PackageReference Include="ColorCode.Core"/>
1919
<PackageReference Include="Markdig"/>
2020
<PackageReference Include="PolySharp">

src/LiveMarkdown.Avalonia/MarkdownRenderer.Rendering.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// @author https://github.com/DearVa
22

33
using System.Runtime.CompilerServices;
4-
using AsyncImageLoader;
54
using Avalonia.Controls;
65
using Avalonia.Controls.Primitives;
76
using Avalonia.Layout;
@@ -315,7 +314,7 @@ protected override bool UpdateCore(
315314
Classes = { "Link" },
316315
};
317316
}
318-
ImageLoader.SetSource(img, linkInline.Url);
317+
AsyncImageLoader.SetSource(img, linkInline.Url);
319318
}
320319
else
321320
{

0 commit comments

Comments
 (0)