using System.Collections.Concurrent; using System.IO; using System.Net; using System.Net.Http; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using PCL.Core.Utils; using PCL.Core.IO.Net.Http; namespace PCL; public class MyImage : Image { public MyImage() { Initialized += (_, _) => Load(); SizeChanged += (_, _) => UpdateClip(); } /// /// 实际被呈现的图片地址。 /// public string ActualSource { get => field; set { if (string.IsNullOrEmpty(value)) value = null; if ((field ?? "") == (value ?? "")) return; field = value; Dispatcher.BeginInvoke(new Func(async () => { try { ImageSource bitmap = value is null ? null : await Task.Run(() => new MyBitmap(value)); base.Source = bitmap; } catch (Exception ex) { ModBase.Log(ex, $"加载图片失败({value})"); try { if (value.StartsWithF(ModBase.pathTemp) && File.Exists(value)) File.Delete(value); } catch { } } })); // 在这里先触发可能的文件读取,尽量避免在 UI 线程中读取文件 // ignored } } private void Load() // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868) { if (Source is null) { ActualSource = null; return; } if (!Source.StartsWithF("http")) { ActualSource = Source; return; } var url = Source; var tempPath = GetTempPath(url); var tempFile = new FileInfo(tempPath); var enableCache = this.EnableCache; if (enableCache && tempFile.Exists) { ActualSource = tempPath; if (DateTime.Now - tempFile.LastWriteTime < fileCacheExpiredTime) return; // 无需刷新缓存 } Dispatcher.BeginInvoke(new Func(async () => { try { // 下载 ActualSource = LoadingSource; var resp = await DownloadImageAsync(url); if (!string.IsNullOrEmpty(resp)) { ActualSource = resp; return; } resp = await DownloadImageAsync(FallbackSource); if (!string.IsNullOrEmpty(resp)) { ActualSource = resp; return; } } catch (Exception ex) { // 更换备用地址 ModBase.Log(ex, $"Online image get fail(source = {url}, fallback = {FallbackSource})", ModBase.LogLevel.Developer); tempPath = GetTempPath(url); tempFile = new FileInfo(tempPath); if (enableCache && tempFile.Exists) { ActualSource = tempPath; if (DateTime.Now - tempFile.LastWriteTime < fileCacheExpiredTime) return; } } })); } public static Task DownloadImageAsync(string url) { return _downloadTasks.GetOrAdd(url, key => { var t = DownloadImageInternalAsync(key); t.ContinueWith(_ => _downloadTasks.TryRemove(url, out _)); return t; }); } public static string GetTempPath(string url) { return Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{ModBase.GetStringMD5(url)}.png"); } private static readonly ConcurrentDictionary> _downloadTasks = new(); private static async Task DownloadImageInternalAsync(string url) { var tempPath = GetTempPath(url); var tempDownloadingPath = tempPath + RandomUtils.NextInt(0, 1000000); try { Directory.CreateDirectory(ModBase.GetPathFromFullPath(tempPath)); // 重新实现下载,以避免携带 Header(#5072) using (var fs = new FileStream(tempDownloadingPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { using (var response = await HttpRequest.Create(url) .SendAsync(addMetedata: false)) { response.EnsureSuccessStatusCode(); using (var nfs = await response.AsStreamAsync()) { fs.SetLength(0L); await nfs.CopyToAsync(fs); } } } File.Move(tempDownloadingPath, tempPath, true); return tempPath; } catch (Exception ex) { if (File.Exists(tempPath)) File.Delete(tempPath); if (File.Exists(tempDownloadingPath)) File.Delete(tempDownloadingPath); ModBase.Log(ex, $"Try to get online image fail (url = {url}, dest = {tempPath})"); return string.Empty; } } #region 公开属性 public TimeSpan fileCacheExpiredTime = TimeSpan.FromDays(14d); public bool EnableCache { get => (bool)GetValue(EnableCacheProperty); set => SetValue(EnableCacheProperty, value); } public new static readonly DependencyProperty EnableCacheProperty = DependencyProperty.Register("EnableCache", typeof(bool), typeof(MyImage), new PropertyMetadata(true)); /// /// 与 Image 的 Source 类似。 /// 若输入以 http 开头的字符串,则会尝试下载图片然后显示,图片会保存为本地缓存。 /// 支持 WebP 格式的图片。 /// public new string Source // 覆写 Image 的 Source 属性 { get => field; set { if (string.IsNullOrEmpty(value)) value = null; if ((field ?? "") == (value ?? "")) return; field = value; if (!IsInitialized) return; // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868) Load(); } } = ""; public new static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(string), typeof(MyImage), new PropertyMetadata((sender, e) => { if (sender is not null) ((MyImage)sender).Source = e.NewValue.ToString(); })); /// /// 当 Source 首次下载失败时,会从该备用地址加载图片。 /// public string FallbackSource { get; set; } /// /// 正在下载网络图片时显示的本地图片。 /// public string LoadingSource { get; set; } = "pack://application:,,,/images/Icons/NoIcon.png"; public CornerRadius CornerRadius { get => (CornerRadius)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } private static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register( "CornerRadius", typeof(CornerRadius), typeof(MyImage), new FrameworkPropertyMetadata( new CornerRadius(-1), OnCornerRadiusChanged) ); private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((MyImage)d).UpdateClip(); } private void UpdateClip() // Handles Me.SizeChanged will be added separately { if (ActualWidth > 0 && ActualHeight > 0 && CornerRadius.TopLeft >= 0 && CornerRadius.TopRight >= 0) { Clip = new RectangleGeometry( new Rect(0, 0, ActualWidth, ActualHeight), CornerRadius.TopLeft, CornerRadius.TopRight); } } #endregion }