diff --git a/QuickLook.Plugin/QuickLook.Plugin.FontViewer/WebfontPanel.cs b/QuickLook.Plugin/QuickLook.Plugin.FontViewer/WebfontPanel.cs index a44c539..96b0014 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.FontViewer/WebfontPanel.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.FontViewer/WebfontPanel.cs @@ -124,55 +124,47 @@ public class WebfontPanel : WebpagePanel return html; } - protected override void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e) + protected override void WebView_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs args) { - if (e.IsSuccess) + Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}"); + + try { - _webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + var requestedUri = new Uri(args.Request.Uri); - _webView.CoreWebView2.WebResourceRequested += (sender, args) => + if (requestedUri.Scheme == "file") { - Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}"); - - try + if (requestedUri.AbsolutePath == "/") { - var requestedUri = new Uri(args.Request.Uri); + 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 (requestedUri.Scheme == "file") + if (File.Exists(localPath)) { - 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; - } - } + 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); - } - }; + } + } + catch (Exception e) + { + // We don't need to feel burdened by any exceptions + Debug.WriteLine(e); } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs index f037269..0e87d6f 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs @@ -176,51 +176,52 @@ public class WebpagePanel : UserControl if (e.IsSuccess) { _webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + _webView.CoreWebView2.WebResourceRequested += WebView_WebResourceRequested; + } + } - _webView.CoreWebView2.WebResourceRequested += (sender, args) => + protected virtual void WebView_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs args) + { + if (string.IsNullOrWhiteSpace(_fallbackPath) || !Directory.Exists(_fallbackPath)) + { + return; + } + + try + { + var requestedUri = new Uri(args.Request.Uri); + + // Check if the request is for a local file + if (requestedUri.Scheme == "file" && !File.Exists(requestedUri.LocalPath)) { - if (string.IsNullOrWhiteSpace(_fallbackPath) || !Directory.Exists(_fallbackPath)) - { - return; - } + // Try loading from fallback directory + var fileName = Path.GetFileName(requestedUri.LocalPath); + var fileDirectoryName = Path.GetDirectoryName(requestedUri.LocalPath); - try + // Convert the primary path to fallback path + if (fileDirectoryName.StartsWith(_primaryPath)) { - var requestedUri = new Uri(args.Request.Uri); + var fallbackFilePath = Path.Combine( + _fallbackPath.Trim('/', '\\'), // Make it combinable + fileDirectoryName.Substring(_primaryPath.Length).Trim('/', '\\'), // Make it combinable + fileName + ); - // Check if the request is for a local file - if (requestedUri.Scheme == "file" && !File.Exists(requestedUri.LocalPath)) + if (File.Exists(fallbackFilePath)) { - // Try loading from fallback directory - var fileName = Path.GetFileName(requestedUri.LocalPath); - var fileDirectoryName = Path.GetDirectoryName(requestedUri.LocalPath); - - // Convert the primary path to fallback path - if (fileDirectoryName.StartsWith(_primaryPath)) - { - var fallbackFilePath = Path.Combine( - _fallbackPath.Trim('/', '\\'), // Make it combinable - fileDirectoryName.Substring(_primaryPath.Length).Trim('/', '\\'), // Make it combinable - fileName - ); - - if (File.Exists(fallbackFilePath)) - { - // Serve the file from the fallback directory - var fileStream = File.OpenRead(fallbackFilePath); - var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( - fileStream, 200, "OK", "Content-Type: application/octet-stream"); - args.Response = response; - } - } + // Serve the file from the fallback directory + var fileStream = File.OpenRead(fallbackFilePath); + var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( + fileStream, 200, "OK", "Content-Type: application/octet-stream"); + args.Response = response; } } - catch (Exception e) - { - // We don't need to feel burdened by any exceptions - Debug.WriteLine(e); - } - }; + } + } + catch (Exception e) + { + // We don't need to feel burdened by any exceptions + Debug.WriteLine(e); } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs index 5320d8a..1329369 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs @@ -18,6 +18,7 @@ using QuickLook.Common.Helpers; using QuickLook.Common.Plugin; using QuickLook.Plugin.ImageViewer.AnimatedImage.Providers; +using QuickLook.Plugin.ImageViewer.Webview; using System; using System.Collections.Generic; using System.IO; @@ -48,7 +49,7 @@ public class Plugin : IViewer ".pbm", ".pcx", ".pef", ".pgm", ".png", ".pnm", ".ppm", ".psb", ".psd", ".ptx", ".pxn", ".qoi", ".r3d", ".raf", ".raw", ".rw2", ".rwl", ".rwz", - ".sr2", ".srf", ".srw", ".svg", ".svga", ".svgz", + ".sr2", ".srf", ".srw", ".svg", ".svgz", ".tga", ".tif", ".tiff", ".wdp", ".webp", ".wmf", ".x3f", ".xcf", ".xbm", ".xpm", @@ -57,8 +58,8 @@ public class Plugin : IViewer private ImagePanel _ip; private MetaProvider _meta; - private SvgImagePanel _ipSvg; - private SvgMetaProvider _metaSvg; + private IWebImagePanel _ipWeb; + private IWebMetaProvider _metaWeb; public int Priority => 0; @@ -100,44 +101,20 @@ public class Plugin : IViewer typeof(ImageMagickProvider))); } - private bool IsWellKnownImageExtension(string path) - { - return WellKnownImageExtensions.Contains(Path.GetExtension(path.ToLower())); - } - public bool CanHandle(string path) { + if (WebHandler.TryCanHandle(path)) + return true; + // Disabled due mishandling text file types e.g., "*.config". // Only check extension for well known image and animated image types. - return !Directory.Exists(path) && IsWellKnownImageExtension(path); + return !Directory.Exists(path) && WellKnownImageExtensions.Contains(Path.GetExtension(path).ToLower()); } public void Prepare(string path, ContextObject context) { - if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) - { - if (SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer")) - { - _metaSvg = new SvgMetaProvider(path); - var sizeSvg = _metaSvg.GetSize(); - - if (!sizeSvg.IsEmpty) - context.SetPreferredSizeFit(sizeSvg, 0.8d); - else - context.PreferredSize = new Size(800, 600); - - context.Theme = (Themes)SettingHelper.Get("LastTheme", 1, "QuickLook.Plugin.ImageViewer"); - return; - } - } - else if (path.EndsWith(".svga", StringComparison.OrdinalIgnoreCase)) - { - _metaSvg = new SvgMetaProvider(path); - - context.PreferredSize = new Size(800, 600); - context.Theme = (Themes)SettingHelper.Get("LastTheme", 1, "QuickLook.Plugin.ImageViewer"); + if (WebHandler.TryPrepare(path, context, out _metaWeb)) return; - } _meta = new MetaProvider(path); @@ -153,25 +130,8 @@ public class Plugin : IViewer public void View(string path, ContextObject context) { - if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) - || path.EndsWith(".svga", StringComparison.OrdinalIgnoreCase)) - { - if (SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer")) - { - _ipSvg = new SvgImagePanel(); - _ipSvg.PreviewSvg(path); - - var sizeSvg = _metaSvg.GetSize(); - - context.ViewerContent = _ipSvg; - context.Title = sizeSvg.IsEmpty - ? $"{Path.GetFileName(path)}" - : $"{sizeSvg.Width}×{sizeSvg.Height}: {Path.GetFileName(path)}"; - - context.IsBusy = false; - return; - } - } + if (WebHandler.TryView(path, context, _metaWeb, out _ipWeb)) + return; _ip = new ImagePanel(context, _meta); var size = _meta.GetSize(); @@ -197,7 +157,7 @@ public class Plugin : IViewer _ip?.Dispose(); _ip = null; - _ipSvg?.Dispose(); - _ipSvg = null; + _ipWeb?.Dispose(); + _ipWeb = null; } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj index 1f347fb..7ddf26d 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj @@ -55,6 +55,7 @@ + all @@ -91,7 +92,7 @@ - + QuickLook.Plugin.ImageViewer.Resources.%(RecursiveDir)%(Filename)%(Extension) diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/lottie2html.html b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/lottie2html.html new file mode 100644 index 0000000..ca104c4 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/lottie2html.html @@ -0,0 +1,25 @@ + + + + + LottieFiles Preview + + + +
+ + + + diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/lottie2html.js b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/lottie2html.js new file mode 100644 index 0000000..497f239 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/lottie2html.js @@ -0,0 +1,37 @@ +/** + * SvgaViewer: Provides SVGA animation preview with the following features. + * + * Requirements: + * - Requires the following HTML structure: + * + * + * - SVGA file path is obtained via chrome.webview.hostObjects.external.GetPath() + * + * Features: + * - Loads and plays SVGA animation files + * - Uses SVGA.js library for parsing and playback + * - Automatically starts playback after loading + * - Handles asynchronous loading and mounting of SVGA files +*/ +class LottieViewer { + constructor() { + } + + /** + * Play Lottie files. + * @async + */ + async play() { + const path = await chrome.webview.hostObjects.external.GetPath(); + lottie.loadAnimation({ + container: document.getElementById('bm'), + renderer: 'svg', + loop: true, + autoplay: true, + path: 'https://' + path, + }); + } +} + +// Create the Lottie viewer and play +new LottieViewer().play(); diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.html b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svg2html.html similarity index 100% rename from QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.html rename to QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svg2html.html diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.js b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svg2html.js similarity index 100% rename from QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svg2html.js rename to QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svg2html.js diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svga2html.html b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svga2html.html similarity index 100% rename from QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svga2html.html rename to QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svga2html.html diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svga2html.js b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svga2html.js similarity index 100% rename from QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/svga2html.js rename to QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Resources/Web/svga2html.js diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/SvgMetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/SvgMetaProvider.cs deleted file mode 100644 index 20060e8..0000000 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/SvgMetaProvider.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Windows; -using System.Xml.Linq; - -namespace QuickLook.Plugin.ImageViewer; - -public class SvgMetaProvider(string path) -{ - private readonly string _path = path; - private Size _size = Size.Empty; - - public Size GetSize() - { - if (_size != Size.Empty) - { - return _size; - } - - if (!File.Exists(_path)) - { - return _size; - } - - if (_path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) - { - try - { - var svgContent = File.ReadAllText(_path); - var svg = XElement.Parse(svgContent); - XNamespace ns = svg.Name.Namespace; - - string widthAttr = svg.Attribute("width")?.Value; - string heightAttr = svg.Attribute("height")?.Value; - - float? width = TryParseSvgLength(widthAttr); - float? height = TryParseSvgLength(heightAttr); - - if (width.HasValue && height.HasValue) - { - _size = new Size { Width = width.Value, Height = height.Value }; - } - - string viewBoxAttr = svg.Attribute("viewBox")?.Value; - if (!string.IsNullOrEmpty(viewBoxAttr)) - { - var parts = viewBoxAttr.Split([' ', ','], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 4 && - float.TryParse(parts[2], out float vbWidth) && - float.TryParse(parts[3], out float vbHeight)) - { - _size = new Size { Width = vbWidth, Height = vbHeight }; - } - } - } - catch (Exception e) - { - Debug.WriteLine(e); - } - } - - return _size; - } - - private static float? TryParseSvgLength(string input) - { - if (string.IsNullOrEmpty(input)) - return null; - - var match = Regex.Match(input.Trim(), @"^([\d.]+)(px|pt|mm|cm|in|em|ex|%)?$", RegexOptions.IgnoreCase); - if (match.Success && float.TryParse(match.Groups[1].Value, out float value)) - { - return value; - } - return null; - } -} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/IWebImagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/IWebImagePanel.cs new file mode 100644 index 0000000..114f6f0 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/IWebImagePanel.cs @@ -0,0 +1,25 @@ +// 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; + +namespace QuickLook.Plugin.ImageViewer.Webview; + +internal interface IWebImagePanel : IDisposable +{ + public void Preview(string path); +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/IWebMetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/IWebMetaProvider.cs new file mode 100644 index 0000000..bfb2dc7 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/IWebMetaProvider.cs @@ -0,0 +1,25 @@ +// 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.Windows; + +namespace QuickLook.Plugin.ImageViewer.Webview; + +internal interface IWebMetaProvider +{ + public Size GetSize(); +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieDetector.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieDetector.cs new file mode 100644 index 0000000..6271b01 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieDetector.cs @@ -0,0 +1,51 @@ +// 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; + +namespace QuickLook.Plugin.ImageViewer.Webview.Lottie; + +internal static class LottieDetector +{ + public static bool IsVaild(string path) + { + try + { + var jsonString = File.ReadAllText(path); + + // No exception will be thrown here + var jsonLottie = LottieParser.Parse>(jsonString); + + if (jsonLottie != null + && jsonLottie.ContainsKey("v") + && jsonLottie.ContainsKey("fr") + && jsonLottie.ContainsKey("ip") + && jsonLottie.ContainsKey("op") + && jsonLottie.ContainsKey("layers")) + { + return true; + } + } + catch + { + // If any exception occurs, assume it's not a valid Lottie file + } + + return false; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieExtractor.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieExtractor.cs new file mode 100644 index 0000000..eade22f --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieExtractor.cs @@ -0,0 +1,97 @@ +// 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.IO.Compression; +using System.Linq; +using System.Text; + +namespace QuickLook.Plugin.ImageViewer.Webview.Lottie; + +internal static class LottieExtractor +{ + public static string GetJsonContent(string path) + { + using var fileStream = File.OpenRead(path); + using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read); + + var manifestEntry = zipArchive.GetEntry("manifest.json"); + List idEntries = []; + + if (manifestEntry != null) + { + using var manifestStream = manifestEntry.Open(); + using var manifestReader = new StreamReader(manifestStream, Encoding.UTF8); + string content = manifestReader.ReadToEnd(); + + if (!string.IsNullOrEmpty(content)) + { + var manifestJson = LottieParser.Parse>(content); + + if (manifestJson.ContainsKey("animations")) + { + object animations = manifestJson["animations"]; + + if (manifestJson["animations"] is IEnumerable animationsEnumerable) + { + foreach (var animationsItem in animationsEnumerable.ToArray()) + { + if (animationsItem is Dictionary animationsItemDict) + { + if (animationsItemDict.ContainsKey("id")) + { + idEntries.Add($"animations/{animationsItemDict["id"]}"); + } + } + } + } + } + + // Read animations error from manifest.json and fallback to read all entries + if (idEntries.Count == 0) + { + foreach (var entry in zipArchive.Entries) + { + if (entry.FullName.StartsWith("animations")) + { + idEntries.Add(entry.FullName); + } + } + } + + // Read the all animations + if (idEntries.Count > 0) + { + // I don't know if there are multiple animations + // But only support the first animation + var idEntry = $"{idEntries[0]}.json"; + var animationEntry = zipArchive.GetEntry(idEntry); + + if (animationEntry != null) + { + using var jsonStream = animationEntry.Open(); + using var jsonReader = new StreamReader(jsonStream, Encoding.UTF8); + return jsonReader.ReadToEnd(); + } + } + } + } + + return null; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieImagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieImagePanel.cs new file mode 100644 index 0000000..644f71c --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieImagePanel.cs @@ -0,0 +1,78 @@ +// 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.ImageViewer.Webview.Svg; +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace QuickLook.Plugin.ImageViewer.Webview.Lottie; + +public class LottieImagePanel : SvgImagePanel +{ + public override void Preview(string path) + { + FallbackPath = Path.GetDirectoryName(path); + + ObjectForScripting ??= new ScriptHandler(path); + + _homePage = _resources["/lottie2html.html"]; + NavigateToUri(new Uri("file://quicklook/")); + } + + protected override void WebView_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs args) + { + try + { + var requestedUri = new Uri(args.Request.Uri); + + if ((requestedUri.Scheme == "https" || requestedUri.Scheme == "http") + && requestedUri.AbsolutePath.EndsWith(".lottie", StringComparison.OrdinalIgnoreCase)) + { + var localPath = Uri.UnescapeDataString($"{requestedUri.Authority}:{requestedUri.AbsolutePath}".Replace('/', '\\')); + + if (localPath.StartsWith(_fallbackPath, StringComparison.OrdinalIgnoreCase)) + { + if (File.Exists(localPath)) + { + var content = LottieExtractor.GetJsonContent(localPath); + byte[] byteArray = Encoding.UTF8.GetBytes(content); + var stream = new MemoryStream(byteArray); + var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( + stream, 200, "OK", + $""" + Access-Control-Allow-Origin: * + Content-Type: {MimeTypes.GetMimeType()} + """ + ); + args.Response = response; + return; + } + } + } + } + catch (Exception e) + { + // We don't need to feel burdened by any exceptions + Debug.WriteLine(e); + } + + base.WebView_WebResourceRequested(sender, args); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieMetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieMetaProvider.cs new file mode 100644 index 0000000..5985ee2 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieMetaProvider.cs @@ -0,0 +1,61 @@ +// 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.Windows; + +namespace QuickLook.Plugin.ImageViewer.Webview.Lottie; + +public class LottieMetaProvider(string path) : IWebMetaProvider +{ + private readonly string _path = path; + private Size _size = Size.Empty; + + public Size GetSize() + { + if (_size != Size.Empty) + { + return _size; + } + + if (!File.Exists(_path)) + { + return _size; + } + + try + { + var jsonString = LottieExtractor.GetJsonContent(_path); + var jsonLottie = LottieParser.Parse>(jsonString); + + if (jsonLottie.ContainsKey("w") + && jsonLottie.ContainsKey("h") + && double.TryParse(jsonLottie["w"].ToString(), out double width) + && double.TryParse(jsonLottie["h"].ToString(), out double height)) + { + return _size = new Size(width, height); + } + } + catch + { + // That's fine, just return the default size. + } + + return new Size(800, 600); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieParser.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieParser.cs new file mode 100644 index 0000000..945d50a --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Lottie/LottieParser.cs @@ -0,0 +1,369 @@ +// 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; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; + +namespace QuickLook.Plugin.ImageViewer.Webview.Lottie; + +internal static class LottieParser +{ + [ThreadStatic] private static Stack> splitArrayPool = null!; + [ThreadStatic] private static StringBuilder stringBuilder = null!; + [ThreadStatic] private static Dictionary> fieldInfoCache = null!; + [ThreadStatic] private static Dictionary> propertyInfoCache = null!; + + public static T Parse(string json) + { + // Initialize, if needed, the ThreadStatic variables + propertyInfoCache ??= []; + fieldInfoCache ??= []; + stringBuilder ??= new StringBuilder(); + splitArrayPool ??= new Stack>(); + + //Remove all whitespace not within strings to make parsing simpler + stringBuilder.Length = 0; + for (int i = 0; i < json.Length; i++) + { + char c = json[i]; + if (c == '"') + { + i = AppendUntilStringEnd(true, i, json); + continue; + } + if (char.IsWhiteSpace(c)) + continue; + + stringBuilder.Append(c); + } + + //Parse the thing! + return (T)ParseValue(typeof(T), stringBuilder.ToString()); + } + + static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) + { + stringBuilder.Append(json[startIdx]); + for (int i = startIdx + 1; i < json.Length; i++) + { + if (json[i] == '\\') + { + if (appendEscapeCharacter) + stringBuilder.Append(json[i]); + stringBuilder.Append(json[i + 1]); + i++;//Skip next character as it is escaped + } + else if (json[i] == '"') + { + stringBuilder.Append(json[i]); + return i; + } + else + stringBuilder.Append(json[i]); + } + return json.Length - 1; + } + + //Splits { :, : } and [ , ] into a list of strings + static List Split(string json) + { + List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : []; + splitArray.Clear(); + if (json.Length == 2) + return splitArray; + int parseDepth = 0; + stringBuilder.Length = 0; + for (int i = 1; i < json.Length - 1; i++) + { + switch (json[i]) + { + case '[': + case '{': + parseDepth++; + break; + + case ']': + case '}': + parseDepth--; + break; + + case '"': + i = AppendUntilStringEnd(true, i, json); + continue; + case ',': + case ':': + if (parseDepth == 0) + { + splitArray.Add(stringBuilder.ToString()); + stringBuilder.Length = 0; + continue; + } + break; + } + + stringBuilder.Append(json[i]); + } + + splitArray.Add(stringBuilder.ToString()); + + return splitArray; + } + + internal static object ParseValue(Type type, string json) + { + if (type == typeof(string)) + { + if (json.Length <= 2) + return string.Empty; + StringBuilder parseStringBuilder = new(json.Length); + for (int i = 1; i < json.Length - 1; ++i) + { + if (json[i] == '\\' && i + 1 < json.Length - 1) + { + int j = "\"\\nrtbf/".IndexOf(json[i + 1]); + if (j >= 0) + { + parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); + ++i; + continue; + } + if (json[i + 1] == 'u' && i + 5 < json.Length - 1) + { + if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out uint c)) + { + parseStringBuilder.Append((char)c); + i += 5; + continue; + } + } + } + parseStringBuilder.Append(json[i]); + } + return parseStringBuilder.ToString(); + } + if (type.IsPrimitive) + { + var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); + return result; + } + if (type == typeof(decimal)) + { + decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal result); + return result; + } + if (type == typeof(DateTime)) + { + DateTime.TryParse(json.Replace("\"", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime result); + return result; + } + if (json == "null") + { + return null!; + } + if (type.IsEnum) + { + if (json[0] == '"') + json = json.Substring(1, json.Length - 2); + try + { + return Enum.Parse(type, json, false); + } + catch + { + return 0; + } + } + if (type.IsArray) + { + Type arrayType = type.GetElementType(); + if (json[0] != '[' || json[json.Length - 1] != ']') + return null!; + + List elems = Split(json); + Array newArray = Array.CreateInstance(arrayType, elems.Count); + for (int i = 0; i < elems.Count; i++) + newArray.SetValue(ParseValue(arrayType, elems[i]), i); + splitArrayPool.Push(elems); + return newArray; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + Type listType = type.GetGenericArguments()[0]; + if (json[0] != '[' || json[json.Length - 1] != ']') + return null!; + + List elems = Split(json); + var list = (IList)type.GetConstructor([typeof(int)]).Invoke([elems.Count]); + for (int i = 0; i < elems.Count; i++) + list.Add(ParseValue(listType, elems[i])); + splitArrayPool.Push(elems); + return list; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type keyType, valueType; + { + Type[] args = type.GetGenericArguments(); + keyType = args[0]; + valueType = args[1]; + } + + //Refuse to parse dictionary keys that aren't of type string + if (keyType != typeof(string)) + return null!; + //Must be a valid dictionary element + if (json[0] != '{' || json[json.Length - 1] != '}') + return null!; + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + if (elems.Count % 2 != 0) + return null!; + + var dictionary = (IDictionary)type.GetConstructor([typeof(int)]).Invoke([elems.Count / 2]); + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string keyValue = elems[i].Substring(1, elems[i].Length - 2); + object val = ParseValue(valueType, elems[i + 1]); + dictionary[keyValue] = val; + } + return dictionary; + } + if (type == typeof(object)) + { + return ParseAnonymousValue(json); + } + if (json[0] == '{' && json[json.Length - 1] == '}') + { + return ParseObject(type, json); + } + + return null!; + } + + static object ParseAnonymousValue(string json) + { + if (json.Length == 0) + return null!; + if (json[0] == '{' && json[json.Length - 1] == '}') + { + List elems = Split(json); + if (elems.Count % 2 != 0) + return null!; + var dict = new Dictionary(elems.Count / 2); + for (int i = 0; i < elems.Count; i += 2) + dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]); + return dict; + } + if (json[0] == '[' && json[json.Length - 1] == ']') + { + List items = Split(json); + var finalList = new List(items.Count); + for (int i = 0; i < items.Count; i++) + finalList.Add(ParseAnonymousValue(items[i])); + return finalList; + } + if (json[0] == '"' && json[json.Length - 1] == '"') + { + string str = json.Substring(1, json.Length - 2); + return str.Replace("\\", string.Empty); + } + if (char.IsDigit(json[0]) || json[0] == '-') + { + if (json.Contains(".")) + { + double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double result); + return result; + } + else + { + int.TryParse(json, out int result); + return result; + } + } + if (json == "true") + return true; + if (json == "false") + return false; + // handles json == "null" as well as invalid JSON + return null!; + } + + static Dictionary CreateMemberNameDictionary(T[] members) where T : MemberInfo + { + Dictionary nameToMember = new(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < members.Length; i++) + { + T member = members[i]; + if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) + continue; + + string name = member.Name; + if (member.IsDefined(typeof(DataMemberAttribute), true)) + { + DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); + if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) + name = dataMemberAttribute.Name; + } + + nameToMember.Add(name, member); + } + + return nameToMember; + } + + static object ParseObject(Type type, string json) + { + object instance = FormatterServices.GetUninitializedObject(type); + + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + if (elems.Count % 2 != 0) + return instance; + + if (!fieldInfoCache.TryGetValue(type, out Dictionary nameToField)) + { + nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + fieldInfoCache.Add(type, nameToField); + } + if (!propertyInfoCache.TryGetValue(type, out Dictionary nameToProperty)) + { + nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + propertyInfoCache.Add(type, nameToProperty); + } + + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string key = elems[i].Substring(1, elems[i].Length - 2); + string value = elems[i + 1]; + + if (nameToField.TryGetValue(key, out FieldInfo fieldInfo)) + fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); + else if (nameToProperty.TryGetValue(key, out PropertyInfo propertyInfo)) + propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); + } + + return instance; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/SvgImagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svg/SvgImagePanel.cs similarity index 58% rename from QuickLook.Plugin/QuickLook.Plugin.ImageViewer/SvgImagePanel.cs rename to QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svg/SvgImagePanel.cs index 3a79700..d5d7052 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/SvgImagePanel.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svg/SvgImagePanel.cs @@ -29,9 +29,9 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace QuickLook.Plugin.ImageViewer; +namespace QuickLook.Plugin.ImageViewer.Webview.Svg; -public class SvgImagePanel : WebpagePanel +public class SvgImagePanel : WebpagePanel, IWebImagePanel { protected const string _resourcePrefix = "QuickLook.Plugin.ImageViewer.Resources."; protected internal static readonly Dictionary _resources = []; @@ -93,80 +93,77 @@ public class SvgImagePanel : WebpagePanel } } - public void PreviewSvg(string path) + public virtual void Preview(string path) { FallbackPath = Path.GetDirectoryName(path); ObjectForScripting ??= new ScriptHandler(path); - _homePage = _resources[path.EndsWith(".svga", StringComparison.OrdinalIgnoreCase) ? "/svga2html.html" : "/svg2html.html"]; + _homePage = _resources["/svg2html.html"]; NavigateToUri(new Uri("file://quicklook/")); } - protected override void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e) + protected override void WebView_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs args) { - if (e.IsSuccess) + Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}"); + + try { - _webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + var requestedUri = new Uri(args.Request.Uri); - _webView.CoreWebView2.WebResourceRequested += (sender, args) => + if (requestedUri.Scheme == "file") { - Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}"); - - try + if (requestedUri.AbsolutePath == "/") { - var requestedUri = new Uri(args.Request.Uri); + var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( + new MemoryStream(_homePage), 200, "OK", MimeTypes.GetContentTypeHeader(".html")); + args.Response = response; + } + else if (ContainsKey(requestedUri.AbsolutePath)) + { + var stream = ReadStream(requestedUri.AbsolutePath); + var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( + stream, 200, "OK", MimeTypes.GetContentTypeHeader(Path.GetExtension(requestedUri.AbsolutePath))); + args.Response = response; + } + else + { + var localPath = _fallbackPath + requestedUri.AbsolutePath.Replace('/', '\\'); - if (requestedUri.Scheme == "file") + if (File.Exists(localPath)) { - 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; - } - } - } - else if (requestedUri.Scheme == "https") - { - var localPath = $"{requestedUri.Authority}:{requestedUri.AbsolutePath}".Replace('/', '\\'); - - if (localPath.StartsWith(_fallbackPath, StringComparison.OrdinalIgnoreCase)) - { - if (File.Exists(localPath)) - { - var fileStream = File.OpenRead(localPath); - var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( - fileStream, 200, "OK", MimeTypes.GetContentType() + "\r\nAccess-Control-Allow-Origin: *"); - args.Response = response; - } - } + var fileStream = File.OpenRead(localPath); + var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( + fileStream, 200, "OK", MimeTypes.GetContentTypeHeader()); + args.Response = response; } } - catch (Exception e) + } + else if (requestedUri.Scheme == "https" || requestedUri.Scheme == "http") + { + var localPath = Uri.UnescapeDataString($"{requestedUri.Authority}:{requestedUri.AbsolutePath}".Replace('/', '\\')); + + if (localPath.StartsWith(_fallbackPath, StringComparison.OrdinalIgnoreCase)) { - // We don't need to feel burdened by any exceptions - Debug.WriteLine(e); + if (File.Exists(localPath)) + { + var fileStream = File.OpenRead(localPath); + var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse( + fileStream, 200, "OK", + $""" + Access-Control-Allow-Origin: * + Content-Type: {MimeTypes.GetMimeType()} + """ + ); + args.Response = response; + } } - }; + } + } + catch (Exception e) + { + // We don't need to feel burdened by any exceptions + Debug.WriteLine(e); } } @@ -191,14 +188,16 @@ public class SvgImagePanel : WebpagePanel { public const string Html = "text/html"; public const string JavaScript = "application/javascript"; + public const string Json = "application/json"; 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 GetContentTypeHeader(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 + ".json" => Json, ".css" => Css, ".html" => Html, _ => Binary, @@ -217,11 +216,6 @@ public sealed class ScriptHandler(string path) return await Task.FromResult(new Uri(Path).AbsolutePath); } - public async Task GetUri() - { - return await Task.FromResult(new Uri(Path).AbsoluteUri); - } - public async Task GetSvgContent() { if (File.Exists(Path)) diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svg/SvgMetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svg/SvgMetaProvider.cs new file mode 100644 index 0000000..e0a8132 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svg/SvgMetaProvider.cs @@ -0,0 +1,93 @@ +// 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.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; +using System.Windows; +using System.Xml.Linq; + +namespace QuickLook.Plugin.ImageViewer.Webview.Svg; + +public class SvgMetaProvider(string path) : IWebMetaProvider +{ + private readonly string _path = path; + private Size _size = Size.Empty; + + public Size GetSize() + { + if (_size != Size.Empty) + { + return _size; + } + + if (!File.Exists(_path)) + { + return _size; + } + + try + { + var svgContent = File.ReadAllText(_path); + var svg = XElement.Parse(svgContent); + XNamespace ns = svg.Name.Namespace; + + string widthAttr = svg.Attribute("width")?.Value; + string heightAttr = svg.Attribute("height")?.Value; + + float? width = TryParseSvgLength(widthAttr); + float? height = TryParseSvgLength(heightAttr); + + if (width.HasValue && height.HasValue) + { + _size = new Size { Width = width.Value, Height = height.Value }; + } + + string viewBoxAttr = svg.Attribute("viewBox")?.Value; + if (!string.IsNullOrEmpty(viewBoxAttr)) + { + var parts = viewBoxAttr.Split([' ', ','], StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 4 && + float.TryParse(parts[2], out float vbWidth) && + float.TryParse(parts[3], out float vbHeight)) + { + _size = new Size { Width = vbWidth, Height = vbHeight }; + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + return _size; + } + + private static float? TryParseSvgLength(string input) + { + if (string.IsNullOrEmpty(input)) + return null; + + var match = Regex.Match(input.Trim(), @"^([\d.]+)(px|pt|mm|cm|in|em|ex|%)?$", RegexOptions.IgnoreCase); + if (match.Success && float.TryParse(match.Groups[1].Value, out float value)) + { + return value; + } + return null; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svga/SvgaImagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svga/SvgaImagePanel.cs new file mode 100644 index 0000000..9ef0cf1 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svga/SvgaImagePanel.cs @@ -0,0 +1,35 @@ +// 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 QuickLook.Plugin.ImageViewer.Webview.Svg; +using System; +using System.IO; + +namespace QuickLook.Plugin.ImageViewer.Webview.Svga; + +public class SvgaImagePanel : SvgImagePanel +{ + public override void Preview(string path) + { + FallbackPath = Path.GetDirectoryName(path); + + ObjectForScripting ??= new ScriptHandler(path); + + _homePage = _resources["/svga2html.html"]; + NavigateToUri(new Uri("file://quicklook/")); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svga/SvgaMetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svga/SvgaMetaProvider.cs new file mode 100644 index 0000000..aad5382 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/Svga/SvgaMetaProvider.cs @@ -0,0 +1,44 @@ +// 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.IO; +using System.Windows; + +namespace QuickLook.Plugin.ImageViewer.Webview.Svga; + +internal class SvgaMetaProvider(string path) : IWebMetaProvider +{ + private readonly string _path = path; + private Size _size = Size.Empty; + + public Size GetSize() + { + if (_size != Size.Empty) + { + return _size; + } + + if (!File.Exists(_path)) + { + return _size; + } + + // TODO + + return new Size(800, 600); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/WebHandler.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/WebHandler.cs new file mode 100644 index 0000000..ba4dd01 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Webview/WebHandler.cs @@ -0,0 +1,123 @@ +// 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 QuickLook.Common.Helpers; +using QuickLook.Common.Plugin; +using QuickLook.Plugin.ImageViewer.Webview.Lottie; +using QuickLook.Plugin.ImageViewer.Webview.Svg; +using QuickLook.Plugin.ImageViewer.Webview.Svga; +using System; +using System.IO; +using System.Windows; + +namespace QuickLook.Plugin.ImageViewer.Webview; + +internal static class WebHandler +{ + public static bool TryCanHandle(string path) + { + if (path.EndsWith(".lottie.json", StringComparison.OrdinalIgnoreCase)) + return true; + + return Path.GetExtension(path).ToLower() switch + { + ".svg" => SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer"), + ".svga" or ".lottie" => true, + ".json" => LottieDetector.IsVaild(path), // Check for Lottie files + _ => false, + }; + } + + public static bool TryPrepare(string path, ContextObject context, out IWebMetaProvider metaWeb) + { + string ext = Path.GetExtension(path).ToLower(); + + if (ext == ".svg" || ext == ".svga" + || ext == ".lottie" || ext == ".json") + { + if (ext == ".svg") + { + if (!SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer")) + { + metaWeb = null; + return false; + } + } + + metaWeb = ext switch + { + ".svg" => new SvgMetaProvider(path), + ".svga" => new SvgaMetaProvider(path), + ".lottie" or ".json" => new LottieMetaProvider(path), + _ => throw new NotSupportedException($"Unsupported file type: {ext}") + }; + var sizeSvg = metaWeb.GetSize(); + + if (!sizeSvg.IsEmpty) + context.SetPreferredSizeFit(sizeSvg, 0.8d); + else + context.PreferredSize = new Size(800, 600); + + context.Theme = (Themes)SettingHelper.Get("LastTheme", 1, "QuickLook.Plugin.ImageViewer"); + return true; + } + + metaWeb = null; + return false; + } + + public static bool TryView(string path, ContextObject context, IWebMetaProvider metaWeb, out IWebImagePanel ipWeb) + { + string ext = Path.GetExtension(path).ToLower(); + + if (ext == ".svg" || ext == ".svga" + || ext == ".lottie" || ext == ".json") + { + if (ext == ".svg") + { + if (!SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer")) + { + ipWeb = null; + return false; + } + } + + ipWeb = ext switch + { + ".svg" => new SvgImagePanel(), + ".svga" => new SvgaImagePanel(), + ".lottie" or ".json" => new LottieImagePanel(), + _ => throw new NotSupportedException($"Unsupported file type: {ext}") + }; + + ipWeb.Preview(path); + + var sizeSvg = metaWeb.GetSize(); + + context.ViewerContent = ipWeb; + context.Title = sizeSvg.IsEmpty + ? $"{Path.GetFileName(path)}" + : $"{sizeSvg.Width}×{sizeSvg.Height}: {Path.GetFileName(path)}"; + + context.IsBusy = false; + return true; + } + + ipWeb = null; + return false; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs index 0189a3e..3576856 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs @@ -83,55 +83,47 @@ public class MarkdownPanel : WebpagePanel return html; } - protected override void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e) + protected override void WebView_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs args) { - if (e.IsSuccess) + Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}"); + + try { - _webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + var requestedUri = new Uri(args.Request.Uri); - _webView.CoreWebView2.WebResourceRequested += (sender, args) => + if (requestedUri.Scheme == "file") { - Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}"); - - try + if (requestedUri.AbsolutePath == "/") { - var requestedUri = new Uri(args.Request.Uri); + 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 (requestedUri.Scheme == "file") + if (File.Exists(localPath)) { - 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; - } - } + 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); - } - }; + } + } + catch (Exception e) + { + // We don't need to feel burdened by any exceptions + Debug.WriteLine(e); } }