Skip to content

Commit ea213a4

Browse files
committed
Add GifImage
1 parent c59d7d9 commit ea213a4

12 files changed

Lines changed: 536 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Install-Package DevWinUI
145145
## 🔥 DevWinUI.Controls 🔥
146146
### ⚡ What’s Inside? ⚡
147147

148+
- ✨ GifImage
148149
- ✨ Accordion
149150
- ✨ ShyHeader
150151
- ✨ FlipToReveal
@@ -287,6 +288,9 @@ Install-Package DevWinUI.ContextMenu
287288

288289
## 🕰️ History 🕰️
289290

291+
### GifImage
292+
![DevWinUI](https://raw.githubusercontent.com/ghost1372/DevWinUI-Resources/refs/heads/main/DevWinUI-Docs/GifImage.gif)
293+
290294
### Accordion
291295
![DevWinUI](https://raw.githubusercontent.com/ghost1372/DevWinUI-Resources/refs/heads/main/DevWinUI-Docs/Accordion.gif)
292296

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace DevWinUI;
2+
public struct FrameProperties
3+
{
4+
public readonly Rect Rect;
5+
public readonly double DelayMilliseconds;
6+
public readonly bool ShouldDispose;
7+
8+
public FrameProperties(Rect rect, double delayMilliseconds, bool shouldDispose)
9+
{
10+
Rect = rect;
11+
DelayMilliseconds = delayMilliseconds;
12+
ShouldDispose = shouldDispose;
13+
}
14+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using Windows.Graphics.Imaging;
2+
using Windows.Storage;
3+
using Windows.Storage.Streams;
4+
5+
namespace DevWinUI;
6+
public partial class GifImage
7+
{
8+
private DispatcherTimer _timer = null;
9+
10+
private int _index = 0;
11+
List<BitmapFrame> _frames = null;
12+
13+
private int _width;
14+
private int _height;
15+
private byte[] _pixels = null;
16+
17+
private void InitializeTimer()
18+
{
19+
_timer = new DispatcherTimer();
20+
_timer.Tick += OnTimerTick;
21+
}
22+
23+
private async void OnTimerTick(object sender, object e)
24+
{
25+
if (_frames != null && _frames.Count > 0)
26+
{
27+
await PlayFrameAsync();
28+
29+
_index = (_index + 1) % _frames.Count;
30+
31+
if (_index == 0 && !this.IsLooping)
32+
{
33+
this.Stop();
34+
}
35+
}
36+
else
37+
{
38+
this.Stop();
39+
}
40+
}
41+
42+
private async Task PlayFrameAsync()
43+
{
44+
if (_frames != null && _frames.Count > 0)
45+
{
46+
var frame = _frames[_index];
47+
var props = await GetFramePropertiesAsync(frame);
48+
_timer.Interval = TimeSpan.FromMilliseconds(props.DelayMilliseconds);
49+
50+
if (_index == 0 || props.ShouldDispose)
51+
{
52+
_width = (int)props.Rect.Width;
53+
_height = (int)props.Rect.Height;
54+
_pixels = await GetPixelsAsync(frame);
55+
}
56+
else
57+
{
58+
var pixels = await GetPixelsAsync(frame);
59+
MergePixels(_pixels, _width, pixels, props.Rect);
60+
}
61+
62+
_image.Source = LoadImage(_pixels, _width, _height);
63+
}
64+
}
65+
66+
private async Task ProcessSourceAsync(Uri uri)
67+
{
68+
try
69+
{
70+
if (uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https"))
71+
{
72+
using (var httpClient = new HttpClient())
73+
{
74+
using (var httpMessage = await httpClient.GetAsync(uri))
75+
{
76+
using var stream = await httpMessage.Content.ReadAsStreamAsync();
77+
_frames = await LoadFramesAsync(stream.AsRandomAccessStream());
78+
}
79+
}
80+
}
81+
else
82+
{
83+
var file = await FileHelper.GetStorageFile(uri);
84+
using (var stream = await file.OpenReadAsync())
85+
{
86+
_frames = await LoadFramesAsync(stream);
87+
}
88+
}
89+
90+
this.ImageOpened?.Invoke(this, new RoutedEventArgs());
91+
}
92+
catch (Exception ex)
93+
{
94+
this.ImageFailed?.Invoke(this, ex);
95+
System.Diagnostics.Debug.WriteLine("ProcessSourceAsync. {0}", ex.Message);
96+
}
97+
}
98+
99+
private async Task<List<BitmapFrame>> LoadFramesAsync(IRandomAccessStream stream)
100+
{
101+
var frames = new List<BitmapFrame>();
102+
var decoder = await BitmapDecoder.CreateAsync(stream);
103+
104+
uint count = decoder.FrameCount;
105+
for (uint n = 0; n < count; n++)
106+
{
107+
var frame = await decoder.GetFrameAsync(n);
108+
frames.Add(frame);
109+
}
110+
111+
return frames;
112+
}
113+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Runtime.InteropServices.WindowsRuntime;
2+
using Windows.Graphics.Imaging;
3+
4+
namespace DevWinUI;
5+
6+
public partial class GifImage
7+
{
8+
private static async Task<byte[]> GetPixelsAsync(BitmapFrame frame)
9+
{
10+
var pixelData = await frame.GetPixelDataAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, new BitmapTransform(), ExifOrientationMode.RespectExifOrientation, ColorManagementMode.DoNotColorManage);
11+
return pixelData.DetachPixelData();
12+
}
13+
14+
private static WriteableBitmap LoadImage(byte[] pixels, int width, int height)
15+
{
16+
var bitmap = new WriteableBitmap(width, height);
17+
var buffer = bitmap.PixelBuffer;
18+
pixels.CopyTo(buffer);
19+
bitmap.Invalidate();
20+
return bitmap;
21+
}
22+
23+
private static void MergePixels(byte[] pixels1, int width, byte[] pixels2, Rect rect)
24+
{
25+
try
26+
{
27+
int x0 = (int)rect.X;
28+
int y0 = (int)rect.Y;
29+
int stride = (int)rect.Width;
30+
31+
for (int y = 0; y < rect.Height; y++)
32+
{
33+
for (int x = 0; x < rect.Width; x++)
34+
{
35+
int index1 = (x0 + x + (y0 + y) * width) * 4;
36+
int index2 = (x + y * stride) * 4;
37+
if (pixels2[index2 + 3] > 0)
38+
{
39+
pixels1[index1 + 0] = pixels2[index2 + 0];
40+
pixels1[index1 + 1] = pixels2[index2 + 1];
41+
pixels1[index1 + 2] = pixels2[index2 + 2];
42+
pixels1[index1 + 3] = pixels2[index2 + 3];
43+
}
44+
}
45+
}
46+
}
47+
catch (Exception ex)
48+
{
49+
System.Diagnostics.Debug.WriteLine("MergePixels. {0}", ex.Message);
50+
}
51+
}
52+
53+
private static async Task<FrameProperties> GetFramePropertiesAsync(BitmapFrame frame)
54+
{
55+
const string leftProperty = "/imgdesc/Left";
56+
const string topProperty = "/imgdesc/Top";
57+
const string widthProperty = "/imgdesc/Width";
58+
const string heightProperty = "/imgdesc/Height";
59+
const string delayProperty = "/grctlext/Delay";
60+
const string disposalProperty = "/grctlext/Disposal";
61+
62+
var propertiesView = frame.BitmapProperties;
63+
var requiredProperties = new[] { leftProperty, topProperty, widthProperty, heightProperty };
64+
var properties = await propertiesView.GetPropertiesAsync(requiredProperties);
65+
66+
var left = (ushort)properties[leftProperty].Value;
67+
var top = (ushort)properties[topProperty].Value;
68+
var width = (ushort)properties[widthProperty].Value;
69+
var height = (ushort)properties[heightProperty].Value;
70+
71+
var delayMilliseconds = 30.0;
72+
var shouldDispose = false;
73+
74+
try
75+
{
76+
var extensionProperties = new[] { delayProperty, disposalProperty };
77+
properties = await propertiesView.GetPropertiesAsync(extensionProperties);
78+
79+
if (properties.ContainsKey(delayProperty) && properties[delayProperty].Type == PropertyType.UInt16)
80+
{
81+
var delayInHundredths = (ushort)properties[delayProperty].Value;
82+
if (delayInHundredths >= 3u) // Prevent degenerate frames with no delay time
83+
{
84+
delayMilliseconds = 10.0 * delayInHundredths;
85+
}
86+
}
87+
88+
if (properties.ContainsKey(disposalProperty) && properties[disposalProperty].Type == PropertyType.UInt8)
89+
{
90+
var disposal = (byte)properties[disposalProperty].Value;
91+
if (disposal == 2)
92+
{
93+
// 0 = undefined
94+
// 1 = none (compose next frame on top of this one, default)
95+
// 2 = dispose
96+
// 3 = revert to previous (not supported)
97+
shouldDispose = true;
98+
}
99+
}
100+
}
101+
catch
102+
{
103+
// These properties are not required, so it's okay to ignore failure.
104+
}
105+
106+
return new FrameProperties(
107+
new Rect(left, top, width, height),
108+
delayMilliseconds,
109+
shouldDispose
110+
);
111+
}
112+
}

0 commit comments

Comments
 (0)