Skip to content

Commit 6d3000b

Browse files
committed
feat: Add support for .webp format
1 parent e1f1f8a commit 6d3000b

File tree

3 files changed

+77
-24
lines changed

3 files changed

+77
-24
lines changed

src/Uno.UI.Composition/Composition/GifFrameProvider.skia.cs renamed to src/Uno.UI.Composition/Composition/AnimatedImageFrameProvider.skia.cs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,57 +8,68 @@
88

99
namespace Microsoft.UI.Composition;
1010

11-
internal sealed class GifFrameProvider : IFrameProvider
11+
internal sealed class AnimatedImageFrameProvider : IFrameProvider
1212
{
1313
private readonly SKImage[] _images;
14-
private readonly SKCodecFrameInfo[]? _frameInfos;
14+
private readonly int[] _durations;
1515
private readonly Timer? _timer;
1616
private readonly Stopwatch? _stopwatch;
1717
private readonly long _totalDuration;
18+
private readonly long _memoryPressure;
1819
private readonly WeakReference<Action> _onFrameChanged;
1920

2021
private int _currentFrame;
2122
private bool _disposed;
2223

23-
// Note: The Timer will keep holding onto the ImageFrameProvider until stopped (it's a static root).
24-
// But we only stop the timer when we dispose ImageFrameProvider from SkiaCompositionSurface finalizer.
24+
// Note: The Timer will keep holding onto the AnimatedImageFrameProvider until stopped (it's a static root).
25+
// But we only stop the timer when we dispose AnimatedImageFrameProvider from SkiaCompositionSurface finalizer.
2526
// The onFrameChanged Action is also holding onto SkiaCompositionSurface.
26-
// So, if ImageFrameProvider holds onto onFrameChanged, the SkiaCompositionSurface is never GC'ed.
27+
// So, if AnimatedImageFrameProvider holds onto onFrameChanged, the SkiaCompositionSurface is never GC'ed.
2728
// That's why we make it a WeakReference.
2829
// Note that SkiaCompositionSurface keeps an unused private field storing onFrameChanged so that it's not GC'ed early.
29-
internal GifFrameProvider(SKImage[] images, SKCodecFrameInfo[] frameInfos, long totalDuration, Action onFrameChanged)
30+
internal AnimatedImageFrameProvider(SKImage[] images, int[] durations, long totalDuration, Action onFrameChanged)
3031
{
3132
_images = images;
32-
_frameInfos = frameInfos;
33+
_durations = durations;
3334
_totalDuration = totalDuration;
3435
_onFrameChanged = new WeakReference<Action>(onFrameChanged);
3536
Debug.Assert(images.Length > 1);
36-
Debug.Assert(frameInfos is not null);
37+
Debug.Assert(durations is not null);
38+
Debug.Assert(durations.Length == images.Length);
3739
Debug.Assert(totalDuration != 0);
3840
Debug.Assert(onFrameChanged is not null);
3941

4042
if (_images.Length < 2)
4143
{
42-
throw new ArgumentException("GifFrameProvider should only be used when there is at least two frames");
44+
throw new ArgumentException("AnimatedImageFrameProvider should only be used when there is at least two frames");
4345
}
4446

47+
long pressure = 0;
48+
for (int i = 0; i < _images.Length; i++)
49+
{
50+
pressure += _images[i].Info.BytesSize;
51+
}
52+
53+
_memoryPressure = pressure;
54+
GC.AddMemoryPressure(_memoryPressure);
55+
4556
_stopwatch = Stopwatch.StartNew();
46-
_timer = new Timer(OnTimerCallback, null, dueTime: _frameInfos![0].Duration, period: Timeout.Infinite);
57+
_timer = new Timer(OnTimerCallback, null, dueTime: _durations[0], period: Timeout.Infinite);
4758
}
4859

4960
public SKImage? CurrentImage => _images[_currentFrame];
5061

5162
private int GetCurrentFrameIndex()
5263
{
5364
var currentTimestampInMilliseconds = _stopwatch!.ElapsedMilliseconds % _totalDuration;
54-
for (int i = 0; i < _frameInfos!.Length; i++)
65+
for (int i = 0; i < _durations.Length; i++)
5566
{
56-
if (currentTimestampInMilliseconds < _frameInfos[i].Duration)
67+
if (currentTimestampInMilliseconds < _durations[i])
5768
{
5869
return i;
5970
}
6071

61-
currentTimestampInMilliseconds -= _frameInfos[i].Duration;
72+
currentTimestampInMilliseconds -= _durations[i];
6273
}
6374

6475
throw new InvalidOperationException("This shouldn't be reachable. A timestamp in total duration range should map to a frame");
@@ -86,7 +97,7 @@ private void OnTimerCallback(object? state)
8697
var nextFrameTimeStamp = 0;
8798
for (int i = 0; i <= _currentFrame; i++)
8899
{
89-
nextFrameTimeStamp += _frameInfos![i].Duration;
100+
nextFrameTimeStamp += _durations[i];
90101
}
91102

92103
var dueTime = nextFrameTimeStamp - timestamp;
@@ -109,8 +120,15 @@ public void Dispose()
109120
{
110121
if (!_disposed)
111122
{
112-
_timer?.Dispose();
113123
_disposed = true;
124+
_timer?.Dispose();
125+
126+
for (int i = 0; i < _images.Length; i++)
127+
{
128+
_images[i].Dispose();
129+
}
130+
131+
GC.RemoveMemoryPressure(_memoryPressure);
114132
}
115133
}
116134
}

src/Uno.UI.Composition/Composition/FrameProviderFactory.skia.cs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,30 @@ public static bool TryCreate(SKManagedStream stream, Action onFrameChanged, [Not
3636
}
3737

3838
var images = GC.AllocateUninitializedArray<SKImage>(frameInfos.Length);
39+
var durations = new int[frameInfos.Length];
3940
var totalDuration = 0;
41+
4042
for (int i = 0; i < frameInfos.Length; i++)
4143
{
42-
var options = new SKCodecOptions(i);
44+
var requiredFrame = frameInfos[i].RequiredFrame;
45+
46+
if (requiredFrame == -1)
47+
{
48+
// Independent frame - clear the bitmap before decoding.
49+
bitmap.Erase(SKColor.Empty);
50+
}
51+
else if (requiredFrame != i - 1)
52+
{
53+
// The required frame is not the immediately preceding one, so we
54+
// need to restore its pixels into the bitmap before decoding.
55+
using var restoreCanvas = new SKCanvas(bitmap);
56+
restoreCanvas.Clear(SKColor.Empty);
57+
restoreCanvas.DrawImage(images[requiredFrame], 0, 0);
58+
}
59+
// When requiredFrame == i - 1, the bitmap already contains the
60+
// correct prior frame pixels from the previous iteration.
61+
62+
var options = new SKCodecOptions(i, requiredFrame);
4363
codec.GetPixels(imageInfo, bitmap.GetPixels(), options);
4464

4565
var currentBitmap = GetImage(bitmap, codec.EncodedOrigin);
@@ -50,10 +70,15 @@ public static bool TryCreate(SKManagedStream stream, Action onFrameChanged, [Not
5070
}
5171

5272
images[i] = currentBitmap;
53-
totalDuration += frameInfos[i].Duration;
73+
74+
// Clamp zero-duration frames to 100ms to prevent division-by-zero
75+
// and match common animated image behavior.
76+
var duration = frameInfos[i].Duration;
77+
durations[i] = duration > 0 ? duration : 100;
78+
totalDuration += durations[i];
5479
}
5580

56-
provider = new GifFrameProvider(images, frameInfos, totalDuration, onFrameChanged);
81+
provider = new AnimatedImageFrameProvider(images, durations, totalDuration, onFrameChanged);
5782
return true;
5883
}
5984

@@ -98,16 +123,16 @@ private static SKMatrix GetExifMatrix(SKEncodedOrigin origin, int width, int hei
98123
private static bool SkEncodedOriginSwapsWidthHeight(SKEncodedOrigin origin)
99124
{
100125
return origin is
101-
// Reflected across x - axis.Rotated 90° counter - clockwise.
126+
// Reflected across x - axis.Rotated 90 counter - clockwise.
102127
SKEncodedOrigin.LeftTop or
103128

104-
// Rotated 90° clockwise.
129+
// Rotated 90 clockwise.
105130
SKEncodedOrigin.RightTop or
106131

107-
// Reflected across x-axis. Rotated 90° clockwise.
132+
// Reflected across x-axis. Rotated 90 clockwise.
108133
SKEncodedOrigin.RightBottom or
109134

110-
// Rotated 90° counter-clockwise.
135+
// Rotated 90 counter-clockwise.
111136
SKEncodedOrigin.LeftBottom;
112137
}
113138

src/Uno.UI/UI/Xaml/Media/Imaging/ImageSourceHelpers.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,23 @@ public static async Task<ImageData> GetImageDataFromUriAsCompositionSurface(Uri
129129
try
130130
{
131131
using var stream = await AppDataUriEvaluator.ToStream(uri, ct);
132-
// add more animation formats here if needed
133-
return await ReadFromStreamAsCompositionSurface(stream, ct, !uri.AbsolutePath.EndsWith(".gif", StringComparison.InvariantCultureIgnoreCase));
132+
return await ReadFromStreamAsCompositionSurface(stream, ct, !IsAnimatableImageFormat(uri));
134133
}
135134
catch (Exception e)
136135
{
137136
return ImageData.FromError(e);
138137
}
139138
}
139+
140+
/// <summary>
141+
/// Returns true for image formats that may contain animation and must bypass the
142+
/// browser Canvas API (which only returns the first frame) in favor of SKCodec.
143+
/// </summary>
144+
private static bool IsAnimatableImageFormat(Uri uri)
145+
{
146+
var path = uri.AbsolutePath;
147+
return path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)
148+
|| path.EndsWith(".webp", StringComparison.OrdinalIgnoreCase);
149+
}
140150
#endif
141151
}

0 commit comments

Comments
 (0)