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; + } +}