diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs
index 2d7967b..e99c77e 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs
@@ -27,7 +27,7 @@ namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
/// Utility class to extract streams and storages from a COM compound file (IStorage) into the file system.
/// This is a thin managed wrapper that enumerates entries inside the compound file and writes streams to disk.
///
-public static class CompoundFileExtractor
+public static partial class CompoundFileExtractor
{
///
/// Extracts all streams and storages from the compound file at
@@ -142,3 +142,77 @@ public static class CompoundFileExtractor
}
}
}
+
+///
+/// Utility class to extract streams and storages from a COM compound file (IStorage) into the memory.
+/// This is a thin managed wrapper that enumerates entries inside the compound file and writes streams to dictionary.
+///
+public static partial class CompoundFileExtractor
+{
+ ///
+ /// Extracts all streams from the compound file at
+ /// into a dictionary where the key is the relative path and the value is the file content.
+ ///
+ /// Path to the compound file (OLE compound file / structured storage).
+ /// A dictionary containing the extracted files.
+ public static Dictionary ExtractToDictionary(string compoundFilePath)
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ // Ensure the compound file exists
+ if (!File.Exists(compoundFilePath))
+ throw new FileNotFoundException("Compound file not found.", compoundFilePath);
+
+ // Validate magic header for OLE compound file: D0 CF 11 E0 A1 B1 1A E1
+ byte[] magicHeader = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
+ byte[] header = new byte[8];
+ using (FileStream fs = new(compoundFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ int read = fs.Read(header, 0, header.Length);
+ if (read < header.Length || !header.SequenceEqual(magicHeader))
+ {
+ throw new InvalidDataException("The specified file does not appear to be an OLE Compound File (invalid header).");
+ }
+ }
+
+ // Open the compound file as an IStorage implementation wrapped by DisposableIStorage.
+ using DisposableIStorage storage = new(compoundFilePath, STGM.DIRECT | STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero);
+ ExtractStorageToDictionary(storage, string.Empty, result);
+
+ return result;
+ }
+
+ private static void ExtractStorageToDictionary(DisposableIStorage storage, string currentPath, Dictionary result)
+ {
+ IEnumerator enumerator = storage.EnumElements();
+
+ // Enumerate all elements (streams and storages) at the root of the compound file.
+ while (enumerator.MoveNext())
+ {
+ STATSTG entryStat = enumerator.Current;
+ string entryPath = string.IsNullOrEmpty(currentPath) ? entryStat.pwcsName : Path.Combine(currentPath, entryStat.pwcsName);
+
+ // STGTY_STREAM indicates the element is a stream (treat as a file).
+ if (entryStat.type == (int)STGTY.STGTY_STREAM)
+ {
+ // Open the stream for reading from the compound file.
+ using DisposableIStream stream = storage.OpenStream(entryStat.pwcsName, IntPtr.Zero, STGM.READ | STGM.SHARE_EXCLUSIVE);
+
+ // Query stream statistics to determine its size.
+ STATSTG streamStat = stream.Stat((int)STATFLAG.STATFLAG_DEFAULT);
+
+ // Allocate a buffer exactly the size of the stream and read it in one call.
+ byte[] buffer = new byte[streamStat.cbSize];
+ stream.Read(buffer, buffer.Length);
+
+ result[entryPath] = buffer;
+ }
+ // STGTY_STORAGE indicates the element is a nested storage (treat as a directory).
+ else if (entryStat.type == (int)STGTY.STGTY_STORAGE)
+ {
+ using DisposableIStorage subStorage = storage.OpenStorage(entryStat.pwcsName, null, STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero);
+ ExtractStorageToDictionary(subStorage, entryPath, result);
+ }
+ }
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/EifExtractor.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/EifExtractor.cs
new file mode 100644
index 0000000..3b38622
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/EifExtractor.cs
@@ -0,0 +1,95 @@
+// Copyright © 2017-2025 QL-Win Contributors
+//
+// This file is part of QuickLook program.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
+
+///
+/// Utility to extract contents from an EIF archive and optionally reorder images
+/// based on metadata stored in Face.dat. The EIF format is a Compound File Binary
+/// (structured storage) used by QQ for emoji packs.
+///
+public static class EifExtractor
+{
+ ///
+ /// File name of the Face.dat metadata stream inside EIF archives.
+ /// Face.dat contains mapping information used to order and rename images.
+ ///
+ public const string FaceDat = "Face.dat";
+
+ ///
+ /// Extracts files from the compound file at into
+ /// . If Face.dat exists inside the archive,
+ /// images will be renamed and reordered according to the mapping in Face.dat.
+ ///
+ /// Path to the EIF compound file.
+ /// Destination directory to write extracted files.
+ public static void ExtractToDirectory(string path, string outputDirectory)
+ {
+ // Extract all streams from the compound file into an in-memory dictionary
+ Dictionary compoundFile = CompoundFileExtractor.ExtractToDictionary(path);
+
+ // If Face.dat exists, build mapping and reorder images accordingly
+ if (compoundFile.ContainsKey(FaceDat))
+ {
+ // Build group -> (filename -> index) mapping from Face.dat
+ Dictionary> faceDat = FaceDatDecoder.Decode(compoundFile[FaceDat]);
+
+ // Flatten mapping to key '\\' joined: "group\filename" -> index
+ Dictionary faceDatMapper = faceDat.SelectMany(
+ outer => outer.Value,
+ (outer, inner) => new { Key = $@"{outer.Key}\{inner.Key}", inner.Value })
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ // Prepare output dictionary for files that match mapping
+ Dictionary output = [];
+
+ foreach (var kv in faceDatMapper)
+ {
+ if (compoundFile.ContainsKey(kv.Key))
+ {
+ // Create a new key using the index as file name and keep original extension
+ string newKey = Path.Combine(Path.GetDirectoryName(kv.Key),
+ faceDatMapper[kv.Key] + Path.GetExtension(kv.Key));
+
+ output[newKey] = compoundFile[kv.Key];
+ }
+ }
+
+ // Ensure target directory exists
+ Directory.CreateDirectory(outputDirectory);
+
+ // Write each matched file to disk using its new name
+ foreach (var kv in output)
+ {
+ (string relativePath, byte[] data) = (kv.Key, kv.Value);
+ string fullPath = Path.Combine(outputDirectory, relativePath);
+
+ // Ensure parent directory exists
+ string dir = Path.GetDirectoryName(fullPath);
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
+
+ // Write file bytes
+ File.WriteAllBytes(fullPath, data);
+ }
+ }
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs
index 94de1bd..71a99a3 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs
@@ -29,6 +29,7 @@ namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
/// - Locates a repeating-key pattern and extracts the XOR-encrypted block
/// - XOR-decodes the block and parses group\filename entries
/// Provides a method to build the same group -> (filename -> index) mapping as the Python tool.
+/// Reference: https://github.com/readme9txt/QQEIF-Extractor
///
public static class FaceDatDecoder
{
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.MoreMenu.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.MoreMenu.cs
index 788b275..f97c803 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.MoreMenu.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.MoreMenu.cs
@@ -25,6 +25,7 @@ using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
+using System.Windows;
using System.Windows.Input;
using WindowsAPICodePack.Dialogs;
@@ -32,17 +33,30 @@ namespace QuickLook.Plugin.ArchiveViewer;
public partial class Plugin
{
+ ///
+ /// Command to extract archive contents to a directory. Executed asynchronously.
+ ///
public ICommand ExtractToDirectoryCommand { get; }
+ ///
+ /// Constructor - initializes commands used by the plugin.
+ ///
public Plugin()
{
ExtractToDirectoryCommand = new AsyncRelayCommand(ExtractToDirectoryAsync);
}
+ ///
+ /// Return additional "More" menu items for the plugin.
+ /// When the current file is an EIF archive, a menu item to extract to directory is provided.
+ ///
public IEnumerable GetMenuItems()
{
- if (_path.EndsWith(".eif", StringComparison.OrdinalIgnoreCase))
+ // Currently only supports for CFB and EIF files
+ if (_path.EndsWith(".cfb", StringComparison.OrdinalIgnoreCase)
+ || _path.EndsWith(".eif", StringComparison.OrdinalIgnoreCase))
{
+ // Use external Translations.config shipped next to the executing assembly
string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config");
yield return new MoreMenuItem()
@@ -55,6 +69,10 @@ public partial class Plugin
}
}
+ ///
+ /// Show folder picker and extract archive contents to the chosen directory.
+ /// For EIF files, prompt the user whether to apply EIF-specific Face.dat ordering.
+ ///
public async Task ExtractToDirectoryAsync()
{
using CommonOpenFileDialog dialog = new()
@@ -64,25 +82,39 @@ public partial class Plugin
if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
{
- await Task.Run(() =>
+ if (_path.EndsWith(".cfb", StringComparison.OrdinalIgnoreCase))
{
- if (_path.EndsWith(".cfb", StringComparison.OrdinalIgnoreCase))
+ // Generic compound file extraction
+ await Task.Run(() =>
{
CompoundFileExtractor.ExtractToDirectory(_path, dialog.FileName);
- }
- else if (_path.EndsWith(".eif", StringComparison.OrdinalIgnoreCase))
+ });
+ }
+ else if (_path.EndsWith(".eif", StringComparison.OrdinalIgnoreCase))
+ {
+ string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config");
+
+ // Ask the user whether to apply EIF-specific `Face.dat` ordering during extraction
+ MessageBoxResult result = MessageBox.Show(TranslationHelper.Get("MW_ExtractToDirectory_EIFOrderFaceDat",
+ translationFile), "QuickLook", MessageBoxButton.YesNo, MessageBoxImage.Question);
+
+ // If user chooses Yes, use EifExtractor which reorders images according to `Face.dat`
+ if (result == MessageBoxResult.Yes)
{
- CompoundFileExtractor.ExtractToDirectory(_path, dialog.FileName);
-
- string faceDatPath = Path.Combine(dialog.FileName, "face.dat");
-
- if (File.Exists(faceDatPath))
+ await Task.Run(() =>
{
- Dictionary> faceDat = FaceDatDecoder.Decode(File.ReadAllBytes(faceDatPath));
- _ = faceDat;
- }
+ EifExtractor.ExtractToDirectory(_path, dialog.FileName);
+ });
}
- });
+ else
+ {
+ // Fallback: generic compound file extraction
+ await Task.Run(() =>
+ {
+ CompoundFileExtractor.ExtractToDirectory(_path, dialog.FileName);
+ });
+ }
+ }
}
}
}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Translations.config b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Translations.config
index 8019dba..a028a77 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Translations.config
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Translations.config
@@ -3,89 +3,118 @@
استخراج إلى المجلد
+ هل تريد الاحتفاظ بترتيب ملفات الرموز التعبيرية المخزنة؟
Extreu a la carpeta
+ Voleu mantenir l'ordre dels fitxers d'emoticones emmagatzemats?
חלץ לתיקייה
+ האם ברצונך לשמור על סדר קבצי הרגשונים המאוחסנים?
डायरेक्टरी में निकालें
+ क्या आप संग्रहीत इमोटिकॉन फ़ाइलों का क्रम बनाए रखना चाहते हैं?
डायरेक्टरीमध्ये काढा
+ आपण संग्रहित इमोटिकॉन फायलींचा क्रम ठेवू इच्छिता?
Kibontás mappába
+ Meg akarja tartani a tárolt hangulatjel-fájlok sorrendjét?
Pakk ut til mappe
+ Vil du beholde rekkefølgen på lagrede uttrykksikonfiler?
Uitpakken naar map
+ Wilt u de volgorde van opgeslagen emoticonbestanden behouden?
Extrahovať do priečinka
+ Chcete zachovať poradie uložených súborov emotikonov?
Ekstrak ke folder
+ Apakah Anda ingin menjaga urutan file emotikon yang tersimpan?
Extract to directory
+ Do you want to keep the order of stored emoticon files?
폴더로 추출
+ 이모티콘 파일의 저장 순서를 유지하시겠습니까?
Extrair para a pasta
+ Deseja manter a ordem dos arquivos de emoticons armazenados?
Extrair para a pasta
+ Deseja manter a ordem dos ficheiros de emoticons armazenados?
Извлечь в папку
+ Вы хотите сохранить порядок сохраненных файлов смайликов?
Klasöre çıkar
+ Saklanan ifade dosyalarının sırasını korumak istiyor musunuz?
Trích xuất vào thư mục
+ Bạn có muốn giữ thứ tự của các tệp biểu tượng cảm xúc đã lưu không?
Extrair para a pasta
+ Deseja manter a ordem dos ficheiros de emoticons armazenados?
Extraire vers le dossier
+ Voulez-vous conserver l'ordre des fichiers d'émoticônes stockés ?
Extraer a la carpeta
+ ¿Desea mantener el orden de los archivos de emoticonos almacenados?
Estrai nella cartella
+ Vuoi mantenere l'ordine dei file di emoticon memorizzati?
Wyodrębnij do folderu
+ Czy chcesz zachować kolejność przechowywanych plików emotikonów?
Εξαγωγή σε φάκελο
+ Θέλετε να διατηρήσετε τη σειρά των αποθηκευμένων αρχείων emoticon;
Розпакувати до папки
+ Ви хочете зберегти порядок збережених файлів смайлів?
提取到目录
+ 是否需要保持表情文件存储的顺序?
提取到資料夾
+ 是否需要保持表情檔案儲存的順序?
フォルダに抽出
+ 絵文字ファイルの保存順序を保持しますか?
In Ordner extrahieren
+ Möchten Sie die Reihenfolge der gespeicherten Emoticon-Dateien beibehalten?
Extrahera till mapp
+ Vill du behålla ordningen på lagrade uttryckssymbolfiler?