From 36d2d44200cab904b4d2f76b74e22f3ad4bb08c9 Mon Sep 17 00:00:00 2001 From: ema Date: Fri, 26 Dec 2025 01:05:52 +0800 Subject: [PATCH] Add Compound File Binary (CFB) archive support Introduces CompoundInfoPanel for viewing Compound File Binary archives (.cfb, .eif) in the ArchiveViewer plugin. Updates Plugin.cs to detect and use the new panel for these file types, enabling preview and information display for CFB-based archives. --- .../CompoundFileBinary/CompoundInfoPanel.xaml | 73 ++++++++ .../CompoundInfoPanel.xaml.cs | 157 ++++++++++++++++++ .../QuickLook.Plugin.ArchiveViewer/Plugin.cs | 15 +- 3 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml create mode 100644 QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml new file mode 100644 index 0000000..5e4c2ab --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml.cs new file mode 100644 index 0000000..0dbe18d --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundInfoPanel.xaml.cs @@ -0,0 +1,157 @@ +using QuickLook.Common.ExtensionMethods; +using QuickLook.Common.Helpers; +using QuickLook.Plugin.ArchiveViewer.ArchiveFile; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary; + +public partial class CompoundInfoPanel : UserControl, IDisposable, INotifyPropertyChanged +{ + private readonly Dictionary _fileEntries = []; + private bool _disposed; + private double _loadPercent; + private ulong _totalSize; + + public CompoundInfoPanel(string path) + { + InitializeComponent(); + + // design-time only + Resources.MergedDictionaries.Clear(); + + BeginLoadArchive(path); + } + + public double LoadPercent + { + get => _loadPercent; + private set + { + if (value == _loadPercent) return; + _loadPercent = value; + OnPropertyChanged(); + } + } + + public void Dispose() + { + GC.SuppressFinalize(this); + + _disposed = true; + + fileListView.Dispose(); + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void BeginLoadArchive(string path) + { + new Task(() => + { + _totalSize = (ulong)new FileInfo(path).Length; + + var root = new ArchiveFileEntry(Path.GetFileName(path), true); + _fileEntries.Add(string.Empty, root); + + try + { + LoadItemsFromArchive(path); + } + catch (Exception e) + { + ProcessHelper.WriteLog(e.ToString()); + Dispatcher.Invoke(() => { lblLoading.Content = "Preview failed. See log for more details."; }); + return; + } + + var folders = -1; // do not count root node + var files = 0; + ulong sizeU = 0L; + + foreach (var item in _fileEntries) + { + if (item.Value.IsFolder) + folders++; + else + files++; + + sizeU += item.Value.Size; + } + + string t; + var d = folders != 0 ? $"{folders} folders" : string.Empty; + var f = files != 0 ? $"{files} files" : string.Empty; + if (!string.IsNullOrEmpty(d) && !string.IsNullOrEmpty(f)) + t = $", {d} and {f}"; + else if (string.IsNullOrEmpty(d) && string.IsNullOrEmpty(f)) + t = string.Empty; + else + t = $", {d}{f}"; + + Dispatcher.Invoke(() => + { + if (_disposed) + return; + + fileListView.SetDataContext(_fileEntries[string.Empty].Children.Keys); + archiveCount.Content = $"Compound File{t}"; + archiveSizeC.Content = string.Empty; + archiveSizeU.Content = $"Total stream size {((long)sizeU).ToPrettySize(2)}"; + }); + + LoadPercent = 100d; + }).Start(); + } + + private void LoadItemsFromArchive(string path) + { + using var storage = new DisposableIStorage(path, STGM.READ | STGM.SHARE_DENY_WRITE, IntPtr.Zero); + ProcessStorage(storage, string.Empty); + } + + private void ProcessStorage(DisposableIStorage storage, string currentPath) + { + var enumerator = storage.EnumElements(); + while (enumerator.MoveNext()) + { + if (_disposed) return; + + var stat = enumerator.Current; + var name = stat.pwcsName; + var fullPath = string.IsNullOrEmpty(currentPath) ? name : currentPath + "\\" + name; + + _fileEntries.TryGetValue(currentPath, out var parent); + + if (stat.type == (int)STGTY.STGTY_STORAGE) + { + var entry = new ArchiveFileEntry(name, true, parent); + _fileEntries.Add(fullPath, entry); + + using var subStorage = storage.OpenStorage(name, null, STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero); + ProcessStorage(subStorage, fullPath); + } + else if (stat.type == (int)STGTY.STGTY_STREAM) + { + long fileTime = ((long)stat.mtime.dwHighDateTime << 32) | (uint)stat.mtime.dwLowDateTime; + var entry = new ArchiveFileEntry(name, false, parent) + { + Size = (ulong)stat.cbSize, + ModifiedDate = DateTime.FromFileTimeUtc(fileTime).ToLocalTime() + }; + _fileEntries.Add(fullPath, entry); + } + } + } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs index ba709a4..433ef15 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs @@ -17,6 +17,7 @@ using QuickLook.Common.Plugin; using QuickLook.Plugin.ArchiveViewer.ArchiveFile; +using QuickLook.Plugin.ArchiveViewer.CompoundFileBinary; using System; using System.IO; using System.Linq; @@ -50,8 +51,8 @@ public class Plugin : IViewer ".zip", // ZIP compressed archive (most common compression format) // List of supported compound file binary file extensions - //".cfb", // Compound File Binary format (used by older Microsoft Office files) - //".eif", // QQ emoji file (Compound File Binary format) + ".cfb", // Compound File Binary format (used by older Microsoft Office files) + ".eif", // QQ emoji file (Compound File Binary format) ]; private IDisposable _panel; @@ -74,7 +75,15 @@ public class Plugin : IViewer public void View(string path, ContextObject context) { - _panel = new ArchiveInfoPanel(path); + if (path.EndsWith(".cfb", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".eif", StringComparison.OrdinalIgnoreCase)) + { + _panel = new CompoundInfoPanel(path); + } + else + { + _panel = new ArchiveInfoPanel(path); + } context.ViewerContent = _panel; context.Title = $"{Path.GetFileName(path)}";