Skip to content

Commit 36d2d44

Browse files
committed
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.
1 parent 19805f0 commit 36d2d44

File tree

3 files changed

+242
-3
lines changed

3 files changed

+242
-3
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<UserControl x:Class="QuickLook.Plugin.ArchiveViewer.CompoundFileBinary.CompoundInfoPanel"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:archive="clr-namespace:QuickLook.Plugin.ArchiveViewer.ArchiveFile"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:local="clr-namespace:QuickLook.Plugin.ArchiveViewer.CompoundFileBinary"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
x:Name="infoPanel"
9+
d:DesignHeight="600"
10+
d:DesignWidth="800"
11+
mc:Ignorable="d">
12+
<UserControl.Resources>
13+
<ResourceDictionary>
14+
<ResourceDictionary.MergedDictionaries>
15+
<!-- only for design -->
16+
<ResourceDictionary Source="/QuickLook.Common;component/Styles/MainWindowStyles.xaml" />
17+
</ResourceDictionary.MergedDictionaries>
18+
<archive:Percent100ToVisibilityVisibleConverter x:Key="Percent100ToVisibilityVisibleConverter" />
19+
<archive:Percent100ToVisibilityCollapsedConverter x:Key="Percent100ToVisibilityCollapsedConverter" />
20+
</ResourceDictionary>
21+
</UserControl.Resources>
22+
<Grid>
23+
<Grid Visibility="{Binding ElementName=infoPanel, Path=LoadPercent, Mode=OneWay, Converter={StaticResource Percent100ToVisibilityCollapsedConverter}}" ZIndex="9999">
24+
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
25+
<Label x:Name="lblLoading"
26+
HorizontalAlignment="Center"
27+
FontSize="14"
28+
Foreground="{DynamicResource WindowTextForeground}">
29+
Loading archive ...
30+
</Label>
31+
<ProgressBar Width="150"
32+
Height="13"
33+
Value="{Binding ElementName=infoPanel, Path=LoadPercent, Mode=OneWay}" />
34+
</StackPanel>
35+
</Grid>
36+
<Grid Visibility="{Binding ElementName=infoPanel, Path=LoadPercent, Mode=OneWay, Converter={StaticResource Percent100ToVisibilityVisibleConverter}}">
37+
<Grid.RowDefinitions>
38+
<RowDefinition />
39+
<RowDefinition Height="30" />
40+
</Grid.RowDefinitions>
41+
<archive:ArchiveFileListView x:Name="fileListView"
42+
Grid.Row="0"
43+
Focusable="False"
44+
Foreground="{DynamicResource WindowTextForeground}" />
45+
<Grid Grid.Row="1">
46+
<Grid.ColumnDefinitions>
47+
<ColumnDefinition Width="40*" />
48+
<ColumnDefinition Width="30*" />
49+
<ColumnDefinition Width="30*" />
50+
</Grid.ColumnDefinitions>
51+
<Label x:Name="archiveCount"
52+
Grid.Column="0"
53+
HorizontalAlignment="Center"
54+
VerticalAlignment="Center"
55+
Foreground="{DynamicResource WindowTextForegroundAlternative}">
56+
0 folders and 0 files
57+
</Label>
58+
<Label x:Name="archiveSizeC"
59+
Grid.Column="1"
60+
HorizontalAlignment="Center"
61+
VerticalAlignment="Center"
62+
Foreground="{DynamicResource WindowTextForegroundAlternative}" />
63+
<Label x:Name="archiveSizeU"
64+
Grid.Column="2"
65+
HorizontalAlignment="Center"
66+
VerticalAlignment="Center"
67+
Foreground="{DynamicResource WindowTextForegroundAlternative}">
68+
Uncompressed size 0 bytes
69+
</Label>
70+
</Grid>
71+
</Grid>
72+
</Grid>
73+
</UserControl>
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using QuickLook.Common.ExtensionMethods;
2+
using QuickLook.Common.Helpers;
3+
using QuickLook.Plugin.ArchiveViewer.ArchiveFile;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel;
7+
using System.IO;
8+
using System.Runtime.CompilerServices;
9+
using System.Runtime.InteropServices.ComTypes;
10+
using System.Threading.Tasks;
11+
using System.Windows.Controls;
12+
13+
namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
14+
15+
public partial class CompoundInfoPanel : UserControl, IDisposable, INotifyPropertyChanged
16+
{
17+
private readonly Dictionary<string, ArchiveFileEntry> _fileEntries = [];
18+
private bool _disposed;
19+
private double _loadPercent;
20+
private ulong _totalSize;
21+
22+
public CompoundInfoPanel(string path)
23+
{
24+
InitializeComponent();
25+
26+
// design-time only
27+
Resources.MergedDictionaries.Clear();
28+
29+
BeginLoadArchive(path);
30+
}
31+
32+
public double LoadPercent
33+
{
34+
get => _loadPercent;
35+
private set
36+
{
37+
if (value == _loadPercent) return;
38+
_loadPercent = value;
39+
OnPropertyChanged();
40+
}
41+
}
42+
43+
public void Dispose()
44+
{
45+
GC.SuppressFinalize(this);
46+
47+
_disposed = true;
48+
49+
fileListView.Dispose();
50+
}
51+
52+
public event PropertyChangedEventHandler PropertyChanged;
53+
54+
private void BeginLoadArchive(string path)
55+
{
56+
new Task(() =>
57+
{
58+
_totalSize = (ulong)new FileInfo(path).Length;
59+
60+
var root = new ArchiveFileEntry(Path.GetFileName(path), true);
61+
_fileEntries.Add(string.Empty, root);
62+
63+
try
64+
{
65+
LoadItemsFromArchive(path);
66+
}
67+
catch (Exception e)
68+
{
69+
ProcessHelper.WriteLog(e.ToString());
70+
Dispatcher.Invoke(() => { lblLoading.Content = "Preview failed. See log for more details."; });
71+
return;
72+
}
73+
74+
var folders = -1; // do not count root node
75+
var files = 0;
76+
ulong sizeU = 0L;
77+
78+
foreach (var item in _fileEntries)
79+
{
80+
if (item.Value.IsFolder)
81+
folders++;
82+
else
83+
files++;
84+
85+
sizeU += item.Value.Size;
86+
}
87+
88+
string t;
89+
var d = folders != 0 ? $"{folders} folders" : string.Empty;
90+
var f = files != 0 ? $"{files} files" : string.Empty;
91+
if (!string.IsNullOrEmpty(d) && !string.IsNullOrEmpty(f))
92+
t = $", {d} and {f}";
93+
else if (string.IsNullOrEmpty(d) && string.IsNullOrEmpty(f))
94+
t = string.Empty;
95+
else
96+
t = $", {d}{f}";
97+
98+
Dispatcher.Invoke(() =>
99+
{
100+
if (_disposed)
101+
return;
102+
103+
fileListView.SetDataContext(_fileEntries[string.Empty].Children.Keys);
104+
archiveCount.Content = $"Compound File{t}";
105+
archiveSizeC.Content = string.Empty;
106+
archiveSizeU.Content = $"Total stream size {((long)sizeU).ToPrettySize(2)}";
107+
});
108+
109+
LoadPercent = 100d;
110+
}).Start();
111+
}
112+
113+
private void LoadItemsFromArchive(string path)
114+
{
115+
using var storage = new DisposableIStorage(path, STGM.READ | STGM.SHARE_DENY_WRITE, IntPtr.Zero);
116+
ProcessStorage(storage, string.Empty);
117+
}
118+
119+
private void ProcessStorage(DisposableIStorage storage, string currentPath)
120+
{
121+
var enumerator = storage.EnumElements();
122+
while (enumerator.MoveNext())
123+
{
124+
if (_disposed) return;
125+
126+
var stat = enumerator.Current;
127+
var name = stat.pwcsName;
128+
var fullPath = string.IsNullOrEmpty(currentPath) ? name : currentPath + "\\" + name;
129+
130+
_fileEntries.TryGetValue(currentPath, out var parent);
131+
132+
if (stat.type == (int)STGTY.STGTY_STORAGE)
133+
{
134+
var entry = new ArchiveFileEntry(name, true, parent);
135+
_fileEntries.Add(fullPath, entry);
136+
137+
using var subStorage = storage.OpenStorage(name, null, STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero);
138+
ProcessStorage(subStorage, fullPath);
139+
}
140+
else if (stat.type == (int)STGTY.STGTY_STREAM)
141+
{
142+
long fileTime = ((long)stat.mtime.dwHighDateTime << 32) | (uint)stat.mtime.dwLowDateTime;
143+
var entry = new ArchiveFileEntry(name, false, parent)
144+
{
145+
Size = (ulong)stat.cbSize,
146+
ModifiedDate = DateTime.FromFileTimeUtc(fileTime).ToLocalTime()
147+
};
148+
_fileEntries.Add(fullPath, entry);
149+
}
150+
}
151+
}
152+
153+
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
154+
{
155+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
156+
}
157+
}

QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
using QuickLook.Common.Plugin;
1919
using QuickLook.Plugin.ArchiveViewer.ArchiveFile;
20+
using QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
2021
using System;
2122
using System.IO;
2223
using System.Linq;
@@ -50,8 +51,8 @@ public class Plugin : IViewer
5051
".zip", // ZIP compressed archive (most common compression format)
5152

5253
// List of supported compound file binary file extensions
53-
//".cfb", // Compound File Binary format (used by older Microsoft Office files)
54-
//".eif", // QQ emoji file (Compound File Binary format)
54+
".cfb", // Compound File Binary format (used by older Microsoft Office files)
55+
".eif", // QQ emoji file (Compound File Binary format)
5556
];
5657

5758
private IDisposable _panel;
@@ -74,7 +75,15 @@ public void Prepare(string path, ContextObject context)
7475

7576
public void View(string path, ContextObject context)
7677
{
77-
_panel = new ArchiveInfoPanel(path);
78+
if (path.EndsWith(".cfb", StringComparison.OrdinalIgnoreCase)
79+
|| path.EndsWith(".eif", StringComparison.OrdinalIgnoreCase))
80+
{
81+
_panel = new CompoundInfoPanel(path);
82+
}
83+
else
84+
{
85+
_panel = new ArchiveInfoPanel(path);
86+
}
7887

7988
context.ViewerContent = _panel;
8089
context.Title = $"{Path.GetFileName(path)}";

0 commit comments

Comments
 (0)