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.
This commit is contained in:
ema
2025-12-26 01:05:52 +08:00
parent 19805f06c5
commit 36d2d44200
3 changed files with 242 additions and 3 deletions

View File

@@ -0,0 +1,73 @@
<UserControl x:Class="QuickLook.Plugin.ArchiveViewer.CompoundFileBinary.CompoundInfoPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:archive="clr-namespace:QuickLook.Plugin.ArchiveViewer.ArchiveFile"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:QuickLook.Plugin.ArchiveViewer.CompoundFileBinary"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="infoPanel"
d:DesignHeight="600"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- only for design -->
<ResourceDictionary Source="/QuickLook.Common;component/Styles/MainWindowStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<archive:Percent100ToVisibilityVisibleConverter x:Key="Percent100ToVisibilityVisibleConverter" />
<archive:Percent100ToVisibilityCollapsedConverter x:Key="Percent100ToVisibilityCollapsedConverter" />
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid Visibility="{Binding ElementName=infoPanel, Path=LoadPercent, Mode=OneWay, Converter={StaticResource Percent100ToVisibilityCollapsedConverter}}" ZIndex="9999">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Label x:Name="lblLoading"
HorizontalAlignment="Center"
FontSize="14"
Foreground="{DynamicResource WindowTextForeground}">
Loading archive ...
</Label>
<ProgressBar Width="150"
Height="13"
Value="{Binding ElementName=infoPanel, Path=LoadPercent, Mode=OneWay}" />
</StackPanel>
</Grid>
<Grid Visibility="{Binding ElementName=infoPanel, Path=LoadPercent, Mode=OneWay, Converter={StaticResource Percent100ToVisibilityVisibleConverter}}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<archive:ArchiveFileListView x:Name="fileListView"
Grid.Row="0"
Focusable="False"
Foreground="{DynamicResource WindowTextForeground}" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*" />
<ColumnDefinition Width="30*" />
<ColumnDefinition Width="30*" />
</Grid.ColumnDefinitions>
<Label x:Name="archiveCount"
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource WindowTextForegroundAlternative}">
0 folders and 0 files
</Label>
<Label x:Name="archiveSizeC"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource WindowTextForegroundAlternative}" />
<Label x:Name="archiveSizeU"
Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource WindowTextForegroundAlternative}">
Uncompressed size 0 bytes
</Label>
</Grid>
</Grid>
</Grid>
</UserControl>

View File

@@ -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<string, ArchiveFileEntry> _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));
}
}

View File

@@ -17,6 +17,7 @@
using QuickLook.Common.Plugin; using QuickLook.Common.Plugin;
using QuickLook.Plugin.ArchiveViewer.ArchiveFile; using QuickLook.Plugin.ArchiveViewer.ArchiveFile;
using QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -50,8 +51,8 @@ public class Plugin : IViewer
".zip", // ZIP compressed archive (most common compression format) ".zip", // ZIP compressed archive (most common compression format)
// List of supported compound file binary file extensions // List of supported compound file binary file extensions
//".cfb", // Compound File Binary format (used by older Microsoft Office files) ".cfb", // Compound File Binary format (used by older Microsoft Office files)
//".eif", // QQ emoji file (Compound File Binary format) ".eif", // QQ emoji file (Compound File Binary format)
]; ];
private IDisposable _panel; private IDisposable _panel;
@@ -74,7 +75,15 @@ public class Plugin : IViewer
public void View(string path, ContextObject context) 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.ViewerContent = _panel;
context.Title = $"{Path.GetFileName(path)}"; context.Title = $"{Path.GetFileName(path)}";