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)}";