mirror of
https://github.com/QL-Win/QuickLook.git
synced 2026-01-13 07:05:24 +08:00
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:
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)}";
|
||||
|
||||
Reference in New Issue
Block a user