Add Lottie Files animation preview support

This commit is contained in:
ema
2025-07-05 09:25:22 +08:00
parent 5e459e35e5
commit 3fce8b4f53
24 changed files with 1237 additions and 313 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();

View File

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

View File

@@ -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);
}

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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))

View File

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

View File

@@ -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/"));
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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);
}
}