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+ }
0 commit comments