diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj
index 2b707d0..71e0484 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj
@@ -64,6 +64,9 @@
all
+
+ all
+
@@ -78,6 +81,12 @@
+
+
+ QuickLook.Plugin.ImageViewer.Resources.%(RecursiveDir)%(Filename)%(Extension)
+
+
+
PreserveNewest
@@ -103,6 +112,11 @@
QuickLook.Common
False
+
+ {CE22A1F3-7F2C-4EC8-BFDE-B58D0EB625FC}
+ QuickLook.Plugin.HtmlViewer
+ False
+
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.html b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.html
new file mode 100644
index 0000000..261f1e9
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.html
@@ -0,0 +1,36 @@
+
+
+
+
+ SVG Preview
+
+
+
+
+
+
+
+
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.js b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.js
new file mode 100644
index 0000000..69f2ebf
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.js
@@ -0,0 +1,64 @@
+(async function () {
+ const wrapper = document.getElementById("svgWrapper");
+
+ const svgString = await chrome.webview.hostObjects.external.GetSvgContent();
+
+ wrapper.innerHTML = svgString;
+
+ let scale = 1;
+ let translate = { x: 0, y: 0 };
+ let isDragging = false;
+ let lastMouse = { x: 0, y: 0 };
+
+ function updateTransform() {
+ wrapper.style.transform = `translate(${translate.x}px, ${translate.y}px) scale(${scale})`;
+ }
+
+ document.addEventListener("wheel", (e) => {
+ e.preventDefault();
+
+ const scaleFactor = 1.1;
+ const oldScale = scale;
+
+ if (e.deltaY < 0) {
+ scale *= scaleFactor;
+ } else {
+ scale /= scaleFactor;
+ }
+
+ const rect = wrapper.getBoundingClientRect();
+ const dx = e.clientX - rect.left - rect.width / 2;
+ const dy = e.clientY - rect.top - rect.height / 2;
+
+ translate.x -= dx * (1 - scale / oldScale);
+ translate.y -= dy * (1 - scale / oldScale);
+
+ updateTransform();
+ }, { passive: false });
+
+ wrapper.addEventListener("mousedown", (e) => {
+ isDragging = true;
+ lastMouse = { x: e.clientX, y: e.clientY };
+ wrapper.style.cursor = "grabbing";
+ });
+
+ document.addEventListener("mousemove", (e) => {
+ if (!isDragging) return;
+
+ const dx = e.clientX - lastMouse.x;
+ const dy = e.clientY - lastMouse.y;
+
+ translate.x += dx;
+ translate.y += dy;
+
+ lastMouse = { x: e.clientX, y: e.clientY };
+ updateTransform();
+ });
+
+ document.addEventListener("mouseup", () => {
+ isDragging = false;
+ wrapper.style.cursor = "grab";
+ });
+
+ updateTransform();
+})();
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/WebImagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/WebImagePanel.cs
new file mode 100644
index 0000000..f6fd937
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/WebImagePanel.cs
@@ -0,0 +1,194 @@
+// 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 Microsoft.Web.WebView2.Core;
+using QuickLook.Plugin.HtmlViewer;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace QuickLook.Plugin.ImageViewer;
+
+public class WebImagePanel : WebpagePanel
+{
+ protected const string _resourcePrefix = "QuickLook.Plugin.ImageViewer.Resources.";
+ protected internal static readonly Dictionary _resources = [];
+ protected byte[] _homePage;
+
+ private object _objectForScripting;
+
+ public object ObjectForScripting
+ {
+ get => _objectForScripting;
+ set
+ {
+ _objectForScripting = value;
+
+ Dispatcher.Invoke(async () =>
+ {
+ await _webView.EnsureCoreWebView2Async();
+ _webView.CoreWebView2.AddHostObjectToScript("external", value);
+ });
+ }
+ }
+
+ static WebImagePanel()
+ {
+ InitializeResources();
+ }
+
+ protected static void InitializeResources()
+ {
+ if (_resources.Any()) return;
+
+ var assembly = Assembly.GetExecutingAssembly();
+
+ foreach (var resourceName in assembly.GetManifestResourceNames())
+ {
+ if (!resourceName.StartsWith(_resourcePrefix)) continue;
+
+ var relativePath = resourceName.Substring(_resourcePrefix.Length);
+ if (relativePath.Equals("resources", StringComparison.OrdinalIgnoreCase)) continue;
+
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream == null) continue;
+ var memoryStream = new MemoryStream();
+ stream.CopyTo(memoryStream);
+ _resources.Add($"/{relativePath.Replace('\\', '/')}", memoryStream.ToArray());
+ }
+ }
+
+ public void PreviewSvg(string path)
+ {
+ FallbackPath = Path.GetDirectoryName(path);
+
+ ObjectForScripting ??= new ScriptHandler(path);
+
+ _homePage = _resources["/svg2html.html"];
+ NavigateToUri(new Uri("file://quicklook/"));
+ }
+
+ protected override void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
+ {
+ if (e.IsSuccess)
+ {
+ _webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
+
+ _webView.CoreWebView2.WebResourceRequested += (sender, args) =>
+ {
+ Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}");
+
+ try
+ {
+ var requestedUri = new Uri(args.Request.Uri);
+
+ if (requestedUri.Scheme == "file")
+ {
+ if (requestedUri.AbsolutePath == "/")
+ {
+ var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
+ new MemoryStream(_homePage), 200, "OK", MimeTypes.GetContentType(".html"));
+ args.Response = response;
+ }
+ else if (ContainsKey(requestedUri.AbsolutePath))
+ {
+ var stream = ReadStream(requestedUri.AbsolutePath);
+ var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
+ stream, 200, "OK", MimeTypes.GetContentType(Path.GetExtension(requestedUri.AbsolutePath)));
+ args.Response = response;
+ }
+ else
+ {
+ var localPath = _fallbackPath + requestedUri.AbsolutePath.Replace('/', '\\');
+
+ if (File.Exists(localPath))
+ {
+ var fileStream = File.OpenRead(localPath);
+ var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
+ fileStream, 200, "OK", MimeTypes.GetContentType());
+ args.Response = response;
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ // We don't need to feel burdened by any exceptions
+ Debug.WriteLine(e);
+ }
+ };
+ }
+ }
+
+ public static bool ContainsKey(string key)
+ {
+ return _resources.ContainsKey(key);
+ }
+
+ public static Stream ReadStream(string key)
+ {
+ byte[] bytes = _resources[key];
+ return new MemoryStream(bytes);
+ }
+
+ public static string ReadString(string key)
+ {
+ using var reader = new StreamReader(ReadStream(key), Encoding.UTF8);
+ return reader.ReadToEnd();
+ }
+
+ public static class MimeTypes
+ {
+ public const string Html = "text/html";
+ public const string JavaScript = "application/javascript";
+ public const string Css = "text/css";
+ public const string Binary = "application/octet-stream";
+
+ public static string GetContentType(string extension = null) => $"Content-Type: {GetMimeType(extension)}";
+
+ public static string GetMimeType(string extension = null) => extension?.ToLowerInvariant() switch
+ {
+ ".js" => JavaScript, // Only handle known extensions from resources
+ ".css" => Css,
+ ".html" => Html,
+ _ => Binary,
+ };
+ }
+}
+
+[ClassInterface(ClassInterfaceType.AutoDual)]
+[ComVisible(true)]
+public sealed class ScriptHandler(string path)
+{
+ public string Path { get; } = path;
+
+ public async Task GetSvgContent()
+ {
+ if (File.Exists(Path))
+ {
+ var bytes = File.ReadAllBytes(Path);
+ return await Task.FromResult(Encoding.UTF8.GetString(bytes));
+ }
+ return string.Empty;
+ }
+}