Skip to content

Commit

Permalink
feat(skia): Gif support
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Jun 11, 2024
1 parent f2534be commit 47a8936
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 15 deletions.
145 changes: 145 additions & 0 deletions src/Uno.UI.Composition/Composition/ImageFrameProvider.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#nullable enable

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using SkiaSharp;

namespace Microsoft.UI.Composition;

internal sealed class ImageFrameProvider : IDisposable
{
private readonly SKImage[] _images;
private readonly SKCodecFrameInfo[]? _frameInfos;
private readonly Timer? _timer;
private readonly Stopwatch? _stopwatch;
private readonly long _totalDuration;
private readonly Action? _onFrameChanged;

private int _currentFrame;
private bool _disposed;

private ImageFrameProvider(SKImage[] images, SKCodecFrameInfo[]? frameInfos, long totalDuration, Action? onFrameChanged)
{
_images = images;
_frameInfos = frameInfos;
_totalDuration = totalDuration;
_onFrameChanged = onFrameChanged;
Debug.Assert(frameInfos is not null || images.Length == 1);
Debug.Assert(totalDuration != 0 || images.Length == 1);
Debug.Assert(onFrameChanged is not null || images.Length == 1);

if (_images.Length == 0)
{
throw new ArgumentException("Images array shouldn't be empty");
}

if (_images.Length > 1)
{
_stopwatch = Stopwatch.StartNew();
_timer = new Timer(OnTimerCallback, null, dueTime: _frameInfos![0].Duration, period: Timeout.Infinite);
}
}

public SKImage? CurrentImage => _images[_currentFrame];

private int GetCurrentFrameIndex()
{
var currentTimestampInMilliseconds = _stopwatch!.ElapsedMilliseconds % _totalDuration;
for (int i = 0; i < _frameInfos!.Length; i++)
{
if (currentTimestampInMilliseconds < _frameInfos[i].Duration)
{
return i;
}

currentTimestampInMilliseconds -= _frameInfos[i].Duration;
}

throw new InvalidOperationException("This shouldn't be reachable. A timestamp in total duration range should map to a frame");
}

private void SetCurrentFrame()
{
var frameIndex = GetCurrentFrameIndex();
if (_currentFrame != frameIndex)
{
_currentFrame = frameIndex;
Debug.Assert(_onFrameChanged is not null);
_onFrameChanged();
}
}

private void OnTimerCallback(object? state)
{
SetCurrentFrame();

var timestamp = _stopwatch!.ElapsedMilliseconds % _totalDuration;
var nextFrameTimeStamp = 0;
for (int i = 0; i <= _currentFrame; i++)
{
nextFrameTimeStamp += _frameInfos![i].Duration;
}

var dueTime = nextFrameTimeStamp - timestamp;
if (dueTime < 0)
{
// Defensive check. When pausing the program for debugging, the calculations can go wrong.
dueTime = 16;
}

try
{
_timer!.Change(dueTime, period: Timeout.Infinite);
}
catch (ObjectDisposedException)
{
}
}

public static bool TryCreate(SKCodec codec, Action onFrameChanged, [NotNullWhen(true)] out ImageFrameProvider? provider)
{
var imageInfo = codec.Info;
var frameInfos = codec.FrameInfo;
imageInfo = new SKImageInfo(imageInfo.Width, imageInfo.Height, SKColorType.Bgra8888, SKAlphaType.Premul);
var bitmap = new SKBitmap(imageInfo);
var images = GC.AllocateUninitializedArray<SKImage>(frameInfos.Length);
var totalDuration = 0;
for (int i = 0; i < frameInfos.Length; i++)
{
var options = new SKCodecOptions(i);
codec.GetPixels(imageInfo, bitmap.GetPixels(), options);
var currentBitmap = SKImage.FromBitmap(bitmap);
if (currentBitmap is null)
{
provider = null;
return false;
}

images[i] = currentBitmap;
totalDuration += frameInfos[i].Duration;
}

provider = new ImageFrameProvider(images, frameInfos, totalDuration, onFrameChanged);
return true;
}

public static ImageFrameProvider Create(SKImage image)
=> new([image], null, 0, null);

public void Dispose()
{
if (!_disposed)
{
_timer?.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}

~ImageFrameProvider()
{
Dispose();
}
}
47 changes: 32 additions & 15 deletions src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
#nullable enable


using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.UI.Dispatching;
using Windows.Graphics;

namespace Microsoft.UI.Composition
{
internal partial class SkiaCompositionSurface : CompositionObject, ICompositionSurface
{
// Don't use field directly. Instead, use Image property.
private SKImage? _image;
private ImageFrameProvider? _frameProvider;

public SKImage? Image
private ImageFrameProvider? FrameProvider
{
get => _image;
private set
get => _frameProvider;
set
{
_image = value;
OnPropertyChanged(nameof(Image), isSubPropertyChange: false);
_frameProvider?.Dispose();
_frameProvider = value;
}
}

public SKImage? Image => FrameProvider?.CurrentImage;

internal SkiaCompositionSurface(SKImage image)
{
Image = image;
_frameProvider = ImageFrameProvider.Create(image);
}

internal (bool success, object nativeResult) LoadFromStream(Stream imageStream) => LoadFromStream(null, null, imageStream);
Expand All @@ -54,7 +58,7 @@ internal SkiaCompositionSurface(SKImage image)

if (result == SKCodecResult.Success)
{
Image = SKImage.FromBitmap(bitmap);
FrameProvider = ImageFrameProvider.Create(SKImage.FromBitmap(bitmap));
}

return (result == SKCodecResult.Success || result == SKCodecResult.IncompleteInput, result);
Expand All @@ -63,13 +67,20 @@ internal SkiaCompositionSurface(SKImage image)
{
try
{
Image = SKImage.FromEncodedData(stream);
return Image is null
? (false, "Failed to decode image")
: (true, "Success");
using var codec = SKCodec.Create(stream);
var onFrameChanged = () => NativeDispatcher.Main.Enqueue(() => OnPropertyChanged(nameof(Image), isSubPropertyChange: false), NativeDispatcherPriority.High);
if (!ImageFrameProvider.TryCreate(codec, onFrameChanged, out var provider))
{
FrameProvider = null;
return (false, "Failed to decode image");
}

FrameProvider = provider;
return (true, "Success");
}
catch (Exception e)
{
FrameProvider = null;
return (false, e.Message);
}
}
Expand All @@ -84,8 +95,14 @@ internal unsafe void CopyPixels(int pixelWidth, int pixelHeight, ReadOnlyMemory<

using (var pData = data.Pin())
{
Image = SKImage.FromPixelCopy(info, (IntPtr)pData.Pointer, pixelWidth * 4);
FrameProvider = ImageFrameProvider.Create(SKImage.FromPixelCopy(info, (IntPtr)pData.Pointer, pixelWidth * 4));
}
}

private protected override void DisposeInternal()
{
base.DisposeInternal();
FrameProvider = null;
}
}
}
2 changes: 2 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/Image/Image.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ partial void OnSourceChanged(ImageSource newValue, bool forceReload)
_sourceDisposable.Disposable = null;
_lastMeasuredSize = default;
_imageSprite.Brush = null;
_currentSurface?.Dispose();
_currentSurface = null;
_pendingImageData = new();
InvalidateMeasure();
Expand Down Expand Up @@ -138,6 +139,7 @@ private void TryProcessPendingSource()
_pendingImageData = new();
if (currentData.HasData)
{
_currentSurface?.Dispose();
_currentSurface = currentData.CompositionSurface;
_surfaceBrush = Visual.Compositor.CreateSurfaceBrush(_currentSurface);
_surfaceBrush.UsePaintColorToColorSurface = MonochromeColor is not null;
Expand Down

0 comments on commit 47a8936

Please sign in to comment.