Gif Rendering in Windows Runtime
Windows users have long complained that there is no native support for rendering animated GIFs. And Windows developers have long complained that rendering animated GIFs is difficult to implement correctly. There are actually several approaches to animating GIFs available to Windows and Windows Phone. We will cover some of them here and discuss the advantages and disadvantages of each.
First, some relevant background into the GIF image format. At its most basic, a GIF contains a color table and any number of frames. The color table contains 256 entries, one entry of which usually denotes transparency (aka the transparency index). Each frame contains bitmap data (represented as indices to the color table), as well as metadata. The most important metadata include:
1) Delay -- the amount of time to wait before rendering the next frame (represented in units of 10ms). A typical value is 6, meaning 60 ms.
2) Left position, Top position -- the offset at which a frame is drawn. The implication is that one frame is not necessarily the size of the entire image.
3) Disposal method -- the method by which the frame is removed before moving to the next frame.
You can check out the entire spec for even more detail. As you can see, rendering a GIF is not as simple as it might seem.
If it suits your needs, the easiest solution is to simply embed a WebView into your app, and let IE do all the rendering work for you. However, the downside is that the WebView control is not well suited for displaying single images; for example, it is difficult (maybe impossible?) to stretch the image to fill a specified area.
Instead of using WebView, we can try doing the rendering ourselves. The Windows Imaging Component (WIC) is a Windows library that can help us with some of the heavy lifting. However, because it is native, it can only be consumed within a Windows Runtime Component.
For those not accustomed to writing C++, WinRT contains the BitmapDecoder (and BitmapEncoder) classes, which are essentially wrappers around WIC. BitmapDecoder is easy to use and, together with WriteableBitmap, forms the keystone to our first real implementation of animating GIFs.
private WriteableBitmap _bitmap;
private DispatcherTimer _timer;
private BitmapDecoder _decoder;
private uint _currentFrame;
private async Task LoadGifAsync(Uri uri)
{
var file = await StorageFile.GetFileFromApplicationUriAsync(uri);
using (var stream = await file.OpenReadAsync())
{
_decoder = await BitmapDecoder.CreateAsync(BitmapDecoder.GifDecoderId, stream);
_image.Source = _bitmap = new WriteableBitmap((int)_decoder.PixelWidth, (int)_decoder.PixelHeight);
_currentFrame = 0;
await RenderFrameAsync();
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
_timer.Tick += async (sender, o) => await RenderFrameAsync();
_timer.Start();
}
}
private async Task RenderFrameAsync()
{
var frame = await _decoder.GetFrameAsync(_currentFrame);
var pixelData = await frame.GetPixelDataAsync(BitmapPixelFormat.Bgra8, _decoder.BitmapAlphaMode,
new BitmapTransform(), ExifOrientationMode.IgnoreExifOrientation, ColorManagementMode.DoNotColorManage);
var bytes = pixelData.DetachPixelData();
using (var stream = _bitmap.PixelBuffer.AsStream())
{
stream.Write(bytes, 0, bytes.Length);
}
_bitmap.Invalidate();
_currentFrame = (_currentFrame + 1)%_decoder.FrameCount;
}
FYI this implementation does not work for most GIFs! It is just an example of how one might make use of WriteableBitmap and BitmapDecoder!
This implementation has one obvious advantage: it is entirely C#. From a performance standpoint, however, it might be possible to do better. For example, we incur some overhead from jumping back and forth across the managed/native boundary. It's also possible that we are copying pixel data more frequently than is necessary, again due to the nature of WinRT interop.
It's likely more performant to work entirely in native C++, so one possibility would be to re-write the above implementation as a C++ WinRT component, ditching BitmapDecoder for WIC. A key takeaway here is that, because the usage of DispatcherTimer would move to C++, we would live entirely in the native side.
However, an even better solution is to forego using WriteableBitmap entirely and leverage the power of DirectX instead with SurfaceImageSource. This requires some extra work, such as converting a WIC bitmap to a Direct2D-compatible bitmap, but it is relatively straightforward.
void GifImageSource::LoadImage(IStream *pStream)
{
HRESULT hr = S_OK;
// IWICImagingFactory is where it all begins
ComPtr<IWICImagingFactory> pFactory;
hr = CoCreateInstance(CLSID_WICImagingFactory,
NULL,
CLSCTX_INPROC_SERVER,
IID_IWICImagingFactory,
(LPVOID*) &pFactory);
// IWICBitmapDecoder is roughly analogous to .NET's BitmapDecoder
ComPtr<IWICBitmapDecoder> pDecoder;
hr = pFactory->CreateDecoderFromStream(pStream, NULL, WICDecodeMetadataCacheOnDemand, &pDecoder);
// Keep track of frame count
UINT dwFrameCount = 0;
hr = pDecoder->GetFrameCount(&dwFrameCount);
// Get and convert each frame bitmap into ID2D1Bitmap
for (UINT dwFrameIndex = 0; dwFrameIndex < dwFrameCount; dwFrameIndex++)
{
// IWICBitmapFrameDecode is roughly analogous to .NET's BitmapFrame
ComPtr<IWICBitmapFrameDecode> pFrameDecode;
hr = pDecoder->GetFrame(dwFrameIndex, &pFrameDecode);
// Save image height and width for use later
if (dwFrameIndex == 0)
{
hr = pFrameDecode->GetSize((UINT*) &m_width, (UINT*) &m_height);
}
// Bitmap must first be converted to B8G8R8A8
ComPtr<IWICFormatConverter> pConvertedBitmap;
hr = pFactory->CreateFormatConverter(&pConvertedBitmap);
hr = pConvertedBitmap->Initialize(pFrameDecode.Get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, NULL, 0, WICBitmapPaletteTypeCustom);
// Final step before the bitmap is usable by D2D
ComPtr<IWICBitmap> pWicBitmap;
hr = pFactory->CreateBitmapFromSource(pConvertedBitmap.Get(), WICBitmapCacheOnDemand, &pWicBitmap);
// Finally, we have something that can be drawn to our SurfaceImageSource
ComPtr<ID2D1Bitmap> pBitmap;
hr = m_d2dContext->CreateBitmapFromWicBitmap(pWicBitmap.Get(), &pBitmap);
// Save pBitmap, do other stuff
}
}
This is an abbreviated version of the full implementation, highlighting the process of taking the raw image stream and building each frame's bitmap from it.
The end result is an image source that handles GIFs fast. Not only have we moved all of our code to native C++, we are also pushing pixels to the screen as fast as we possibly can using Direct2D. The full source code and sample code are hosted on GitHub.
Comments
- Anonymous
November 18, 2014
simple and useful!