diff --git a/CHANGELOG.md b/CHANGELOG.md index fe56de6f..9d2273dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to Stability Matrix will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). +## v2.8.0-pre.3 +### Fixed +- Webp static images can now be shown alongside existing webp animation support +- Fixed image gallery arrow key navigation requiring clicking before responding + ## v2.8.0-pre.2 ### Added - Added German language option, thanks to Mario da Graca for the translation diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml index 73b43f25..4728d093 100644 --- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml +++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml @@ -15,7 +15,10 @@ x:DataType="models:ImageSource" mc:Ignorable="d"> - + + @@ -42,6 +45,13 @@ + + + + diff --git a/StabilityMatrix.Avalonia/Models/ImageSource.cs b/StabilityMatrix.Avalonia/Models/ImageSource.cs index f20d316a..973b0bf6 100644 --- a/StabilityMatrix.Avalonia/Models/ImageSource.cs +++ b/StabilityMatrix.Avalonia/Models/ImageSource.cs @@ -1,13 +1,16 @@ using System; using System.Diagnostics; using System.IO; +using System.Net.Http; using System.Text.Json.Serialization; using System.Threading.Tasks; using AsyncImageLoader; using Avalonia.Media.Imaging; using Blake3; +using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.Webp; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Avalonia.Models; @@ -56,22 +59,79 @@ public record ImageSource : IDisposable, ITemplateKey } /// - public ImageSourceTemplateType TemplateKey + public ImageSourceTemplateType TemplateKey { get; private set; } + + private async Task TryRefreshTemplateKeyAsync() { - get + if ((LocalFile?.Extension ?? Path.GetExtension(RemoteUrl?.ToString())) is not { } extension) + { + return false; + } + + if (extension.Equals(".webp", StringComparison.OrdinalIgnoreCase)) { - var ext = LocalFile?.Extension ?? Path.GetExtension(RemoteUrl?.ToString()); + if (LocalFile is not null && LocalFile.Exists) + { + await using var stream = LocalFile.Info.OpenRead(); + using var reader = new WebpReader(stream); + + try + { + TemplateKey = reader.GetIsAnimatedFlag() + ? ImageSourceTemplateType.WebpAnimation + : ImageSourceTemplateType.Image; + } + catch (InvalidDataException) + { + return false; + } + + return true; + } - if (ext is not null && ext.Equals(".webp", StringComparison.OrdinalIgnoreCase)) + if (RemoteUrl is not null) { - // TODO: Check if webp is animated - return ImageSourceTemplateType.WebpAnimation; + var httpClientFactory = App.Services.GetRequiredService(); + using var client = httpClientFactory.CreateClient(); + + try + { + await using var stream = await client.GetStreamAsync(RemoteUrl); + using var reader = new WebpReader(stream); + + TemplateKey = reader.GetIsAnimatedFlag() + ? ImageSourceTemplateType.WebpAnimation + : ImageSourceTemplateType.Image; + } + catch (Exception) + { + return false; + } + + return true; } - return ImageSourceTemplateType.Image; + return false; + } + + TemplateKey = ImageSourceTemplateType.Image; + + return true; + } + + public async Task GetOrRefreshTemplateKeyAsync() + { + if (TemplateKey is ImageSourceTemplateType.Default) + { + await TryRefreshTemplateKeyAsync(); } + + return TemplateKey; } + [JsonIgnore] + public Task TemplateKeyAsync => GetOrRefreshTemplateKeyAsync(); + [JsonIgnore] public Task BitmapAsync => GetBitmapAsync(); diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml index d39924d2..bafc43ad 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml @@ -1,4 +1,5 @@  + @@ -86,6 +89,13 @@ + + + + diff --git a/StabilityMatrix.Core/Helper/Webp/WebpReader.cs b/StabilityMatrix.Core/Helper/Webp/WebpReader.cs new file mode 100644 index 00000000..006fe1fc --- /dev/null +++ b/StabilityMatrix.Core/Helper/Webp/WebpReader.cs @@ -0,0 +1,57 @@ +using System.Text; + +namespace StabilityMatrix.Core.Helper.Webp; + +public class WebpReader(Stream stream) : BinaryReader(stream, Encoding.ASCII, true) +{ + private uint headerFileSize; + + public bool GetIsAnimatedFlag() + { + ReadHeader(); + + while (BaseStream.Position < headerFileSize) + { + if (ReadVoidChunk() is "ANMF" or "ANIM") + { + return true; + } + } + + return false; + } + + private void ReadHeader() + { + // RIFF + var riff = ReadBytes(4); + if (!riff.SequenceEqual([.."RIFF"u8])) + { + throw new InvalidDataException("Invalid RIFF header"); + } + + // Size: uint32 + headerFileSize = ReadUInt32(); + + // WEBP + var webp = ReadBytes(4); + if (!webp.SequenceEqual([.."WEBP"u8])) + { + throw new InvalidDataException("Invalid WEBP header"); + } + } + + // Read a single chunk and discard its contents + private string ReadVoidChunk() + { + // FourCC: 4 bytes in ASCII + var result = ReadBytes(4); + + // Size: uint32 + var size = ReadUInt32(); + + BaseStream.Seek(size, SeekOrigin.Current); + + return Encoding.ASCII.GetString(result); + } +}