Tell us your problem!  We can usually fix it. Windows Imaging Component and Animated GIF Files

Last update: 27 May 2019:

If you've reached this page, you are interested in the Windows Imaging Component libraries (WIC ) and will have been working with them to some extent.

WIC supports reading and writing BMP, TIFF, JPEG, PNG and GIF files, as well as importing raw high resolution image files from a number of cameras (Nikon .NEF, Canon .CR2, Sony .ARW, Olympus .ORF).

This article describes how to read and write animated GIF files using WIC methods, and the issues that arise. The GIF format specification is reasonably complex, but fortunately there's little need to delve into the depths of it to make use of GIFs.

There's sample code on MSDN that shows how to read animated GIFs, but almost none on how to create them.

The most common developer issue is how to read and more importantly, write the metadata.

What is an animated GIF exactly?

An animated GIF is a 256 colour paletted image file that plays in a browser a sequence of frames to simulate movement. The "headbanger" image above is an example. Internally, there is a master frame (#0) and a set of content frames (#1 to #n). The time each frame can be displayed, or Delay, can be set for each frame, and is a multiple of 10ms. In many cases, the master and content frames have the same dimensions, but they don't have to.

In fact, content frames can be smaller than the master frame, and the displayed position in the master frame can be set. The transparency palette colour index (commonly 0) can be set for each frame.

Read the GIF89a Specification

This 1990 Compuserve text document is not the easiest to read. Most readers will find the Wikipedia article easier to understand.

Read the Wikipedia article.    

What's the problem?

There's an MSDN article that describes how to use WIC to read and write image metadata that is concise to the point of obscurity, and the code fragments are disjointed and incomplete. Our aim is to rectify that.

Metadata can be considered a binary directory structure, though you won't find the names listed in the GIF header area in a binary listing. There's two types of metadata: global and frame.

Animated GIF metadata directory names as implemented by WIC are listed and helpfully provide the metadata names and the PROPVARIANT types required for each one.

The code fragments listed below are to indicate the processes needed to write GIF metadata successfully. The development PC was running Windows 7 32-bit.

Starting WIC

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <wincodec.h>
#include <wincodecsdk.h>
// These passed in from calling app code 
LPWSTR pOutputFilePath = L"path\\MyAnimated.gif";
HBITMAP hFrameBitmaps[6];
int frameDelayValues[6];

int imageWidth, imageHeight;

HRESULT hr = S_OK;
ULONG result;
IWICImagingFactory *pIWICFactory = NULL;
IWICBitmapSource *pWICBitmapSource = NULL;
IWICBitmap *pWICBitmap = NULL;
IWICBitmapScaler *pScaler = NULL;
IWICFormatConverter *pConverter = NULL;
IWICBitmapFrameEncode *pBitmapFrameEncode = NULL;
IWICMetadataQueryWriter *pEncoderMetadataQueryWriter = NULL;
IWICMetadataQueryWriter *pFrameMetadataQueryWriter = NULL;   
IWICPalette *pPalette = NULL;
WICPixelFormatGUID pixelFormat;
UINT uWidth = 0, uHeight = 0;
PROPVARIANT propValue;
PropVariantInit(&propValue);

// Create the class factory, make the bitmap encoder

hr = CoCreateInstance(CLSID_WICImagingFactory,NULL,
      
CLSCTX_INPROC_SERVER,
        IID_PPV_ARGS(&pIWICFactory)  );

hr = pIWICFactory->CreateEncoder(GUID_ContainerFormatGif,
          &GUID_VendorMicrosoft,&pBitmapEncoder);

hr = pIWICFactory->CreateStream(&pIGIFstream);

hr = pIGIFstream->InitializeFromFilename(pOutputFilePath,GENERIC_WRITE);

Writing Metadata with IWICMetadataQueryWriter

This is the one that causes the most problems. It's simple emough to set up the frame encoder and pass metadata settings to it. It's then necessary to create a second query writer to write the first according to the MSDN article. Well, actually, no, it's not necessary at all. 

 

hr = pBitmapEncoder->Initialize(pIGIFstream, WICBitmapEncoderNoCache);
hr = pBitmapEncoder->GetMetadataQueryWriter
         (&pEncoderMetadataQueryWriter);
// We have to write the "/appext/application/NETSCAPE2.0" value into the global metadata.
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 11;
propValue.caub.pElems = new UCHAR[11];
memcpy(propValue.caub.pElems, "NETSCAPE2.0", 11); //
hr = pEncoderMetadataQueryWriter->SetMetadataByName
        (L
"/appext/Application",&propValue);
delete[] propValue.caub.pElems; // required
PropVariantClear(&propValue);

// Set animated GIF format

propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 5;
propValue.caub.pElems = new UCHAR[5];
*(propValue.caub.pElems) = 3; // must be > 1,
*(propValue.caub.pElems + 1) = 1; // defines animated GIF
*(propValue.caub.pElems + 2) = 0; // LSB 0 = infinite loop.
*(propValue.caub.pElems + 3) = 0; // MSB of iteration count value
*(propValue.caub.pElems + 4) = 0; // NULL == end of data
hr = pEncoderMetadataQueryWriter->SetMetadataByName(L"/appext/Data",
              &propValue);
delete[] propValue.caub.pElems;
PropVariantClear(&propValue);
propValue.vt = VT_LPSTR;
propValue.pszVal = comment_string_pointer; // use new-delete[] calls
hr = pEncoderMetadataQueryWriter->SetMetadataByName(L"/commentext/TextEntry",&propValue);
delete[] propValue.pszVal;

PropVariantClear(&propValue);

// Get the first frame width and height. Code not shown.

// Global Width and Height are written successfully.
propValue.vt = VT_UI2;
propValue.uiVal = (USHORT)imageWidth;
hr = pEncoderMetadataQueryWriter->SetMetadataByName
          (L
"/logscrdesc/Width",&propValue);
PropVariantClear(&propValue);
propValue.vt = VT_UI2;
propValue.uiVal = (USHORT)imageHeight;
hr = pEncoderMetadataQueryWriter->SetMetadataByName
          (L
"/logscrdesc/Height",&propValue);
PropVariantClear(&propValue);

 

 

// We have for this example, 6 frames to process.
for(int frameIndex = 0; frameIndex < 6; frameIndex++)
{

 hr = pIWICFactory->CreateBitmapFromHBITMAP(hFrameBitmaps[frameIndex],NULL,WICBitmapIgnoreAlpha,&pWICBitmap);

hr = pIWICFactory->CreateFormatConverter(&pConverter);

// The input bitmaps in the PiXCL list are (by definition) RGB24, and GIF

// frames are 8bppIndexed, so we have to convert each one.

if (SUCCEEDED(hr))
{
hr = pWICBitmap->GetSize(&uWidth, &uHeight);
hr = pIWICFactory->CreateBitmapScaler(&pScaler);// Released later
hr = pScaler->Initialize((IWICBitmapSource *)pWICBitmap,
uWidth, uHeight, //
WICBitmapInterpolationModeFant);
hr = pWICBitmap->CopyPalette(pPalette); // Return NULL for RGB24
if(NULL == pPalette)
{ // we usually have to make this
hr = pIWICFactory->CreatePalette(&pPalette); // Released later.
pPalette->InitializeFromBitmap((IWICBitmapSource *)pWICBitmap,256,TRUE);
}
hr = pConverter->Initialize(
pScaler,// Input bitmap to convert
GUID_WICPixelFormat8bppIndexed, // Destination pixel format
ditherType, // see wincodec.h
pPalette, // Windows decides the palette
0.f, // Alpha threshold
WICBitmapPaletteTypeFixedWebPalette // probably more useful
);

// Store the converted bitmap as ppToRenderBitmapSource

if (SUCCEEDED(hr))
{
hr = pConverter->QueryInterface(IID_PPV_ARGS(&pWICBitmapSource));
}
pConverter->Release();
}// endif

// Create a new default pBitmapFrameEncode object.
hr = pBitmapEncoder->CreateNewFrame(&pBitmapFrameEncode,NULL);
hr = pBitmapFrameEncode->Initialize(NULL); // no options yet
hr = pBitmapFrameEncode->WriteSource(pWICBitmapSource,NULL);

hr = pBitmapFrameEncode->GetMetadataQueryWriter(&pFrameMetadataQueryWriter);

hr = pBitmapFrameEncode->Commit(); // has to be HERE !

propValue.vt = VT_UI2;
propValue.uiVal = (WORD)(frameDelayValues[frameIndex] / 10);
hr = pFrameMetadataQueryWriter->SetMetadataByName(L "/grctlext/Delay", &propValue);
PropVariantClear(&propValue);

// Other "/grctlext/*" values written here. Writing to the root
// metadata region is not required.

// Set the Frame Width and Height. WIC writes both of these.
propValue.vt = VT_UI2;
propValue.uiVal = uWidth;
hr = pFrameMetadataQueryWriter->SetMetadataByName(L"/imgdesc/Width", &propValue);
PropVariantClear(&propValue);
propValue.vt = VT_UI2;
propValue.uiVal = uHeight;
hr = pFrameMetadataQueryWriter->SetMetadataByName(L"/imgdesc/Height", &propValue);
PropVariantClear(&propValue);

// Other "/imgdesc" values written here

/* // Write to the imgdesc root "directory" with everything. This works
// and can be read back from the created animGIF.
THIS IS NOT REQUIRED, despite what you might read in MSDN 
propValue.vt = VT_UNKNOWN;
propValue.punkVal = pFrameMetadataQueryWriter;
propValue.punkVal->AddRef();
hr = pGlobalMetadataQueryWriter->SetMetadataByName(L"/imgdesc",&propValue);
pFrameMetadataQueryWriter->Release();
PropVariantClear(&propValue);
*/
do { result = pConverter->Release();} while(result > 0);
do { result = pBitmapFrameEncode->Release();} while(result > 0);
do { result = pWICBitmap->Release();} while(result > 0);
}
 

 

Fully releasing WIC objects is important

When you get a WIC object, such as when calling QueryInterface, the object's reference count is incremented, and decremented when you call Release() on that object.  The Release() call returns the new reference count, and for the object to be fully released, the reference count must be 0.

When WIC is accessing image objects, there are often internal and undocumented increments to an object's reference count. Hence, in pseudocode...

WICobject->GetOtherObject(&pOtherWICObject); // increments reference count
... actions using pOtherWICObject
count = pOtherWICObject->Release();

does not necessarily mean that pOtherWICObject is fully released from memory. It may have no effect, but can also lock the object, preventing it from access again. An example is writing "/grctlext/Delay" data to the global metadata region. The data is written successfully, and the image will display in a browser, but trying to access the same image file again in code result in an "object locked by another process" error. This internal WIC process does not appear in the process list and cannot be stopped except by logging out or rebooting.

Hence, the correct and robust code is

WICobject->GetOtherObject(&pOtherWICObject); // increments reference count
... actions using pOtherWICObject
do { count = pOtherWICObject->Release();} while (count > 0);

Writing frame metadata to "/grctlext"

A WIC Query Reader can read data from this tree, and the most common is the "/grctlext/Delay" value. If a GIF created with other software is queried, a non-zero number will be returned.

We more or less followed an MSDN article on writing metadata, in sequence creating a frame, writing the metadata, then committing (i.e. saving) the frame. We found that "/imgdesc" data could be written, and verified in a binary edit of the created GIF file.

A WIC Query Writer writes to "/grctlext" and returns an S_OK value, but it seems that the data is never written. When a Query Reader checks it, all "/grctlext" values return 0. There's no other error that provides any clue. We might conclude that the WIC libraries have a bug that prevents writing to the "/grctlext" tree.

This is not actually a real problem, as the Delay value, if zero. results in a default frame Duration of 100ms.

Digging a bit deeper in the "/grctlext" bug

Using the Visual Studio binary editor, we can get a listing of the GIF file contents. Refering to the GIF Specification, Section 23, Graphic Control Extension (grctlext), we see that a grctlext starts with hex 21 F9 04 Only frames have grctlext blocks, and all contain 4 bytes plus a terminator byte.

Hence, for a sample GIF created with WIC, and having six frame grctlext blocks, we can expect to find six hex 21 F9 04 entries. Decoding ...

  1. 21 F9  04    01 00 00 FF     00 (block terminator)   frame #1
  2. 21 F9  04    01 00 00 FF     00 (block terminator)   frame #2
  3. 21 F9  04    01 00 00 FF     00 (block terminator)   frame #3
  4. 21 F9  04    01 00 00 FF     00 (block terminator)    frame #4
  5. 21 F9  04    01 00 00 FF     00 (block terminator)    frame #5
  6. 21 F9  04    01 00 00 FF     00 (block terminator)    frame #6

01 Packed Fields = Reserved            3 Bits
                             Disposal Method     3 Bits
                             User Input Flag         1 Bit
                             Transparent Color Flag 1 Bit set

00 00 Frame Delay LSB, MSB no delay, default to 100ms

FF Transparent colour index == 255

What happens if we manually edit the metadata blocks, setting the LSB to 15 (0x0F) ? A GIF reader app successfully reads the correct value.

Finally we discovered the cause of the problem. When the frame has been loaded or created it must first be written to the file by the encoder, i.e. by calling ...

hr = pBitmapFrameEncode->Commit();

This is what we think happens:

  1. Commit() writes the frame data, and sets up default "/grctlext" blocks.
  2. If the "/grctlext" blocks are written before the Commit(), there's probably nowhere for them to be written. The bug is that WIC returns an S_OK anyway. A following Commit() will overwrite the blocks if they do in fact exist somewhere in memory.
  3. Since "/grctlext" blocks are optional, Commit() is required first. "/imgdesc" blocks are not optional, and these can be written at any time, including before Commit() is called.

A related issue is the supposed need to write the root metadata encoder as a VT_UNKNOWN. In testing we found

  1. global data (say, 4 values) written to "/appext" resulted in four "NETSCAPE2.0" entries in the GIF file, a corruption of the format, although the file still displayed in a browser.
  2. Same applies to "/logscrdesc" data fields.
  3. Same for comments in "/commentext/TextEntry". If the root is written, two copies of the comment are stored.
  4. Same for frame data in the "/grctlext" and "/imgdesc" fields.

We suspect that the MSDN article was written using an early WIC version, and the current version does not require VT_UNKNOWN entries, or handles them internally.

It's rather unclear what the point of writing the VT_UNKNOWN value, which is the QueryWriter interface, might be. Possibly its intent is a signal to the browser that WIC should be used. In any case, it seems to make no difference when not present.

Even though writing "/grctlext" values now works, there's another bug. We expected that the "frameIndex" code loop above would write the Delay value into frames #0 to #n. In fact, it writes the Delay values to frames #1 to #n-1. This was verified with the binary editor.

The question now is, why? Is this a classic "1-off" pointer bug, or is the Commit() incrementing some internal frame pointer? Either way, it sure looks like a bug.

We fixed it with this work-around. From the GIF spec, the "/grctlext" block starts with hex 21 F9.

 

// In the frame creation loop, write the Delay values into the
// correct frame metadata entry. Our example has 6 frames.
 

if (frameIndex < 5)
{
propValue.vt = VT_UI2;
    propValue.uiVal = (WORD)(frameDelayValues[frameIndex + 1] /10);
    hr = pFrameMetadataQueryWriter->SetMetadataByName
           (L
"/grctlext/Delay", &propValue);
    PropVariantClear(&propValue);
}

// ... and finally, once the WIC operations have completed ...
// open the file and fix the bug entry.

HANDLE hGifFile = CreateFile(pOutputFilePath,
GENERIC_READ|GENERIC_WRITE,0,NULL,
OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL, NULL);

if (NULL != hGifFile)
{
    DWORD dwBytesRW;
    BYTE buffer[100];
    LPBYTE ptr = buffer;
    ReadFile(hGifFile,buffer,100,&dwBytesRW,NULL);
    SetFilePointer(hGifFile,0,NULL,FILE_BEGIN);
    // OK, now we can fix the bug. Find 21 F9, add 2 bytes
    //
    while ((*ptr != 0x21)||(*(ptr+1) != 0xF9))  ptr++; 
    // Should be at first grctlext block.
    ptr += 4; // should be the target Delay value
    *ptr = (BYTE)(frameDelayValues[0] / 10);
    WriteFile(hGifFile,buffer,100,&dwBytesRW,NULL);
    CloseHandle(hGifFile);
}

 

If you have a more complete explanation or a code variation that does not have the bug, do please let us know.

Finally, to verify animated GIFs created using WIC, look on MSDN for the Microsoft GIFAnimator.exe. This dates from 1996, and still works on Windows 10, 8 and 7. The UI is understandably a bit dated, but it works well and allows all the metadata regions to be inspected and adjusted.

Got a question on this code? Email Technical Support

Like us on Facebook.


Since you are here, do please have a look around the rest of our website. Tell us what you think.


Copyright 2012-2019 PiXCL Automation Technologies Inc, Canada. All Rights Reserved.