using System.Diagnostics; using System.IO; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using WpfAnimatedGif; using XFEToolBox.Client.Utilities; namespace XFEToolBox.Client.Wpf.Test; public static class WebImageSourceLoaderTests { private const string SvgMarkup = ""; [Test] public static void WebImageLoaderDecodesSvgIcoAndSvgDataUris() { Exception? failure = null; var thread = new Thread(() => { try { var svgImage = WebImageSourceLoader.Decode( Encoding.UTF8.GetBytes(SvgMarkup), "application/octet-stream", "icon?id=42"); Ensure(svgImage is DrawingImage { IsFrozen: true }, "没有后缀且类型不准确的 SVG 没有被内容识别并转换成 WPF DrawingImage。"); var icoPath = Path.Combine(AppContext.BaseDirectory, "TestAssets", "XFEToolBoxIcon.ico"); var icoImage = WebImageSourceLoader.Decode( File.ReadAllBytes(icoPath), "image/x-icon", "favicon.ico"); Ensure(icoImage is DrawingImage { IsFrozen: true, Drawing: ImageDrawing { ImageSource: BitmapSource { PixelWidth: >= 32 } } }, "ICO 没有选择清晰的位图帧并包装成可安全跨线程显示的图像。"); var dataUri = "data:image/svg+xml," + Uri.EscapeDataString(SvgMarkup); var dataImage = AwaitWithDispatcher(WebImageSourceLoader.LoadAsync(dataUri), TimeSpan.FromSeconds(10)); Ensure(dataImage is DrawingImage { IsFrozen: true }, "非 Base64 的 SVG data URI 没有被正确解码。"); var gifBytes = CreateAnimatedGif(); var gifImage = WebImageSourceLoader.Decode( gifBytes, "application/octet-stream", "icon?format=animated"); Ensure(gifImage is BitmapFrame { Decoder: GifBitmapDecoder gifDecoder, IsFrozen: true } && gifDecoder.Frames.Count == 2, "动态 GIF 没有被内容识别,或多帧信息在解码后丢失。"); var gifDataUri = "data:image/gif;base64," + Convert.ToBase64String(gifBytes); var gifDataImage = AwaitWithDispatcher( WebImageSourceLoader.LoadAsync(gifDataUri), TimeSpan.FromSeconds(10)); Ensure(gifDataImage is BitmapFrame { Decoder: GifBitmapDecoder dataGifDecoder } && dataGifDecoder.Frames.Count == 2, "Base64 GIF data URI 没有保留动画帧。"); } catch (Exception exception) { failure = exception; } }) { IsBackground = true }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); Ensure(thread.Join(TimeSpan.FromSeconds(15)), "网络图标格式测试超时。"); if (failure is not null) throw new InvalidOperationException($"网络图标格式加载测试失败:{failure.Message}", failure); } [Test] public static void WebImageLoaderRejectsOversizedAndNonImageContent() { EnsureThrows(() => WebImageSourceLoader.Decode(new byte[WebImageSourceLoader.MaximumImageBytes + 1], "image/png")); EnsureThrows(() => WebImageSourceLoader.Decode(Encoding.UTF8.GetBytes("not an image"), "application/octet-stream")); } [Test] public static void BackgroundDecodedPngCanBeDisplayedByAnimatedImageBehavior() { Exception? failure = null; var thread = new Thread(() => { Window? window = null; DispatcherUnhandledExceptionEventHandler? dispatcherFailureHandler = null; try { var dispatcher = Dispatcher.CurrentDispatcher; Exception? dispatcherFailure = null; dispatcherFailureHandler = (_, eventArgs) => { dispatcherFailure = eventArgs.Exception; eventArgs.Handled = true; }; dispatcher.UnhandledException += dispatcherFailureHandler; var dataUri = "data:image/png;base64," + Convert.ToBase64String(CreatePng()); var source = AwaitWithDispatcher( WebImageSourceLoader.LoadAsync(dataUri), TimeSpan.FromSeconds(10)); var image = new Image { Width = 32, Height = 32 }; var fallback = WebImageSourceLoader.Decode( Encoding.UTF8.GetBytes(SvgMarkup), "image/svg+xml", "fallback.svg"); ImageBehavior.SetAnimatedSource(image, fallback); window = new Window { Width = 80, Height = 80, Left = -10_000, Top = -10_000, ShowInTaskbar = false, WindowStartupLocation = WindowStartupLocation.Manual, Content = image }; window.Show(); PumpDispatcher(dispatcher); window.UpdateLayout(); Ensure(ReferenceEquals(image.Source, fallback), "主页卡片的初始回退图标没有完成显示。"); // 模拟 RecentUsageCardViewModel 在卡片 Loaded 后异步换成目录中的真实工具图标。 ImageBehavior.SetAnimatedSource(image, source); PumpDispatcher(dispatcher); window.UpdateLayout(); Ensure(dispatcherFailure is null, $"后台解码的静态 PNG 进入动画兼容显示路径时触发 UI 异常:{dispatcherFailure}"); Ensure(ReferenceEquals(image.Source, source), "后台解码的静态 PNG 没有替换主页卡片的回退图标。"); } catch (Exception exception) { failure = exception; } finally { window?.Close(); if (dispatcherFailureHandler is not null) Dispatcher.CurrentDispatcher.UnhandledException -= dispatcherFailureHandler; } }) { IsBackground = true, Name = "Background decoded recent icon display test" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); Ensure(thread.Join(TimeSpan.FromSeconds(15)), "后台解码的最近使用图标显示测试超时。"); if (failure is not null) throw new InvalidOperationException($"后台解码的最近使用图标无法显示:{failure.Message}", failure); } [Test] public static void LargeSvgLoadingStaysOffTheUiThreadAndSharesItsCachedResult() { var dataUri = "data:image/svg+xml;base64," + Convert.ToBase64String(Encoding.UTF8.GetBytes(CreateComplexSvg(6000))); Exception? failure = null; var thread = new Thread(() => { try { var dispatcher = Dispatcher.CurrentDispatcher; var invocationTimer = Stopwatch.StartNew(); var firstLoad = WebImageSourceLoader.LoadAsync(dataUri); var concurrentLoad = WebImageSourceLoader.LoadAsync(dataUri); invocationTimer.Stop(); // 如果解析仍在调用线程同步执行,这里会在大型 SVG 解析完之后才有机会投递 UI 消息。 Ensure(invocationTimer.Elapsed < TimeSpan.FromMilliseconds(150), $"大型 SVG 的 LoadAsync 调用阻塞了 UI {invocationTimer.Elapsed.TotalMilliseconds:F1} ms。"); var uiPulseReceived = false; dispatcher.BeginInvoke(DispatcherPriority.Send, new Action(() => uiPulseReceived = true)); var allLoads = Task.WhenAll(firstLoad, concurrentLoad); var frame = new DispatcherFrame(); _ = Task.Run(async () => { await Task.WhenAny(allLoads, Task.Delay(TimeSpan.FromSeconds(15))); await dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => frame.Continue = false)); }); Dispatcher.PushFrame(frame); Ensure(uiPulseReceived, "大型 SVG 解析期间 WPF Dispatcher 未能处理界面消息。"); Ensure(allLoads.IsCompletedSuccessfully, "大型 SVG 没有在 15 秒内完成后台解析。\n" + allLoads.Exception?.GetBaseException().Message); var images = allLoads.GetAwaiter().GetResult(); Ensure(images[0] is DrawingImage { IsFrozen: true }, "后台解析结果不是可跨线程使用的冻结 DrawingImage。"); Ensure(ReferenceEquals(images[0], images[1]), "相同 SVG 的并发请求没有复用同一个解析任务和缓存结果。"); var cacheTimer = Stopwatch.StartNew(); var cachedImage = WebImageSourceLoader.LoadAsync(dataUri).GetAwaiter().GetResult(); cacheTimer.Stop(); Ensure(ReferenceEquals(images[0], cachedImage), "大型 SVG 的后续请求没有命中缓存。"); Ensure(cacheTimer.Elapsed < TimeSpan.FromMilliseconds(250), $"大型 SVG 缓存读取耗时异常:{cacheTimer.Elapsed.TotalMilliseconds:F1} ms。"); } catch (Exception exception) { failure = exception; } }) { IsBackground = true, Name = "WebImageSourceLoader SVG responsiveness test" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); Ensure(thread.Join(TimeSpan.FromSeconds(20)), "大型 SVG UI 响应性测试超时。"); if (failure is not null) throw new InvalidOperationException($"大型 SVG 后台解析与缓存测试失败:{failure.Message}", failure); } private static string CreateComplexSvg(int elementCount) { var svg = new StringBuilder(elementCount * 64); svg.Append(""); for (var index = 0; index < elementCount; index++) { var x = index % 512; var y = index * 17 % 512; svg.Append(""); } return svg.Append("").ToString(); } private static T AwaitWithDispatcher(Task task, TimeSpan timeout) { if (!task.IsCompleted) { var dispatcher = Dispatcher.CurrentDispatcher; var frame = new DispatcherFrame(); _ = Task.Run(async () => { await Task.WhenAny(task, Task.Delay(timeout)); await dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => frame.Continue = false)); }); Dispatcher.PushFrame(frame); } Ensure(task.IsCompleted, $"等待后台图像解析超过 {timeout.TotalSeconds:F0} 秒。"); return task.GetAwaiter().GetResult(); } private static byte[] CreateAnimatedGif() { var encoder = new GifBitmapEncoder(); encoder.Frames.Add(CreateSolidFrame(0xE7, 0x98, 0x98)); encoder.Frames.Add(CreateSolidFrame(0x98, 0xE7, 0xB0)); using var output = new MemoryStream(); encoder.Save(output); return output.ToArray(); } private static byte[] CreatePng() { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(CreateSolidFrame(0x98, 0x98, 0xE7)); using var output = new MemoryStream(); encoder.Save(output); return output.ToArray(); } private static void PumpDispatcher(Dispatcher dispatcher) { var frame = new DispatcherFrame(); _ = dispatcher.BeginInvoke( DispatcherPriority.ApplicationIdle, new Action(() => frame.Continue = false)); Dispatcher.PushFrame(frame); } private static BitmapFrame CreateSolidFrame(byte red, byte green, byte blue) { const int size = 4; var pixels = new byte[size * size * 4]; for (var index = 0; index < pixels.Length; index += 4) { pixels[index] = blue; pixels[index + 1] = green; pixels[index + 2] = red; pixels[index + 3] = 0xFF; } var bitmap = BitmapSource.Create( size, size, 96, 96, PixelFormats.Bgra32, null, pixels, size * 4); return BitmapFrame.Create(bitmap); } private static void EnsureThrows(Action action) where TException : Exception { try { action(); } catch (TException) { return; } throw new InvalidOperationException($"预期抛出 {typeof(TException).Name},但操作成功了。"); } private static void Ensure(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } }