Add SVGA animation preview support

This commit is contained in:
ema
2025-07-05 06:46:52 +08:00
parent 3858e142e2
commit eecb56db14
6 changed files with 125 additions and 31 deletions

View File

@@ -48,7 +48,7 @@ public class Plugin : IViewer
".pbm", ".pcx", ".pef", ".pgm", ".png", ".pnm", ".ppm", ".psb", ".psd", ".ptx", ".pxn", ".pbm", ".pcx", ".pef", ".pgm", ".png", ".pnm", ".ppm", ".psb", ".psd", ".ptx", ".pxn",
".qoi", ".qoi",
".r3d", ".raf", ".raw", ".rw2", ".rwl", ".rwz", ".r3d", ".raf", ".raw", ".rw2", ".rwl", ".rwz",
".sr2", ".srf", ".srw", ".svg", ".svgz", ".sr2", ".srf", ".srw", ".svg", ".svga", ".svgz",
".tga", ".tif", ".tiff", ".tga", ".tif", ".tiff",
".wdp", ".webp", ".wmf", ".wdp", ".webp", ".wmf",
".x3f", ".xcf", ".xbm", ".xpm", ".x3f", ".xcf", ".xbm", ".xpm",
@@ -114,7 +114,8 @@ public class Plugin : IViewer
public void Prepare(string path, ContextObject context) public void Prepare(string path, ContextObject context)
{ {
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".svga", StringComparison.OrdinalIgnoreCase))
{ {
if (SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer")) if (SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer"))
{ {
@@ -145,7 +146,8 @@ public class Plugin : IViewer
public void View(string path, ContextObject context) public void View(string path, ContextObject context)
{ {
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".svga", StringComparison.OrdinalIgnoreCase))
{ {
if (SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer")) if (SettingHelper.Get("RenderSvgWeb", true, "QuickLook.Plugin.ImageViewer"))
{ {

View File

@@ -91,7 +91,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Resources\svg2html.*"> <EmbeddedResource Include="Resources\svg*">
<LogicalName>QuickLook.Plugin.ImageViewer.Resources.%(RecursiveDir)%(Filename)%(Extension)</LogicalName> <LogicalName>QuickLook.Plugin.ImageViewer.Resources.%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SVGA Preview</title>
<style>
html, body {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: #fff;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="https://unpkg.com/svga/dist/index.min.js"></script>
<script src="svga2html.js"></script>
</body>
</html>

View File

@@ -0,0 +1,39 @@
/**
* 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 SvgaViewer {
constructor() {
}
/**
* Play SVGA file.
* @async
*/
async play() {
const path = await chrome.webview.hostObjects.external.GetPath();
const parser = new SVGA.Parser();
// Because the path is a local file, we need to convert it to a URL format
parser.load('https://' + path).then(svga => {
const player = new SVGA.Player(document.getElementById('canvas'));
player.mount(svga).then(() => {
player.start();
});
});
}
}
// Create the SVGA viewer and play
new SvgaViewer().play();

View File

@@ -99,7 +99,7 @@ public class SvgImagePanel : WebpagePanel
ObjectForScripting ??= new ScriptHandler(path); ObjectForScripting ??= new ScriptHandler(path);
_homePage = _resources["/svg2html.html"]; _homePage = _resources[path.EndsWith(".svga", StringComparison.OrdinalIgnoreCase) ? "/svga2html.html" : "/svg2html.html"];
NavigateToUri(new Uri("file://quicklook/")); NavigateToUri(new Uri("file://quicklook/"));
} }
@@ -145,6 +145,21 @@ public class SvgImagePanel : WebpagePanel
} }
} }
} }
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;
}
}
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -197,6 +212,16 @@ public sealed class ScriptHandler(string path)
{ {
public string Path { get; } = path; public string Path { get; } = path;
public async Task<string> GetPath()
{
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() public async Task<string> GetSvgContent()
{ {
if (File.Exists(Path)) if (File.Exists(Path))

View File

@@ -23,38 +23,42 @@ public class SvgMetaProvider(string path)
{ {
return _size; return _size;
} }
try
if (_path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
{ {
var svgContent = File.ReadAllText(_path); try
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 }; var svgContent = File.ReadAllText(_path);
} var svg = XElement.Parse(svgContent);
XNamespace ns = svg.Name.Namespace;
string viewBoxAttr = svg.Attribute("viewBox")?.Value; string widthAttr = svg.Attribute("width")?.Value;
if (!string.IsNullOrEmpty(viewBoxAttr)) string heightAttr = svg.Attribute("height")?.Value;
{
var parts = viewBoxAttr.Split([' ', ','], StringSplitOptions.RemoveEmptyEntries); float? width = TryParseSvgLength(widthAttr);
if (parts.Length == 4 && float? height = TryParseSvgLength(heightAttr);
float.TryParse(parts[2], out float vbWidth) &&
float.TryParse(parts[3], out float vbHeight)) if (width.HasValue && height.HasValue)
{ {
_size = new Size { Width = vbWidth, Height = vbHeight }; _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)
catch (Exception e) {
{ Debug.WriteLine(e);
Debug.WriteLine(e); }
} }
return _size; return _size;