Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(skia): Gif support #15951

Merged
merged 8 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/SamplesApp/UITests.Shared/UITests.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9612,6 +9612,7 @@
<Content Include="$(MSBuildThisFileDirectory)Assets\ResizedLargeWisteria.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\square100.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\testimage_exif_rotated.jpg" />
<Content Include="$(MSBuildThisFileDirectory)Assets\testimage_exif_rotated_different_dimensions.jpg" />
<Content Include="$(MSBuildThisFileDirectory)Assets\test_image_200_200.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\theme-dark\ThemeTestImage.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\theme-light\ThemeTestImage.png" />
Expand Down
107 changes: 107 additions & 0 deletions src/Uno.UI.Composition/Composition/FrameProviderFactory.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using SkiaSharp;

namespace Microsoft.UI.Composition;

internal static class FrameProviderFactory
{
public static IFrameProvider Create(SKImage image)
=> new SingleFrameProvider(image);

public static bool TryCreate(SKManagedStream stream, Action onFrameChanged, [NotNullWhen(true)] out IFrameProvider? provider)
{
using var codec = SKCodec.Create(stream);
var imageInfo = codec.Info;
var frameInfos = codec.FrameInfo;
imageInfo = new SKImageInfo(imageInfo.Width, imageInfo.Height, SKColorType.Bgra8888, SKAlphaType.Premul);
using var bitmap = new SKBitmap(imageInfo);

if (codec.FrameInfo.Length < 2)
{
// FrameInfo can be zero for single-frame images
codec.GetPixels(imageInfo, bitmap.GetPixels());
provider = new SingleFrameProvider(GetImage(bitmap, codec.EncodedOrigin));
return true;
}

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 = GetImage(bitmap, codec.EncodedOrigin);
if (currentBitmap is null)
{
provider = null;
return false;
}

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

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

private static SKImage GetImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
var info = bitmap.Info;
if (SkEncodedOriginSwapsWidthHeight(origin))
{
info = new SKImageInfo(info.Height, info.Width, SKColorType.Bgra8888, SKAlphaType.Premul);
}

var matrix = GetExifMatrix(origin, info.Width, info.Height);
if (matrix.IsIdentity)
{
return SKImage.FromBitmap(bitmap);
}

var newBitmap = new SKBitmap(info);
using var canvas = new SKCanvas(newBitmap);
canvas.SetMatrix(matrix);
canvas.DrawBitmap(bitmap, 0, 0);
return SKImage.FromBitmap(newBitmap);
}

// https://github.com/google/skia/blob/b20651c1aad43e3447830d6ce7a68ca507b398a4/include/codec/SkEncodedOrigin.h#L32-L42
private static SKMatrix GetExifMatrix(SKEncodedOrigin origin, int width, int height)
{
return origin switch
{
SKEncodedOrigin.TopLeft => SKMatrix.Identity,
SKEncodedOrigin.TopRight => new SKMatrix(-1, 0, width, 0, 1, 0, 0, 0, 1),
SKEncodedOrigin.BottomRight => new SKMatrix(-1, 0, width, 0, -1, height, 0, 0, 1),
SKEncodedOrigin.BottomLeft => new SKMatrix(1, 0, 0, 0, -1, height, 0, 0, 1),
SKEncodedOrigin.LeftTop => new SKMatrix(0, 1, 0, 1, 0, 0, 0, 0, 1),
SKEncodedOrigin.RightTop => new SKMatrix(0, -1, width, 1, 0, 0, 0, 0, 1),
SKEncodedOrigin.RightBottom => new SKMatrix(0, -1, width, -1, 0, height, 0, 0, 1),
SKEncodedOrigin.LeftBottom => new SKMatrix(0, 1, 0, -1, 0, height, 0, 0, 1),
_ => throw new ArgumentException($"Unexpected SKEncodedOrigin value '{origin}'.", nameof(origin)),
};
}

private static bool SkEncodedOriginSwapsWidthHeight(SKEncodedOrigin origin)
{
return origin is
// Reflected across x - axis.Rotated 90° counter - clockwise.
SKEncodedOrigin.LeftTop or

// Rotated 90° clockwise.
SKEncodedOrigin.RightTop or

// Reflected across x-axis. Rotated 90° clockwise.
SKEncodedOrigin.RightBottom or

// Rotated 90° counter-clockwise.
SKEncodedOrigin.LeftBottom;
}

}
116 changes: 116 additions & 0 deletions src/Uno.UI.Composition/Composition/GifFrameProvider.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#nullable enable

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

namespace Microsoft.UI.Composition;

internal sealed class GifFrameProvider : IFrameProvider
{
private readonly SKImage[] _images;
private readonly SKCodecFrameInfo[]? _frameInfos;
private readonly Timer? _timer;
private readonly Stopwatch? _stopwatch;
private readonly long _totalDuration;
private readonly WeakReference<Action> _onFrameChanged;

private int _currentFrame;
private bool _disposed;

// Note: The Timer will keep holding onto the ImageFrameProvider until stopped (it's a static root).
// But we only stop the timer when we dispose ImageFrameProvider from SkiaCompositionSurface finalizer.
// The onFrameChanged Action is also holding onto SkiaCompositionSurface.
// So, if ImageFrameProvider holds onto onFrameChanged, the SkiaCompositionSurface is never GC'ed.
// That's why we make it a WeakReference.
// Note that SkiaCompositionSurface keeps an unused private field storing onFrameChanged so that it's not GC'ed early.
internal GifFrameProvider(SKImage[] images, SKCodecFrameInfo[] frameInfos, long totalDuration, Action onFrameChanged)
{
_images = images;
_frameInfos = frameInfos;
_totalDuration = totalDuration;
_onFrameChanged = new WeakReference<Action>(onFrameChanged);
Debug.Assert(images.Length > 1);
Debug.Assert(frameInfos is not null);
Debug.Assert(totalDuration != 0);
Debug.Assert(onFrameChanged is not null);

if (_images.Length < 2)
{
throw new ArgumentException("GifFrameProvider should only be used when there is at least two frames");
}

_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);
if (_onFrameChanged.TryGetTarget(out var onFrameChanged))
{
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)
{

Check warning on line 104 in src/Uno.UI.Composition/Composition/GifFrameProvider.skia.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Composition/Composition/GifFrameProvider.skia.cs#L104

Either remove or fill this block of code.
}
}

public void Dispose()
{
if (!_disposed)
{
_timer?.Dispose();
_disposed = true;
}
}
}
11 changes: 11 additions & 0 deletions src/Uno.UI.Composition/Composition/IFrameProvider.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable

using System;
using SkiaSharp;

namespace Microsoft.UI.Composition;

internal interface IFrameProvider : IDisposable
{
SKImage? CurrentImage { get; }
}
27 changes: 27 additions & 0 deletions src/Uno.UI.Composition/Composition/SingleFrameProvider.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#nullable enable

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

namespace Microsoft.UI.Composition;

internal sealed class SingleFrameProvider : IFrameProvider
{
private SKImage? _image;

public SingleFrameProvider(SKImage image)
{
_image = image;
}

public SKImage? CurrentImage => _image;

public void Dispose()
{
_image?.Dispose();
_image = null;
}
}
60 changes: 45 additions & 15 deletions src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,54 @@
#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;
// Don't set this field directly. Use SetFrameProviderAndOnFrameChanged instead.
private IFrameProvider? _frameProvider;

// Unused: But intentionally kept!
// This is here to keep the Action lifetime the same as SkiaCompositionSurface.
// i.e, only cause the Action to be GC'ed if SkiaCompositionSurface is GC'ed.
private Action? _onFrameChanged;
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved

public SKImage? Image
// Don't set directly. Use SetFrameProviderAndOnFrameChanged instead
private IFrameProvider? FrameProvider
{
get => _image;
private set
get => _frameProvider;
set
{
_image = value;
OnPropertyChanged(nameof(Image), isSubPropertyChange: false);
_frameProvider?.Dispose();
_frameProvider = value;
OnPropertyChanged(nameof(FrameProvider), isSubPropertyChange: false);
}
}

public SKImage? Image => FrameProvider?.CurrentImage;

internal SkiaCompositionSurface(SKImage image)
{
Image = image;
FrameProvider = FrameProviderFactory.Create(image);
}

private void SetFrameProviderAndOnFrameChanged(IFrameProvider? provider, Action? onFrameChanged)
{
FrameProvider = provider;
_onFrameChanged = onFrameChanged;
}

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

if (result == SKCodecResult.Success)
{
Image = SKImage.FromBitmap(bitmap);
SetFrameProviderAndOnFrameChanged(FrameProviderFactory.Create(SKImage.FromBitmap(bitmap)), null);
}

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

SetFrameProviderAndOnFrameChanged(provider, onFrameChanged);
GC.KeepAlive(onFrameChanged);
return (true, "Success");
}
catch (Exception e)
{
SetFrameProviderAndOnFrameChanged(null, null);
return (false, e.Message);
}
}
Expand All @@ -84,8 +109,13 @@ internal unsafe void CopyPixels(int pixelWidth, int pixelHeight, ReadOnlyMemory<

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

~SkiaCompositionSurface()
{
SetFrameProviderAndOnFrameChanged(null, null);
}
}
}
Loading
Loading