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/), 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). 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 ## v2.8.0-pre.2
### Added ### Added
- Added German language option, thanks to Mario da Graca for the translation - 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" x:DataType="models:ImageSource"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <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> <ContentPresenter.ContentTemplate>
<controls:DataTemplateSelector x:TypeArguments="models:ImageSourceTemplateType"> <controls:DataTemplateSelector x:TypeArguments="models:ImageSourceTemplateType">
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.WebpAnimation}" DataType="models:ImageSource"> <DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.WebpAnimation}" DataType="models:ImageSource">
@ -42,6 +45,13 @@
</controls:AdvancedImageBox.ContextFlyout> </controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox> </controls:AdvancedImageBox>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Default}" DataType="models:ImageSource">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Unsupported Format"/>
</DataTemplate>
</controls:DataTemplateSelector> </controls:DataTemplateSelector>
</ContentPresenter.ContentTemplate> </ContentPresenter.ContentTemplate>
</ContentPresenter> </ContentPresenter>

74
StabilityMatrix.Avalonia/Models/ImageSource.cs

@ -1,13 +1,16 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncImageLoader; using AsyncImageLoader;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Blake3; using Blake3;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Webp;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.Models;
@ -56,22 +59,79 @@ public record ImageSource : IDisposable, ITemplateKey<ImageSourceTemplateType>
} }
/// <inheritdoc /> /// <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 var httpClientFactory = App.Services.GetRequiredService<IHttpClientFactory>();
return ImageSourceTemplateType.WebpAnimation; 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] [JsonIgnore]
public Task<Bitmap?> BitmapAsync => GetBitmapAsync(); public Task<Bitmap?> BitmapAsync => GetBitmapAsync();

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

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