Search code examples
c#uwpuwp-xamlanimated-gifmemorystream

Show frame of GIF in Image control


I try to show a single frame of a loaded gif image (from a file) inside an Image control. I have no issue in showing the (full) gif image inside an Image control, but I cannot get the frame to show up. For testing purposes I always try to load frame '0' (the first frame).

XAML:

<Image>
    <Image.Source>
        <BitmapImage x:Name="GifFrame" AutoPlay="False" />
    </Image.Source>
</Image>

I don't really find much information online of how to do this but here and there are some code fragments, some may unfortunately come from WPF and I a m not 100% sure if they can be used also for UWP.

When the image is opened the following "_OnImageOpened" method gets called (_gifStream is the opened gif image, stream has not been closed):

private IRandomAccessStream _gifStream = null;

private async void GifImage_OnImageOpened(object sender, RoutedEventArgs e)
{
    // ...

    uint frameCount = 0;
    if (_gifStream != null)
    {
        var decoder = await BitmapDecoder.CreateAsync(_gifStream);
        frameCount = decoder.FrameCount;

        var frame = await decoder.GetFrameAsync(0);

        // Create frame image
        var pixelData = await frame.GetPixelDataAsync(
            BitmapPixelFormat.Bgra8,
            BitmapAlphaMode.Premultiplied,
            new BitmapTransform(),
            ExifOrientationMode.IgnoreExifOrientation,
            ColorManagementMode.DoNotColorManage
            ).AsTask().ConfigureAwait(false);
        var frameBytes = pixelData.DetachPixelData();
        var memoryStream = new MemoryStream(frameBytes);
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                GifFrame.SetSource(memoryStream.AsRandomAccessStream());
            });
    }
}

When I set the new source (GifFrame.SetSource(...)) the window freezes and nothing happens, I have to kill the process. Before I added the Dispatcher.RunAsync(...) around the setting of the new source I got an exception "The application called an interface that was marshalled for a different thread...".

I don't have any idea what causes this. Maybe I need to convert the bytes to something the image control can understand or maybe the whole pixel data creation is wrong?


Solution

  • Here's one way to achieve what you're looking for:

    XAML:

    <Page
        x:Class="App1.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid>
            <StackPanel>
                <Button Content="Open" Click="Button_Click" />
                <Image x:Name="Image" Width="100" Height="100" />
            </StackPanel>
        </Grid>
    </Page>
    

    Code:

    using System;
    using Windows.Graphics.Imaging;
    using Windows.Storage;
    using Windows.Storage.Pickers;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Media.Imaging;
    
    namespace App1
    {
        public sealed partial class MainPage
        {
            public MainPage()
            {
                InitializeComponent();
            }
    
            private async void Button_Click(object sender, RoutedEventArgs e)
            {
                var picker = new FileOpenPicker();
    
                picker.FileTypeFilter.Add(".gif");
    
                picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    
                var file = await picker.PickSingleFileAsync();
                if (file == null)
                    return;
    
                var stream = await file.OpenAsync(FileAccessMode.Read);
    
                var decoder = await BitmapDecoder.CreateAsync(stream);
    
                var frame = await decoder.GetFrameAsync(0);
    
                var softwareBitmap = await frame.GetSoftwareBitmapAsync();
    
                var convert = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    
                var softwareBitmapSource = new SoftwareBitmapSource();
    
                await softwareBitmapSource.SetBitmapAsync(convert);
    
                Image.Source = softwareBitmapSource;
            }
        }
    }
    

    Result:

    enter image description here

    Quite convoluted process to say the least ...

    Original GIF:

    enter image description here

    Your actual error is that you're trying to feed SetSource with a raw stream of pixel data, this is not what it expects.