Browse Source

Add animated webp rendering

pull/438/head
Ionite 11 months ago
parent
commit
280fc8c89f
No known key found for this signature in database
  1. 1
      Avalonia.Gif/Avalonia.Gif.csproj
  2. 9
      Avalonia.Gif/GifImage.cs
  3. 2
      Avalonia.Gif/GifInstance.cs
  4. 15
      Avalonia.Gif/IGifInstance.cs
  5. 180
      Avalonia.Gif/WebpInstance.cs
  6. 8
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  7. 2
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
  8. 2
      StabilityMatrix.Core/Models/Database/LocalImageFile.cs

1
Avalonia.Gif/Avalonia.Gif.csproj

@ -11,6 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="SkiaSharp" Version="2.88.6" />
</ItemGroup>
</Project>

9
Avalonia.Gif/GifImage.cs

@ -32,7 +32,7 @@ namespace Avalonia.Gif
IterationCount
>("IterationCount", IterationCount.Infinite);
private GifInstance? _gifInstance;
private IGifInstance? _gifInstance;
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty = AvaloniaProperty.Register<
GifImage,
@ -166,7 +166,7 @@ namespace Avalonia.Gif
{
private TimeSpan _animationElapsed;
private TimeSpan? _lastServerTime;
private GifInstance? _currentInstance;
private IGifInstance? _currentInstance;
private bool _running;
public static readonly object StopMessage = new(),
@ -184,7 +184,7 @@ namespace Avalonia.Gif
{
_running = false;
}
else if (message is GifInstance instance)
else if (message is IGifInstance instance)
{
_currentInstance?.Dispose();
_currentInstance = instance;
@ -288,7 +288,8 @@ namespace Avalonia.Gif
private void UpdateGifInstance(object source)
{
_gifInstance?.Dispose();
_gifInstance = new GifInstance(source);
_gifInstance = new WebpInstance(source);
// _gifInstance = new GifInstance(source);
_gifInstance.IterationCount = IterationCount;
_customVisual?.SendHandlerMessage(_gifInstance);
}

2
Avalonia.Gif/GifInstance.cs

@ -11,7 +11,7 @@ using Avalonia.Platform;
namespace Avalonia.Gif
{
public class GifInstance : IDisposable
public class GifInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;

15
Avalonia.Gif/IGifInstance.cs

@ -0,0 +1,15 @@
using Avalonia.Animation;
using Avalonia.Media.Imaging;
namespace Avalonia.Gif;
public interface IGifInstance : IDisposable
{
IterationCount IterationCount { get; set; }
bool AutoStart { get; }
CancellationTokenSource CurrentCts { get; }
int GifFrameCount { get; }
PixelSize GifPixelSize { get; }
bool IsDisposed { get; }
WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed);
}

180
Avalonia.Gif/WebpInstance.cs

@ -0,0 +1,180 @@
using Avalonia.Animation;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Gif;
public class WebpInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;
private readonly WriteableBitmap? _targetBitmap;
private TimeSpan _totalTime;
private readonly List<TimeSpan> _frameTimes;
private uint _iterationCount;
private int _currentFrameIndex;
private SKCodec? _codec;
public CancellationTokenSource CurrentCts { get; }
internal WebpInstance(object newValue)
: this(
newValue switch
{
Stream s => s,
Uri u => GetStreamFromUri(u),
string str => GetStreamFromString(str),
_ => throw new InvalidDataException("Unsupported source object")
}
) { }
public WebpInstance(string uri)
: this(GetStreamFromString(uri)) { }
public WebpInstance(Uri uri)
: this(GetStreamFromUri(uri)) { }
public WebpInstance(Stream currentStream)
{
if (!currentStream.CanSeek)
throw new InvalidDataException("The provided stream is not seekable.");
if (!currentStream.CanRead)
throw new InvalidOperationException("Can't read the stream provided.");
currentStream.Seek(0, SeekOrigin.Begin);
CurrentCts = new CancellationTokenSource();
var managedStream = new SKManagedStream(currentStream);
_codec = SKCodec.Create(managedStream);
var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height);
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
GifPixelSize = pixSize;
_totalTime = TimeSpan.Zero;
_frameTimes = _codec
.FrameInfo
.Select(frame =>
{
_totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration));
return _totalTime;
})
.ToList();
RenderFrame(_codec, _targetBitmap, 0);
}
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index)
{
codec.GetFrameInfo(index, out var frameInfo);
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
using var frameBuffer = targetBitmap.Lock();
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index));
if (result != SKCodecResult.Success)
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
}
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex)
{
codec.GetFrameInfo(index, out var frameInfo);
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
using var frameBuffer = targetBitmap.Lock();
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex));
if (result != SKCodecResult.Success)
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
}
private static Stream GetStreamFromString(string str)
{
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
{
throw new InvalidCastException("The string provided can't be converted to URI.");
}
return GetStreamFromUri(res);
}
private static Stream GetStreamFromUri(Uri uri)
{
var uriString = uri.OriginalString.Trim();
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
{
return new FileStream(uriString, FileMode.Open, FileAccess.Read);
}
return AssetLoader.Open(uri);
}
public int GifFrameCount => _frameTimes.Count;
public PixelSize GifPixelSize { get; }
public void Dispose()
{
IsDisposed = true;
CurrentCts.Cancel();
_targetBitmap?.Dispose();
_codec?.Dispose();
}
public bool IsDisposed { get; private set; }
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
{
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
{
return null;
}
if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
{
return null;
}
var elapsedTicks = stopwatchElapsed.Ticks;
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
var currentFrame = _frameTimes.IndexOf(targetFrame);
if (currentFrame == -1)
currentFrame = 0;
if (_currentFrameIndex == currentFrame)
return _targetBitmap;
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
return ProcessFrameIndex(currentFrame);
}
internal WriteableBitmap ProcessFrameIndex(int frameIndex)
{
if (_codec is null)
throw new InvalidOperationException("The codec is null.");
if (_targetBitmap is null)
throw new InvalidOperationException("The target bitmap is null.");
RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex);
_currentFrameIndex = frameIndex;
return _targetBitmap;
}
}

8
StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

@ -409,14 +409,14 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
);
// convert to gif
var inputStream = File.OpenRead(webpFilePath);
/*var inputStream = File.OpenRead(webpFilePath);
var gifFilePath = webpFilePath.ToString().Replace(".webp", ".gif");
var outputStream = File.OpenWrite(gifFilePath);
await GifConverter.ConvertAnimatedWebpToGifAsync(inputStream, outputStream);
await inputStream.DisposeAsync();
await outputStream.FlushAsync();
await outputStream.DisposeAsync();
await outputStream.DisposeAsync();*/
// if (File.Exists(gifFilePath))
// {
@ -424,8 +424,8 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
// File.Delete(webpFilePath);
// }
outputImages.Add(new ImageSource(gifFilePath));
EventManager.Instance.OnImageFileAdded(gifFilePath);
outputImages.Add(new ImageSource(webpFilePath));
EventManager.Instance.OnImageFileAdded(webpFilePath);
}
else
{

2
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs

@ -130,7 +130,7 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
private void OnImageFileAdded(object? sender, FilePath e)
{
if (!e.Extension.Contains("gif"))
if (!e.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
return;
OutputUri = e;

2
StabilityMatrix.Core/Models/Database/LocalImageFile.cs

@ -104,5 +104,5 @@ public record LocalImageFile
};
}
public static readonly HashSet<string> SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif"];
public static readonly HashSet<string> SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
}

Loading…
Cancel
Save