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

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

@@ -0,0 +1,228 @@
// 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 Microsoft.Web.WebView2.Wpf;
using QuickLook.Common.Helpers;
using QuickLook.Plugin.HtmlViewer;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace QuickLook.Plugin.ImageViewer.Webview.Svg;
public class SvgImagePanel : WebpagePanel, IWebImagePanel
{
protected const string _resourcePrefix = "QuickLook.Plugin.ImageViewer.Resources.";
protected internal static readonly Dictionary<string, byte[]> _resources = [];
protected byte[] _homePage;
private object _objectForScripting;
public object ObjectForScripting
{
get => _objectForScripting;
set
{
_objectForScripting = value;
_webView?.EnsureCoreWebView2Async()
.ContinueWith(_ =>
_webView?.Dispatcher.Invoke(() =>
_webView?.CoreWebView2.AddHostObjectToScript("external", value)
)
);
}
}
static SvgImagePanel()
{
InitializeResources();
}
protected override void InitializeComponent()
{
_webView = new WebView2()
{
CreationProperties = new CoreWebView2CreationProperties
{
UserDataFolder = Path.Combine(SettingHelper.LocalDataPath, @"WebView2_Data\"),
},
};
_webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
Content = _webView;
}
protected static void InitializeResources()
{
if (_resources.Any()) return;
var assembly = Assembly.GetExecutingAssembly();
foreach (var resourceName in assembly.GetManifestResourceNames())
{
if (!resourceName.StartsWith(_resourcePrefix)) continue;
var relativePath = resourceName.Substring(_resourcePrefix.Length);
if (relativePath.Equals("resources", StringComparison.OrdinalIgnoreCase)) continue;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null) continue;
var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
_resources.Add($"/{relativePath.Replace('\\', '/')}", memoryStream.ToArray());
}
}
public virtual void Preview(string path)
{
FallbackPath = Path.GetDirectoryName(path);
ObjectForScripting ??= new ScriptHandler(path);
_homePage = _resources["/svg2html.html"];
NavigateToUri(new Uri("file://quicklook/"));
}
protected override void WebView_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs args)
{
Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}");
try
{
var requestedUri = new Uri(args.Request.Uri);
if (requestedUri.Scheme == "file")
{
if (requestedUri.AbsolutePath == "/")
{
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
new MemoryStream(_homePage), 200, "OK", MimeTypes.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 (File.Exists(localPath))
{
var fileStream = File.OpenRead(localPath);
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
fileStream, 200, "OK", MimeTypes.GetContentTypeHeader());
args.Response = response;
}
}
}
else if (requestedUri.Scheme == "https" || requestedUri.Scheme == "http")
{
var localPath = Uri.UnescapeDataString($"{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",
$"""
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);
}
}
public static bool ContainsKey(string key)
{
return _resources.ContainsKey(key);
}
public static Stream ReadStream(string key)
{
byte[] bytes = _resources[key];
return new MemoryStream(bytes);
}
public static string ReadString(string key)
{
using var reader = new StreamReader(ReadStream(key), Encoding.UTF8);
return reader.ReadToEnd();
}
public static class MimeTypes
{
public const string Html = "text/html";
public const string JavaScript = "application/javascript";
public const string Json = "application/json";
public const string Css = "text/css";
public const string Binary = "application/octet-stream";
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,
};
}
}
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public sealed class ScriptHandler(string path)
{
public string Path { get; } = path;
public async Task<string> GetPath()
{
return await Task.FromResult(new Uri(Path).AbsolutePath);
}
public async Task<string> GetSvgContent()
{
if (File.Exists(Path))
{
var bytes = File.ReadAllBytes(Path);
return await Task.FromResult(Encoding.UTF8.GetString(bytes));
}
return string.Empty;
}
}

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