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