mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-11 17:59:17 +00:00
Add Lottie Files animation preview support
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -55,6 +55,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
|
||||
<PackageReference Include="QuickLook.ImageGlass.WebP" Version="1.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
@@ -91,7 +92,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\svg*">
|
||||
<EmbeddedResource Include="Resources\Web\*">
|
||||
<LogicalName>QuickLook.Plugin.ImageViewer.Resources.%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LottieFiles Preview</title>
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height:100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#bm {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bm"></div>
|
||||
<script src="https://unpkg.com/lottie-web/build/player/lottie.min.js"></script>
|
||||
<script src="lottie2html.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* SvgaViewer: Provides SVGA animation preview with the following features.
|
||||
*
|
||||
* Requirements:
|
||||
* - Requires the following HTML structure:
|
||||
* <canvas id="canvas">
|
||||
* </canvas>
|
||||
* - 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();
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
|
||||
namespace QuickLook.Plugin.ImageViewer.Webview;
|
||||
|
||||
internal interface IWebImagePanel : IDisposable
|
||||
{
|
||||
public void Preview(string path);
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Windows;
|
||||
|
||||
namespace QuickLook.Plugin.ImageViewer.Webview;
|
||||
|
||||
internal interface IWebMetaProvider
|
||||
{
|
||||
public Size GetSize();
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<Dictionary<string, object>>(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;
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<string> 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<Dictionary<string, object>>(content);
|
||||
|
||||
if (manifestJson.ContainsKey("animations"))
|
||||
{
|
||||
object animations = manifestJson["animations"];
|
||||
|
||||
if (manifestJson["animations"] is IEnumerable<object> animationsEnumerable)
|
||||
{
|
||||
foreach (var animationsItem in animationsEnumerable.ToArray())
|
||||
{
|
||||
if (animationsItem is Dictionary<string, object> 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;
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<Dictionary<string, object>>(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);
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<List<string>> splitArrayPool = null!;
|
||||
[ThreadStatic] private static StringBuilder stringBuilder = null!;
|
||||
[ThreadStatic] private static Dictionary<Type, Dictionary<string, FieldInfo>> fieldInfoCache = null!;
|
||||
[ThreadStatic] private static Dictionary<Type, Dictionary<string, PropertyInfo>> propertyInfoCache = null!;
|
||||
|
||||
public static T Parse<T>(string json)
|
||||
{
|
||||
// Initialize, if needed, the ThreadStatic variables
|
||||
propertyInfoCache ??= [];
|
||||
fieldInfoCache ??= [];
|
||||
stringBuilder ??= new StringBuilder();
|
||||
splitArrayPool ??= new Stack<List<string>>();
|
||||
|
||||
//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 { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
static List<string> Split(string json)
|
||||
{
|
||||
List<string> 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<string> 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<string> 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<string> 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<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
return null!;
|
||||
var dict = new Dictionary<string, object>(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<string> items = Split(json);
|
||||
var finalList = new List<object>(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<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
|
||||
{
|
||||
Dictionary<string, T> 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<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
return instance;
|
||||
|
||||
if (!fieldInfoCache.TryGetValue(type, out Dictionary<string, FieldInfo> nameToField))
|
||||
{
|
||||
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
fieldInfoCache.Add(type, nameToField);
|
||||
}
|
||||
if (!propertyInfoCache.TryGetValue(type, out Dictionary<string, PropertyInfo> 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;
|
||||
}
|
||||
}
|
@@ -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<string, byte[]> _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<string> GetUri()
|
||||
{
|
||||
return await Task.FromResult(new Uri(Path).AbsoluteUri);
|
||||
}
|
||||
|
||||
public async Task<string> GetSvgContent()
|
||||
{
|
||||
if (File.Exists(Path))
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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/"));
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user