From 922c2e91a62ed1e8005686eacfcec5a7490e9be9 Mon Sep 17 00:00:00 2001 From: Twometer Date: Sun, 8 Aug 2021 11:26:42 +0200 Subject: [PATCH] fix #11 - Add thumbnail generator to images --- NoFences/FenceWindow.cs | 16 ++++-- NoFences/Model/FenceEntry.cs | 15 +++++- NoFences/NoFences.csproj | 1 + NoFences/Util/ThumbnailProvider.cs | 78 ++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 NoFences/Util/ThumbnailProvider.cs diff --git a/NoFences/FenceWindow.cs b/NoFences/FenceWindow.cs index bb72afc..b948fcb 100644 --- a/NoFences/FenceWindow.cs +++ b/NoFences/FenceWindow.cs @@ -3,10 +3,8 @@ using NoFences.Win32; using Peter; using System; -using System.Diagnostics; using System.Drawing; using System.IO; -using System.Threading.Tasks; using System.Windows.Forms; using static NoFences.Win32.WindowUtil; @@ -44,6 +42,8 @@ public partial class FenceWindow : Form private readonly ShellContextMenu shellContextMenu = new ShellContextMenu(); + private readonly ThumbnailProvider thumbnailProvider = new ThumbnailProvider(); + private void ReloadFonts() { var family = new FontFamily("Segoe UI"); @@ -61,6 +61,7 @@ public FenceWindow(FenceInfo fenceInfo) //DesktopUtil.PreventMinimize(Handle); this.titleHeight = fenceInfo.TitleHeight; this.MouseWheel += FenceWindow_MouseWheel; + thumbnailProvider.IconThumbnailLoaded += ThumbnailProvider_IconThumbnailLoaded; if (titleHeight < 16 || titleHeight > 100) titleHeight = 35; @@ -305,8 +306,8 @@ private void FenceWindow_Paint(object sender, PaintEventArgs e) scrollOffset = Math.Min(scrollOffset, scrollHeight); } - - + + // Click handlers if (shouldUpdateSelection && !hasSelectionUpdated) @@ -323,7 +324,7 @@ private void FenceWindow_Paint(object sender, PaintEventArgs e) private void RenderEntry(Graphics g, FenceEntry entry, int x, int y) { - var icon = entry.ExtractIcon(); + var icon = entry.ExtractIcon(thumbnailProvider); var name = entry.Name; var textPosition = new PointF(x, y + icon.Height + 5); @@ -484,6 +485,11 @@ private void FenceWindow_MouseWheel(object sender, MouseEventArgs e) Invalidate(); } + private void ThumbnailProvider_IconThumbnailLoaded(object sender, EventArgs e) + { + Invalidate(); + } + private bool ItemExists(string path) { return File.Exists(path) || Directory.Exists(path); diff --git a/NoFences/Model/FenceEntry.cs b/NoFences/Model/FenceEntry.cs index 51aff49..c842719 100644 --- a/NoFences/Model/FenceEntry.cs +++ b/NoFences/Model/FenceEntry.cs @@ -4,6 +4,7 @@ using System; using System.IO; using NoFences.Win32; +using NoFences.Util; namespace NoFences.Model { @@ -30,9 +31,19 @@ public static FenceEntry FromPath(string path) else return null; } - public Icon ExtractIcon() + public Icon ExtractIcon(ThumbnailProvider thumbnailProvider) { - return Type == EntryType.File ? Icon.ExtractAssociatedIcon(Path) : IconUtil.FolderLarge; + if (Type == EntryType.File) + { + if (thumbnailProvider.IsSupported(Path)) + return thumbnailProvider.GenerateThumbnail(Path); + else + return Icon.ExtractAssociatedIcon(Path); + } + else + { + return IconUtil.FolderLarge; + } } public void Open() diff --git a/NoFences/NoFences.csproj b/NoFences/NoFences.csproj index 12fe325..114f1b9 100644 --- a/NoFences/NoFences.csproj +++ b/NoFences/NoFences.csproj @@ -72,6 +72,7 @@ + diff --git a/NoFences/Util/ThumbnailProvider.cs b/NoFences/Util/ThumbnailProvider.cs new file mode 100644 index 0000000..dafd4f0 --- /dev/null +++ b/NoFences/Util/ThumbnailProvider.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NoFences.Util +{ + public class ThumbnailProvider + { + // Supported .NET images as per https://docs.microsoft.com/en-us/dotnet/api/system.drawing.image.fromfile + private static readonly string[] SupportedExtensions = + { + ".bmp", + ".gif", + ".jpg", + ".jpeg", + ".png", + ".tiff", + ".tif" + }; + + private class ThumbnailState + { + public Icon icon; + } + + // Only allow 4 concurrent images to be decoded to try and prevent OOM errors + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(4); + private readonly IDictionary iconCache = new Dictionary(); + public event EventHandler IconThumbnailLoaded; + + public bool IsSupported(string path) + { + return SupportedExtensions.Any(ext => path.EndsWith(ext)); + } + + public Icon GenerateThumbnail(string path) + { + if (!iconCache.ContainsKey(path)) + { + return SubmitGeneratorTask(path).icon; + } + else + { + return iconCache[path].icon; + } + } + + private ThumbnailState SubmitGeneratorTask(string path) + { + var state = new ThumbnailState() { icon = Icon.ExtractAssociatedIcon(path) }; + iconCache[path] = state; + + Task.Run(() => + { + semaphore.Wait(); + using (MemoryStream ms = new MemoryStream(File.ReadAllBytes(path))) + { + using (var img = Image.FromStream(ms)) + { + var thumb = (Bitmap)img.GetThumbnailImage(32, 32, () => false, IntPtr.Zero); + var icon = Icon.FromHandle(thumb.GetHicon()); + state.icon = icon; + IconThumbnailLoaded(this, new EventArgs()); + semaphore.Release(); + return icon; + } + } + }); + return state; + } + + } +}