Browse Source

Merge pull request #473 from ionite34/fix-webp

Add animated imagesource template parsing
pull/438/head
Ionite 10 months ago committed by GitHub
parent
commit
436003d183
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 12
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
  3. 74
      StabilityMatrix.Avalonia/Models/ImageSource.cs
  4. 10
      StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
  5. 57
      StabilityMatrix.Core/Helper/Webp/WebpReader.cs

5
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

12
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml

@ -15,7 +15,10 @@
x:DataType="models:ImageSource"
mc:Ignorable="d">
<Grid>
<ContentPresenter Content="{Binding}">
<!-- Tag is not used but sets TemplateKey which is used to select the DataTemplate later -->
<ContentPresenter
Tag="{Binding TemplateKeyAsync^}"
Content="{Binding}">
<ContentPresenter.ContentTemplate>
<controls:DataTemplateSelector x:TypeArguments="models:ImageSourceTemplateType">
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.WebpAnimation}" DataType="models:ImageSource">
@ -42,6 +45,13 @@
</controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Default}" DataType="models:ImageSource">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Unsupported Format"/>
</DataTemplate>
</controls:DataTemplateSelector>
</ContentPresenter.ContentTemplate>
</ContentPresenter>

74
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<ImageSourceTemplateType>
}
/// <inheritdoc />
public ImageSourceTemplateType TemplateKey
public ImageSourceTemplateType TemplateKey { get; private set; }
private async Task<bool> 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<IHttpClientFactory>();
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<ImageSourceTemplateType> GetOrRefreshTemplateKeyAsync()
{
if (TemplateKey is ImageSourceTemplateType.Default)
{
await TryRefreshTemplateKeyAsync();
}
return TemplateKey;
}
[JsonIgnore]
public Task<ImageSourceTemplateType> TemplateKeyAsync => GetOrRefreshTemplateKeyAsync();
[JsonIgnore]
public Task<Bitmap?> BitmapAsync => GetBitmapAsync();

10
StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml

@ -1,4 +1,5 @@
<controls:UserControlBase
Focusable="True"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.ImageViewerDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -55,7 +56,9 @@
Command="{Binding CopyImageCommand}"/>
</Grid.Resources>
<!-- Tag is not used but sets TemplateKey which is used to select the DataTemplate later -->
<ContentPresenter Grid.Row="0"
Tag="{Binding TemplateKeyAsync^}"
DataContext="{Binding ImageSource}"
Content="{Binding }">
<ContentPresenter.ContentTemplate>
@ -86,6 +89,13 @@
</controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Default}" DataType="models:ImageSource">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Unsupported Format"/>
</DataTemplate>
</controls:DataTemplateSelector>
</ContentPresenter.ContentTemplate>
</ContentPresenter>

57
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);
}
}
Loading…
Cancel
Save