diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs
new file mode 100644
index 0000000..94de1bd
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs
@@ -0,0 +1,239 @@
+// 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;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
+
+///
+/// Decoder for Face.dat entries used by QQ EIF packages.
+/// Re-implements the behavior of the provided Python scripts:
+/// - Finds special marker sequence e_str_file_org inside each line
+/// - Skips 4 bytes after the marker (same as the Python implementation)
+/// - 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.
+///
+public static class FaceDatDecoder
+{
+ ///
+ /// Marker sequence used in the Python script
+ ///
+ private static readonly byte[] EStrFileOrg = [0x98, 0xEB, 0x9F, 0xEB, 0x99, 0xEB, 0xAD, 0xEB, 0x82, 0xEB, 0x87, 0xEB, 0x8E, 0xEB, 0x84, 0xEB, 0x99, 0xEB, 0x8C, 0xEB];
+
+ ///
+ /// Decode returns a mapping of group name to a dictionary mapping file name to index within the group.
+ /// This matches the Python script's group_dict structure.
+ ///
+ /// The raw bytes of Face.dat.
+ /// Nested dictionary: group -> (filename -> index).
+ public static Dictionary> Decode(byte[] fileBytes)
+ {
+ return BuildGroupIndex(fileBytes);
+ }
+
+ ///
+ /// Build group index mapping from Face.dat bytes like the Python script does.
+ ///
+ /// The raw bytes of Face.dat.
+ /// Dictionary where key is group name and value maps filename to index.
+ public static Dictionary> BuildGroupIndex(byte[] fileBytes)
+ {
+ var result = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ if (fileBytes == null || fileBytes.Length == 0)
+ return result;
+
+ // Split into lines by LF, trimming optional CR (same semantics as Python's strip on lines)
+ int lineStart = 0;
+ for (int i = 0; i <= fileBytes.Length; i++)
+ {
+ if (i == fileBytes.Length || fileBytes[i] == (byte)'\n')
+ {
+ int len = i - lineStart;
+ if (len > 0)
+ {
+ // Trim trailing CR if present
+ if (fileBytes[lineStart + len - 1] == (byte)'\r')
+ len--;
+
+ var line = new byte[len];
+ Buffer.BlockCopy(fileBytes, lineStart, line, 0, len);
+
+ ProcessLineForIndex(line, result);
+ }
+
+ lineStart = i + 1;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Process a single decoded line and update the group dictionary if a valid entry is found.
+ ///
+ private static void ProcessLineForIndex(byte[] line, Dictionary> groupDict)
+ {
+ // Find marker sequence
+ int start = IndexOfSequence(line, EStrFileOrg, 0);
+ if (start == -1)
+ return;
+
+ // Take bytes after marker plus 4 (matches Python behavior)
+ int partStart = start + EStrFileOrg.Length + 4;
+ if (partStart >= line.Length)
+ return;
+
+ int partLen = line.Length - partStart;
+ var part = new byte[partLen];
+ Buffer.BlockCopy(line, partStart, part, 0, partLen);
+
+ var (key, idx) = FindKey(part, 0);
+ if (key == null)
+ return;
+
+ var (eStrFileOrgValue, _) = GetPart(part, key.Value, idx);
+
+ string dPart = XorDecodeToString(eStrFileOrgValue, key.Value);
+ if (string.IsNullOrEmpty(dPart))
+ return;
+
+ // If the decoded part contains a colon, the Python script expects it to start with the prefix
+ const string prefix = "UserDataCustomFace";
+ string remainder;
+ var colonParts = dPart.Split([':'], 2);
+ if (colonParts.Length > 1)
+ {
+ if (!dPart.StartsWith(prefix, StringComparison.Ordinal))
+ return; // same as Python: skip if prefix missing
+
+ // Strip prefix and the following ':'
+ int removeLen = prefix.Length + 1;
+ if (dPart.Length <= removeLen)
+ return;
+ remainder = dPart.Substring(removeLen);
+ }
+ else
+ {
+ remainder = dPart;
+ }
+
+ // Split remainder by backslash to get group and filename
+ var arr = remainder.Split(['\\'], StringSplitOptions.None);
+ if (arr.Length < 2)
+ return;
+
+ string group = arr[0];
+ string filename = arr[1];
+
+ if (!groupDict.TryGetValue(group, out var files))
+ {
+ files = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ groupDict[group] = files;
+ }
+
+ if (!files.ContainsKey(filename))
+ {
+ files[filename] = files.Count;
+ }
+ }
+
+ ///
+ /// Locate subsequence in data starting at fromIndex, return -1 if not found
+ ///
+ private static int IndexOfSequence(byte[] data, byte[] seq, int fromIndex)
+ {
+ if (seq.Length == 0)
+ return fromIndex <= data.Length ? fromIndex : -1;
+ for (int i = fromIndex; i <= data.Length - seq.Length; i++)
+ {
+ bool ok = true;
+ for (int j = 0; j < seq.Length; j++)
+ {
+ if (data[i + j] != seq[j])
+ {
+ ok = false;
+ break;
+ }
+ }
+ if (ok)
+ return i;
+ }
+ return -1;
+ }
+
+ ///
+ /// Equivalent to Python find_key: find a byte that repeats at offsets +2 and +4
+ ///
+ private static (byte? key, int seek) FindKey(byte[] data, int startIdx)
+ {
+ for (int i = startIdx; i + 4 < data.Length; i++)
+ {
+ byte b = data[i];
+ if (b == data[i + 2] && b == data[i + 4])
+ return (b, i);
+ }
+ return (null, 0);
+ }
+
+ ///
+ /// Equivalent to Python get_part: extract the encrypted part starting at startIdx-1 up to end
+ ///
+ private static (byte[] part, int end) GetPart(byte[] data, byte key, int startIdx)
+ {
+ int end = 0;
+ for (int i = startIdx; i < data.Length; i += 2)
+ {
+ if (data[i] != key)
+ {
+ end = i - 1;
+ break;
+ }
+ }
+ if (end == 0)
+ end = data.Length - 1;
+
+ int start = startIdx - 1;
+ int length = end - start; // Python slice end is exclusive => length = end - (startIdx-1)
+ if (length <= 0)
+ return (Array.Empty(), end);
+
+ var part = new byte[length];
+ Buffer.BlockCopy(data, start, part, 0, length);
+ return (part, end);
+ }
+
+ ///
+ /// XOR-decode bytes and build a string, ignoring zero bytes (matches Python behavior)
+ ///
+ private static string XorDecodeToString(byte[] data, byte key)
+ {
+ if (data == null || data.Length == 0)
+ return string.Empty;
+
+ var sb = new StringBuilder();
+ foreach (var b in data)
+ {
+ byte v = (byte)(b ^ key);
+ if (v != 0)
+ sb.Append((char)v);
+ }
+ return sb.ToString();
+ }
+}