diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/ChromiumResourcePackage/PakExtractor.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/ChromiumResourcePackage/PakExtractor.cs
new file mode 100644
index 0000000..dab9e21
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/ChromiumResourcePackage/PakExtractor.cs
@@ -0,0 +1,231 @@
+// Copyright © 2017-2026 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.Buffers;
+using System.Collections.Generic;
+using System.IO;
+
+namespace QuickLook.Plugin.ArchiveViewer.ChromiumResourcePackage;
+
+///
+/// Provides static methods for extracting resources from Chrome .pak archive files.
+///
+public static class PakExtractor
+{
+ ///
+ /// Extracts all resources from a Chrome .pak file and saves them as individual files in the specified output directory.
+ /// Each resource file is named using a 9-digit zero-padded decimal string representing its resource ID (e.g., "000000001").
+ ///
+ /// The path to the .pak file to extract.
+ /// The directory where extracted resource files will be saved.
+ public static void ExtractToDirectory(string fileName, string outputDirectory)
+ {
+ using var stream = File.OpenRead(fileName);
+ using var br = new BinaryReader(stream);
+ var version = br.ReadUInt32();
+ var encoding = br.ReadByte();
+ stream.Seek(3, SeekOrigin.Current); // Skip 3 reserved bytes
+ var resourceCount = br.ReadUInt16();
+ var aliasCount = br.ReadUInt16();
+
+ Entry[] entries = new Entry[resourceCount + 1];
+ for (int i = 0; i < resourceCount + 1; i++)
+ {
+ var resourceId = br.ReadUInt16();
+ var fileOffset = br.ReadUInt32();
+ entries[i] = new Entry(resourceId, fileOffset);
+ }
+ // Aliases are not used in extraction, so just skip reading if not needed
+ stream.Seek(aliasCount * 4, SeekOrigin.Current);
+
+ Directory.CreateDirectory(outputDirectory);
+
+ // Use a single buffer for all resources (max resource size)
+ int maxLength = 0;
+ for (int i = 0; i < resourceCount; i++)
+ {
+ int len = (int)(entries[i + 1].FileOffset - entries[i].FileOffset);
+ if (len > maxLength) maxLength = len;
+ }
+ byte[] buffer = ArrayPool.Shared.Rent(maxLength);
+
+ try
+ {
+ for (int i = 0; i < resourceCount; i++)
+ {
+ int length = (int)(entries[i + 1].FileOffset - entries[i].FileOffset);
+ stream.Seek(entries[i].FileOffset, SeekOrigin.Begin);
+ int read = 0;
+ while (read < length)
+ {
+ int n = stream.Read(buffer, read, length - read);
+ if (n == 0) break;
+ read += n;
+ }
+ string resourceName = entries[i].ResourceId.ToString("D9");
+ using var file = File.Create(Path.Combine(outputDirectory, resourceName));
+ file.Write(buffer, 0, length);
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ ///
+ /// Extracts all resources from a Chrome .pak file and returns them as a dictionary.
+ /// The dictionary key is a 9-digit zero-padded decimal string representing the resource ID,
+ /// optionally with a guessed file extension, and the value is the resource content as a byte array.
+ ///
+ /// The path to the .pak file to extract.
+ /// If true, append guessed file extension to the key (e.g., "000000001.png").
+ /// A dictionary mapping resource names to their byte content.
+ public static Dictionary ExtractToDictionary(string fileName, bool appendExtension = true)
+ {
+ using var stream = File.OpenRead(fileName);
+ using var br = new BinaryReader(stream);
+ var version = br.ReadUInt32();
+ var encoding = br.ReadByte();
+ stream.Seek(3, SeekOrigin.Current); // Skip 3 reserved bytes
+ var resourceCount = br.ReadUInt16();
+ var aliasCount = br.ReadUInt16();
+
+ Entry[] entries = new Entry[resourceCount + 1];
+ for (int i = 0; i < resourceCount + 1; i++)
+ {
+ var resourceId = br.ReadUInt16();
+ var fileOffset = br.ReadUInt32();
+ entries[i] = new Entry(resourceId, fileOffset);
+ }
+ // Aliases are not used in extraction, so just skip reading if not needed
+ stream.Seek(aliasCount * 4, SeekOrigin.Current);
+
+ // Use a single buffer for all resources (max resource size)
+ int maxLength = 0;
+ for (int i = 0; i < resourceCount; i++)
+ {
+ int len = (int)(entries[i + 1].FileOffset - entries[i].FileOffset);
+ if (len > maxLength) maxLength = len;
+ }
+ byte[] buffer = ArrayPool.Shared.Rent(maxLength);
+ var dict = new Dictionary(resourceCount);
+
+ try
+ {
+ for (int i = 0; i < resourceCount; i++)
+ {
+ int length = (int)(entries[i + 1].FileOffset - entries[i].FileOffset);
+ stream.Seek(entries[i].FileOffset, SeekOrigin.Begin);
+ int read = 0;
+ while (read < length)
+ {
+ int n = stream.Read(buffer, read, length - read);
+ if (n == 0) break;
+ read += n;
+ }
+ string resourceName = entries[i].ResourceId.ToString("D9");
+ if (appendExtension)
+ {
+ string ext = GuessFileExtension(buffer, length);
+ resourceName += ext;
+ }
+ // Copy only the valid part of buffer
+ var data = new byte[length];
+ Buffer.BlockCopy(buffer, 0, data, 0, length);
+ dict[resourceName] = data;
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ return dict;
+ }
+
+ ///
+ /// Guesses the file extension based on the content of the given byte array.
+ /// Returns common extensions such as "png", "jpg", "gif", "bmp", "pdf", "zip", etc.
+ /// Returns ".bin" if the type cannot be determined.
+ ///
+ /// The byte array containing the file data.
+ /// The valid length of data to check (for pooled buffer usage).
+ /// The guessed file extension with dot, e.g. ".png".
+ public static string GuessFileExtension(byte[] data, int length = -1)
+ {
+ if (data == null || (length < 0 ? data.Length : length) < 4)
+ return ".bin";
+ int len = length < 0 ? data.Length : length;
+ // PNG
+ if (len > 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47)
+ return ".png";
+ // JPEG
+ if (data[0] == 0xFF && data[1] == 0xD8)
+ return ".jpg";
+ // GIF
+ if (data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46)
+ return ".gif";
+ // BMP
+ if (data[0] == 0x42 && data[1] == 0x4D)
+ return ".bmp";
+ // PDF
+ if (data[0] == 0x25 && data[1] == 0x50 && data[2] == 0x44 && data[3] == 0x46)
+ return ".pdf";
+ // ZIP/Office
+ if (data[0] == 0x50 && data[1] == 0x4B && (data[2] == 0x03 || data[2] == 0x05 || data[2] == 0x07) && (data[3] == 0x04 || data[3] == 0x06 || data[3] == 0x08))
+ return ".zip";
+ // RAR
+ if (len > 7 && data[0] == 0x52 && data[1] == 0x61 && data[2] == 0x72 && data[3] == 0x21 && data[4] == 0x1A && data[5] == 0x07 && (data[6] == 0x00 || data[6] == 0x01))
+ return ".rar";
+ // 7z
+ if (len > 5 && data[0] == 0x37 && data[1] == 0x7A && data[2] == 0xBC && data[3] == 0xAF && data[4] == 0x27 && data[5] == 0x1C)
+ return ".7z";
+ // MP3
+ if (len > 2 && data[0] == 0x49 && data[1] == 0x44 && data[2] == 0x33)
+ return ".mp3";
+ // MP4
+ if (len > 11 && data[4] == 0x66 && data[5] == 0x74 && data[6] == 0x79 && data[7] == 0x70)
+ return ".mp4";
+ // WebP
+ if (len > 11 && data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 && data[8] == 0x57 && data[9] == 0x45 && data[10] == 0x42 && data[11] == 0x50)
+ return ".webp";
+ // TXT (heuristic: printable ASCII, no nulls)
+ bool isText = true;
+ for (int i = 0; i < Math.Min(64, len); i++)
+ {
+ if (data[i] == 0 || (data[i] < 0x09) || (data[i] > 0x0D && data[i] < 0x20))
+ {
+ isText = false;
+ break;
+ }
+ }
+ if (isText)
+ return ".txt";
+ return ".bin";
+ }
+}
+
+///
+/// Represents a resource entry in the .pak file, containing the resource ID and its file offset.
+///
+public record struct Entry(ushort ResourceId, uint FileOffset);
+
+///
+/// Represents an alias entry in the .pak file, mapping a resource ID to an entry index.
+///
+public record struct Alias(ushort ResourceId, ushort EntryIndex);
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs
index fb51bd5..084ea7b 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs
@@ -18,6 +18,7 @@
using QuickLook.Common.Plugin;
using QuickLook.Common.Plugin.MoreMenu;
using QuickLook.Plugin.ArchiveViewer.ArchiveFile;
+using QuickLook.Plugin.ArchiveViewer.ChromiumResourcePackage;
using QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
using System;
using System.Collections.Generic;
@@ -55,6 +56,9 @@ public sealed partial class Plugin : IViewer, IMoreMenu
// 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)
+
+ // List of supported chromium resource package file extensions
+ ".pak", // Chromium resource package file, used by Chromium-based applications (e.g., Google Chrome)
];
private IDisposable _panel;
@@ -86,6 +90,13 @@ public sealed partial class Plugin : IViewer, IMoreMenu
{
_panel = new CompoundInfoPanel(path);
}
+ else if (path.EndsWith(".pak", StringComparison.OrdinalIgnoreCase))
+ {
+ var dict = PakExtractor.ExtractToDictionary(path);
+
+ // TODO
+ _ = dict;
+ }
else
{
_panel = new ArchiveInfoPanel(path);