Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
UriImage.cs
1using System.ComponentModel;
2using System.IO;
3using System.Threading;
4using System.Threading.Tasks;
10using Microsoft.Maui;
11using SkiaSharp;
12using Svg.Skia;
13
15{
20 public class UriImage : ContentView
21 {
22 // The source URI string of the image to load.
23 public static readonly BindableProperty SourceProperty =
24 BindableProperty.Create(
25 nameof(Source),
26 typeof(string),
27 typeof(UriImage),
28 default(string),
29 propertyChanged: OnSourceChanged);
30
31 // Placeholder to show if loading fails.
32 public static readonly BindableProperty ErrorPlaceholderProperty =
33 BindableProperty.Create(
34 nameof(ErrorPlaceholder),
35 typeof(ImageSource),
36 typeof(UriImage),
37 default(ImageSource),
38 propertyChanged: OnErrorPlaceholderChanged);
39
40 // Aspect property to control image aspect ratio.
41 public static readonly BindableProperty AspectProperty =
42 BindableProperty.Create(
43 nameof(Aspect),
44 typeof(Aspect),
45 typeof(UriImage),
46 Aspect.AspectFill,
47 propertyChanged: OnAspectChanged);
48
49 // Optional custom parent ID for cache entries (defaults to Source).
50 public static readonly BindableProperty ParentIdProperty =
51 BindableProperty.Create(
52 nameof(ParentId),
53 typeof(string),
54 typeof(UriImage),
55 default(string));
56
57 // Whether to make this cache entry permanent or temporary.
58 public static readonly BindableProperty PermanentProperty =
59 BindableProperty.Create(
60 nameof(Permanent),
61 typeof(bool),
62 typeof(UriImage),
63 false);
64
65 // Optional custom cache duration; null means use service default.
66 public static readonly BindableProperty CacheDurationProperty =
67 BindableProperty.Create(
68 nameof(CacheDuration),
69 typeof(TimeSpan?),
70 typeof(UriImage),
71 Constants.Cache.DefaultImageCache);
72
73 // Read-only IsLoading property to indicate loading state.
74 private static readonly BindablePropertyKey IsLoadingPropertyKey =
75 BindableProperty.CreateReadOnly(
76 nameof(IsLoading),
77 typeof(bool),
78 typeof(UriImage),
79 false);
80
81 public static readonly BindableProperty IsLoadingProperty = IsLoadingPropertyKey.BindableProperty;
82
86 public string Source
87 {
88 get => (string)this.GetValue(SourceProperty);
89 set => this.SetValue(SourceProperty, value);
90 }
91
95 [TypeConverter(typeof(ImageSourceConverter))]
96 public ImageSource ErrorPlaceholder
97 {
98 get => (ImageSource)this.GetValue(ErrorPlaceholderProperty);
99 set => this.SetValue(ErrorPlaceholderProperty, value);
100 }
101
106 {
107 get => (Aspect)this.GetValue(AspectProperty);
108 set => this.SetValue(AspectProperty, value);
109 }
110
114 public string ParentId
115 {
116 get => (string)this.GetValue(ParentIdProperty);
117 set => this.SetValue(ParentIdProperty, value);
118 }
119
123 public bool Permanent
124 {
125 get => (bool)this.GetValue(PermanentProperty);
126 set => this.SetValue(PermanentProperty, value);
127 }
128
132 public TimeSpan? CacheDuration
133 {
134 get => (TimeSpan?)this.GetValue(CacheDurationProperty);
135 set => this.SetValue(CacheDurationProperty, value);
136 }
137
141 public bool IsLoading
142 {
143 get => (bool)this.GetValue(IsLoadingProperty);
144 private set => this.SetValue(IsLoadingPropertyKey, value);
145 }
146
147 private readonly Image imageView;
148 private readonly ActivityIndicator spinner;
149 private readonly ObservableTask<int> loadImageTask;
150 private bool isErrorDisplayed;
151 private bool isDisposed;
152
153 public UriImage()
154 {
155 this.spinner = new ActivityIndicator
156 {
157 IsVisible = false,
158 IsRunning = false,
159 HorizontalOptions = LayoutOptions.Center,
160 VerticalOptions = LayoutOptions.Center
161 };
162
163 this.imageView = new Image
164 {
165 Aspect = this.Aspect
166 };
167
168 this.Content = new Grid
169 {
170 Children = { this.imageView, this.spinner }
171 };
172
174 Builder.Named("UriImage.Load");
175 Builder.AutoStart(false);
176 Builder.WithPolicy(Policies.Retry(3, (int Attempt, Exception Error) => TimeSpan.FromMilliseconds(Math.Min(2000, 200 * Attempt))));
177 Builder.Run(this.LoadImageCoreAsync);
178
179 this.loadImageTask = Builder.Build();
180 this.loadImageTask.StateChanged += this.OnLoadTaskStateChanged;
181 }
182
183 protected override void OnHandlerChanging(HandlerChangingEventArgs Args)
184 {
185 base.OnHandlerChanging(Args);
186
187 if (Args.NewHandler is null && !this.isDisposed)
188 {
189 this.loadImageTask.StateChanged -= this.OnLoadTaskStateChanged;
190 this.loadImageTask.Dispose();
191 this.isDisposed = true;
192 }
193 }
194
195 private static void OnAspectChanged(BindableObject Bindable, object OldValue, object NewValue)
196 {
197 UriImage Control = (UriImage)Bindable;
198 Control.imageView.Aspect = (Aspect)NewValue;
199 }
200
201 private static void OnSourceChanged(BindableObject Bindable, object OldValue, object NewValue)
202 {
203 UriImage Control = (UriImage)Bindable;
204 Control.isErrorDisplayed = false;
205 if (!Control.isDisposed)
206 Control.loadImageTask.Run();
207 }
208
209 private static void OnErrorPlaceholderChanged(BindableObject Bindable, object OldValue, object NewValue)
210 {
211 UriImage Control = (UriImage)Bindable;
212 if (Control.isErrorDisplayed)
213 Control.imageView.Source = (ImageSource)NewValue;
214 }
215
216 private void OnLoadTaskStateChanged(object? Sender, ObservableTaskStatus Status)
217 {
218 bool IsRunning = Status == ObservableTaskStatus.Running;
219 MainThread.BeginInvokeOnMainThread(() =>
220 {
221 this.IsLoading = IsRunning;
222 this.spinner.IsRunning = IsRunning;
223 this.spinner.IsVisible = IsRunning;
224 });
225
226 if (Status == ObservableTaskStatus.Failed && !this.isErrorDisplayed)
227 _ = this.ShowErrorAsync();
228 }
229
230 private async Task LoadImageCoreAsync(TaskContext<int> Context)
231 {
232 try
233 {
234 string? CurrentSource = this.Source;
235 if (string.IsNullOrWhiteSpace(CurrentSource))
236 {
237 await this.ShowErrorAsync();
238 return;
239 }
240
241 ImageSource? NewImage = await this.ResolveImageSourceAsync(CurrentSource, Context.CancellationToken);
242 if (NewImage is null)
243 {
244 await this.ShowErrorAsync();
245 return;
246 }
247
248 Context.CancellationToken.ThrowIfCancellationRequested();
249
250 await MainThread.InvokeOnMainThreadAsync(() =>
251 {
252 this.isErrorDisplayed = false;
253 this.imageView.Source = NewImage;
254 });
255 }
256 catch (OperationCanceledException)
257 {
258 throw;
259 }
260 catch (Exception Ex)
261 {
262 ServiceRef.LogService.LogException(Ex);
263 throw;
264 }
265 }
266
267 private async Task<ImageSource?> ResolveImageSourceAsync(string CurrentSource, CancellationToken CancellationToken)
268 {
269 bool IsSvgUri = CurrentSource.EndsWith(".svg", StringComparison.OrdinalIgnoreCase);
270
271 if (CurrentSource.StartsWith("resource://", StringComparison.Ordinal))
272 {
273 CancellationToken.ThrowIfCancellationRequested();
274 return ImageSource.FromResource(CurrentSource[11..]);
275 }
276
277 if (CurrentSource.StartsWith("file://", StringComparison.Ordinal))
278 {
279 CancellationToken.ThrowIfCancellationRequested();
280 if (IsSvgUri)
281 return CurrentSource[7..^4];
282 return CurrentSource[7..];
283 }
284
285 if (!Uri.TryCreate(CurrentSource, UriKind.Absolute, out Uri? UriValue))
286 return null;
287
288 string Key = string.IsNullOrEmpty(this.ParentId) ? CurrentSource : this.ParentId;
289 IInternetCacheService CacheService = ServiceRef.InternetCacheService;
290 (byte[]? ImageBytes, string _) = await CacheService.GetOrFetch(UriValue, Key, this.Permanent);
291
292 if (ImageBytes is null || ImageBytes.Length == 0)
293 {
294 ServiceRef.LogService.LogWarning($"UriImage: Failed to load image from '{CurrentSource}'.");
295 return null;
296 }
297
298 byte[] ProcessedBytes = ImageBytes;
299 if (IsSvgUri)
300 ProcessedBytes = this.TryConvertSvgToPng(ProcessedBytes, CancellationToken);
301
302 CancellationToken.ThrowIfCancellationRequested();
303
304 return this.CreateImageSourceFromBytes(ProcessedBytes);
305 }
306
307 private ImageSource CreateImageSourceFromBytes(byte[] ImageBytes)
308 {
309 byte[] Buffer = ImageBytes;
310 return ImageSource.FromStream(() => new MemoryStream(Buffer));
311 }
312
313 private byte[] TryConvertSvgToPng(byte[] ImageBytes, CancellationToken CancellationToken)
314 {
315 try
316 {
317 CancellationToken.ThrowIfCancellationRequested();
318 SKSvg Svg = new SKSvg();
319 using (MemoryStream InputStream = new MemoryStream(ImageBytes))
320 {
321 Svg.Load(InputStream);
322 }
323
324 CancellationToken.ThrowIfCancellationRequested();
325
326 if (Svg.Picture is null)
327 return ImageBytes;
328
329 using (MemoryStream OutputStream = new MemoryStream())
330 {
331 bool Converted = Svg.Picture.ToImage(OutputStream, SKColor.Parse("#00FFFFFF"), SKEncodedImageFormat.Png, 100, 1, 1, SKColorType.Rgba8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb());
332 if (!Converted)
333 return ImageBytes;
334
335 return OutputStream.ToArray();
336 }
337 }
338 catch (OperationCanceledException)
339 {
340 throw;
341 }
342 catch (Exception Ex)
343 {
344 ServiceRef.LogService.LogException(Ex);
345 return ImageBytes;
346 }
347 }
348
349 private Task ShowErrorAsync()
350 {
351 return MainThread.InvokeOnMainThreadAsync(() =>
352 {
353 this.isErrorDisplayed = true;
354 this.IsLoading = false;
355 this.spinner.IsRunning = false;
356 this.spinner.IsVisible = false;
357 this.imageView.Source = this.ErrorPlaceholder;
358 });
359 }
360
361 private static bool IsSvg(byte[] imageBytes)
362 {
363 if (imageBytes is null || imageBytes.Length < 5)
364 return false;
365
366 // Check if it starts with "<svg" (ignoring whitespace)
367 string Header = System.Text.Encoding.UTF8.GetString(imageBytes, 0, Math.Min(imageBytes.Length, 256));
368 return Header.TrimStart().StartsWith("<svg", StringComparison.OrdinalIgnoreCase);
369 }
370 }
371}
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
A control that displays an image from a URI (string) with built-in caching, loading indicator,...
Definition: UriImage.cs:21
bool IsLoading
Indicates whether the image is currently loading.
Definition: UriImage.cs:142
Aspect Aspect
The Aspect for the inner Image.
Definition: UriImage.cs:106
bool Permanent
Whether to make this cache entry permanent (true) or temporary (false).
Definition: UriImage.cs:124
string Source
Source URI string of the image to load.
Definition: UriImage.cs:87
TimeSpan? CacheDuration
Custom cache duration for this entry; null uses default expiry.
Definition: UriImage.cs:133
ImageSource ErrorPlaceholder
Image shown if loading fails.
Definition: UriImage.cs:97
string ParentId
Optional custom parent ID for cache entries.
Definition: UriImage.cs:115
Provides a data-binding friendly mechanism to manage and report the status of asynchronous operations...
Defines operations for caching arbitrary internet-fetchable content.
Task<(byte[]? Data, string ContentType)> GetOrFetch(Uri Uri, string ParentId, bool Permanent)
Retrieves content for the specified URI from cache or fetches and caches it if missing.
Definition: ImplTypes.g.cs:58
ObservableTaskStatus
UI-friendly status for an observable async operation. (Renamed to avoid collision with System....
class Header(ISimulationNode Parent, Model Model)
Represents an identity property.
Definition: Header.cs:18