Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<!--<script src="https://unpkg.com/svga/dist/index.min.js"></script>-->
<!--<script src="svga/index.min.js"></script>-->
<!--<script src="https://unpkg.com/svgaplayerweb/build/svga.min.js"></script>-->
<script src="svgaplayerweb/jszip.min.js"></script>
<script src="svgaplayerweb/jszip-utils.min.js"></script>
<script src="svgaplayerweb/svga.min.js"></script>
<script src="js/svga2html.js"></script>
</body>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.

using Com.Opensource.Svga;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;

namespace QuickLook.Plugin.ImageViewer.Webview.Svga;

Expand All @@ -33,6 +36,11 @@ public partial class SvgaPlayer
/// </summary>
private byte[] _inflatedBytes;

/// <summary>
/// Whether the data is in JSON format (SVGA 1.x) or protobuf format (SVGA 2.x)
/// </summary>
private bool _isJsonFormat;

/// <summary>
/// SVGA configuration parameters.
/// </summary>
Expand Down Expand Up @@ -132,46 +140,192 @@ public float StageHeight
}

/// <summary>
/// Inflate the SVGA file to get its original data
/// The SVGA file has been deflated, so the first step is to inflate it
/// Check if the stream is a ZIP archive (SVGA 1.x format)
/// ZIP files start with PK header (0x50, 0x4B)
/// </summary>
private void InflateSvgaFile(Stream svgaFileBuffer)
private static bool IsZipArchive(Stream stream)
{
var originalPosition = stream.Position;
stream.Seek(0, SeekOrigin.Begin);

var header = new byte[2];
var bytesRead = stream.Read(header, 0, 2);

stream.Seek(originalPosition, SeekOrigin.Begin);

return bytesRead == 2 && header[0] == 0x50 && header[1] == 0x4B; // PK
}

/// <summary>
/// Check if the data is JSON format (SVGA 1.x)
/// Handles UTF-8 BOM and leading whitespace
/// </summary>
private static bool IsJsonPayload(byte[] data)
{
int i = 0;

// Skip UTF-8 BOM (EF BB BF)
if (data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
{
i = 3;
}

// Skip leading whitespace
while (i < data.Length && (data[i] == ' ' || data[i] == '\t' || data[i] == '\r' || data[i] == '\n'))
{
i++;
}

// Check if first non-whitespace byte is '{'
return i < data.Length && data[i] == '{';
}

/// <summary>
/// Extract SVGA data from ZIP archive (SVGA 1.x format)
/// SVGA 1.x stores JSON data in "movie.spec" file
/// </summary>
private (byte[] data, bool isJson) ExtractFromZip(Stream svgaFileBuffer)
{
byte[] inflatedBytes;
svgaFileBuffer.Seek(0, SeekOrigin.Begin);

// The built-in DeflateStream in Microsoft .NET does not recognize the first two bytes of the file header. For SVGA, these two bytes are 78 9C, which is the default compression indicator for Deflate
// For more information, see https://stackoverflow.com/questions/17212964/net-zlib-inflate-with-net-4-5
// For Zlib file header, see https://stackoverflow.com/questions/9050260/what-does-a-zlib-header-look-like
svgaFileBuffer.Seek(2, SeekOrigin.Begin);
using var archive = new ZipArchive(svgaFileBuffer, ZipArchiveMode.Read, leaveOpen: true);

// SVGA 1.x stores the JSON data in "movie.spec"
foreach (var entry in archive.Entries)
{
if (entry.Name.Equals("movie.spec", StringComparison.OrdinalIgnoreCase))
{
using var entryStream = entry.Open();
using var memoryStream = new MemoryStream();
entryStream.CopyTo(memoryStream);
var data = memoryStream.ToArray();

// Check if it's JSON or protobuf
// Skip UTF-8 BOM (EF BB BF) and leading whitespace
bool isJson = IsJsonPayload(data);
return (data, isJson);
}
}

using (var deflatedStream = new DeflateStream(svgaFileBuffer, CompressionMode.Decompress))
// Fallback: try to find any .spec file
foreach (var entry in archive.Entries)
{
using var stream = new MemoryStream();
deflatedStream.CopyTo(stream);
inflatedBytes = stream.ToArray();
if (entry.Name.EndsWith(".spec", StringComparison.OrdinalIgnoreCase))
{
using var entryStream = entry.Open();
using var memoryStream = new MemoryStream();
entryStream.CopyTo(memoryStream);
var data = memoryStream.ToArray();
bool isJson = IsJsonPayload(data);
return (data, isJson);
}
}

_inflatedBytes = inflatedBytes;
throw new InvalidDataException("No valid SVGA data found in ZIP archive");
}

/// <summary>
/// Inflate the SVGA file to get its original data
/// Supports both SVGA 1.x (ZIP/JSON) and 2.x (zlib/protobuf) formats
/// </summary>
private void InflateSvgaFile(Stream svgaFileBuffer)
{
if (IsZipArchive(svgaFileBuffer))
{
// SVGA 1.x format: ZIP archive containing JSON data
var (data, isJson) = ExtractFromZip(svgaFileBuffer);
_inflatedBytes = data;
_isJsonFormat = isJson;
}
else
{
// SVGA 2.x format: zlib compressed protobuf data
// The built-in DeflateStream in Microsoft .NET does not recognize the first two bytes of the file header. For SVGA, these two bytes are 78 9C, which is the default compression indicator for Deflate
// For more information, see https://stackoverflow.com/questions/17212964/net-zlib-inflate-with-net-4-5
// For Zlib file header, see https://stackoverflow.com/questions/9050260/what-does-a-zlib-header-look-like
svgaFileBuffer.Seek(2, SeekOrigin.Begin);

using (var deflatedStream = new DeflateStream(svgaFileBuffer, CompressionMode.Decompress))
{
using var stream = new MemoryStream();
deflatedStream.CopyTo(stream);
_inflatedBytes = stream.ToArray();
}
_isJsonFormat = false;
}
}

/// <summary>
/// Get the SVGA MovieEntity from the inflated data
/// Supports both JSON (SVGA 1.x) and protobuf (SVGA 2.x) formats
/// </summary>
/// <param name="inflatedBytes"></param>
private void InitMovieEntity()
{
if (_inflatedBytes == null)
{
return;
}

var moveEntity = MovieEntity.Parser.ParseFrom(_inflatedBytes);
_movieParams = moveEntity.Params;
_sprites = [.. moveEntity.Sprites];
TotalFrame = moveEntity.Params.Frames;
SpriteCount = _sprites.Count;
StageWidth = _movieParams.ViewBoxWidth;
StageHeight = _movieParams.ViewBoxHeight;
if (_isJsonFormat)
{
// SVGA 1.x: JSON format
InitMovieEntityFromJson();
}
else
{
// SVGA 2.x: Protobuf format
var moveEntity = MovieEntity.Parser.ParseFrom(_inflatedBytes);
_movieParams = moveEntity.Params;
_sprites = [.. moveEntity.Sprites];
TotalFrame = moveEntity.Params.Frames;
SpriteCount = _sprites.Count;
StageWidth = _movieParams.ViewBoxWidth;
StageHeight = _movieParams.ViewBoxHeight;
}
}

/// <summary>
/// Parse SVGA 1.x JSON format
/// </summary>
private void InitMovieEntityFromJson()
{
var json = System.Text.Encoding.UTF8.GetString(_inflatedBytes);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;

if (root.TryGetProperty("movie", out var movie))
{
if (movie.TryGetProperty("viewBox", out var viewBox))
{
StageWidth = viewBox.GetProperty("width").GetSingle();
StageHeight = viewBox.GetProperty("height").GetSingle();
}

if (movie.TryGetProperty("frames", out var frames))
{
TotalFrame = frames.GetInt32();
}

if (movie.TryGetProperty("fps", out var fps))
{
Fps = fps.GetInt32();
}
}

if (root.TryGetProperty("sprites", out var sprites))
{
SpriteCount = sprites.GetArrayLength();
}

// Create a minimal MovieParams for compatibility
_movieParams = new MovieParams
{
ViewBoxWidth = StageWidth,
ViewBoxHeight = StageHeight,
Frames = TotalFrame,
Fps = Fps
};
_sprites = new List<SpriteEntity>();
}

/// <summary>
Expand Down