To load and play an animated GIF or WebP file, you can load a list of bitmaps from the file, then play the list to a target bitmap in a loop that displays each change in the target bitmap. You can also create an animation sequence from scratch and save the bitmaps to create an animated GIF or WebP file.
The following is an outline of possible steps:
Create and load a bitmap list using L_LoadBitmapList.
Use L_CreateBitmap to create a target bitmap that is the size of the images in the bitmap list.
Use L_CreatePlayback to create an animation playback that references bitmap list and the target bitmap.
In a loop that processes each bitmap in the list, do the following:
Use L_ProcessPlayback to process the current state and get the next state of the playback.
Use L_GetPlaybackUpdateRect to get the update rectangle (the portion of the target bitmap that has changed).
Use L_PaintDC to paint the changed portion of the bitmap. To paint only the changed portion, use the update rectangle as the source clipping rectangle.
The playback loop can have a number of states, and you should only paint the changes in an appropriate state (for example, when the next state is PLAYSTATE_POSTRENDER). For a list of possible states, refer to Animation Playback States.
The DisposalMethod field in the BITMAPHANDLE for the target bitmap determines what happens to the bitmap in the playback loop after rendering and after any wait state, when the next state is PLAYSTATE_POSTDISPOSE. The options are as follows: keep the image as it is, restore the background, or restore the previous image. (Restoring the background is a common option for animation.)
The playback loop goes continuously through the list of bitmaps. If you want to stop at the end of the list, you can exit the loop when the next state is PLAYSTATE_END.
Before you load an animated GIF or WebP file, use the L_FileInfo function to get the file's global animation information, which is updated in the following FILEINFO fields: Flags
, GlobalLoop
, GlobalWidth
, GlobalHeight
, GlobalBackground
, and GlobalPalette
(GIF only). You can use these values in the animation playback.
When you load an animated GIF or WebP file, using L_LoadBitmapList, the following animation fields in each BITMAPHANDLE are updated with information from the file: Left
, Top
, Delay
, Background
, Transparency
(GIF only), and DisposalMethod
. These fields are used in the animation playback.
When you save an animated GIF or WebP file, using L_SaveBitmapList, these same BITMAPHANDLE fields are saved in the file. In addition, if you want to save the global animation information, use the SAVEFILEOPTION structure to set the following fields: Flags
, GlobalLoop
, GlobalWidth
, GlobalHeight
, GlobalBackground
, and GlobalPalette
(GIF only). Notice that the field names are the same as in the FILEINFO structure (although the values of the Flags field are different).
GIF animations are <= 8 bits/pixel, therefore they use palettes. One of the palette colors might be considered transparent.
WebP animations are 24 or 32-bit, with the latter being most common. They use alpha channels to implement transparency. The alpha channel value for a pixel determines its transparency, translucency, and opaqueness as follows:
Therefore, WebP animations do not use the SAVEFILEOPTION.GlobalPalette or BITMAPHANDLE.Transparency settings.
Some of the frames in a WebP animations are supposed to be combined with the current output frame using an alpha blend operation. This is the case if BITMAPHANDLE.Flags.AlphaBlend = 1. This can be achieved with the L_CombineBitmap function. L_ProcessPlayback will automatically take this into account and perform the alpha blend operation if necessary.
Code examples for supporting functions show how to implement various features. Refer to the following function descriptions:
Function | What the Example Shows |
---|---|
L_ProcessPlayback | This example loads a bitmap list from an animated GIF or WEBP file. It then plays the animation, using the current bitmap as the target. |
L_AppendPlayback | This example plays an animated GIF or WEBP file as it loads the file. The example includes both the calling function and the FILEREADCALLBACK function, which displays the image. |
L_CreateBitmapList | This example creates a bitmap list from the current bitmap, rotating each copy by 10 degrees. It defines the rotation fill color as a transparent color, and assigns an animation background color. It then plays the animation, using the current bitmap as the target. |
L_GetBitmapListItem | This example changes the hue of bitmaps in a list, then updates the paint palette and the target bitmap's palette for animation playback. When changing the hue, it preserves the transparent color in the last palette entry. |
When you save animations, you have two choices:
Add all the animation frames to a HBITMAPLIST and create the animation using L_SaveBitmapList. This method is simple, but uses a lot of memory. This examples shows this approach:
/* This example shows how to save the animation using L_SaveBitmapList.
All the animation frames have already been loaded into a HBITMAPLIST */
L_INT SaveWebpAnimation(HBITMAPLIST hList, L_INT nAnimationWidth, L_INT nAnimationHeight,
L_COLORREF crBackground) /* Use -2 if no background color */
{
/* Set the animation settings in the SAVEFILEOPTION structure */
SAVEFILEOPTION saveFileOption;
L_GetDefaultSaveFileOption(&saveFileOption, sizeof(SAVEFILEOPTION));
saveFileOption.GlobalWidth = nAnimationWidth;
saveFileOption.GlobalHeight = nAnimationHeight;
if ((L_INT)crBackground != -2)
{
saveFileOption.Flags |= ESO_GLOBALBACKGROUND;
saveFileOption.GlobalBackground = fileInfo.GlobalBackground;
}
/* We are about to save the file. You can call L_SetComment and L_SetTag here if you
want to write any metadata into an Exif chunk that will be saved with the file */
/* Use lossy compression with qFactor = 20. Change this to 0 for lossless compression */
return L_SaveBitmapList(L_TEXT("anim.web"), hList, FILE_WEBP_ANI, 0, 20, &saveFileOption);
}
Save the animation frame by frame by appending each frame using L_SaveFile or L_SaveBitmap. This is useful when you do not have all the frames or you do not want to use too much memory.
For example, you can use this approach with the LEAD Video Callback Filter (2.0) from the multimedia toolkit to convert a fragment from a source video to animated webp. The filter calls a callback every time it receives a frame. In the callback, use L_SaveFile to save each frame.
This example shows how to save an animation a frame at a time:
/* Save the animation frame-by-frame using L_SaveFile.
This approach is useful when you don't have all the frames.
*/
L_INT TestSaveWebpAnimationExample()
{
L_TCHAR* pszSrcFile = L_TEXT("SourceFile.tif"),
* pszDstFile = L_TEXT("FrameByFrame.q20.webp");
FILEINFO fileInfo = { sizeof(FILEINFO) };
LOADFILEOPTION loadFileOption;
L_GetDefaultLoadFileOption(&loadFileOption, sizeof(LOADFILEOPTION));
/* Get the number of pages in the source file.
If the source file is an animation, we also get the animation settings (size, background color, loop count) */
L_INT nRet = L_FileInfo(pszSrcFile, &fileInfo, sizeof(FILEINFO), FILEINFO_TOTALPAGES, &loadFileOption);
CHECK_RET(nRet);
/* Delete the output file if it exists. You can skip this step if you wish to merge
multiple animations into the same file */
DeleteFile(pszDstFile);
SAVEFILEOPTION saveFileOption;
L_GetDefaultSaveFileOption(&saveFileOption, sizeof(SAVEFILEOPTION));
saveFileOption.GlobalWidth = fileInfo.GlobalWidth;
saveFileOption.GlobalHeight = fileInfo.GlobalHeight;
if (fileInfo.Flags & FILEINFO_HAS_GLOBALBACKGROUND)
{
saveFileOption.GlobalBackground = fileInfo.GlobalBackground;
saveFileOption.Flags |= ESO_GLOBALBACKGROUND;
}
if (fileInfo.Flags & FILEINFO_HAS_GLOBALLOOP)
{
saveFileOption.GlobalLoop = fileInfo.GlobalLoop;
saveFileOption.Flags |= ESO_GLOBALLOOP;
}
/* Add an Exif chunk with some metadata. This is optional, you can take these out */
L_SetComment(CMNT_SZDESC, (L_UCHAR*)"My Animation", 12);
L_SetComment(CMNT_SZSOFTWARE, (L_UCHAR*)"LEADTOOLS", 9);
L_SetTag(0x8001, TAG_ASCII, 10, "Custom tag");
L_INT nTotalPages = fileInfo.TotalPages;
for (loadFileOption.PageNumber = 1; loadFileOption.PageNumber <= nTotalPages; loadFileOption.PageNumber++)
{
BITMAPHANDLE bitmap;
nRet = L_LoadBitmap(pszSrcFile, &bitmap, sizeof(BITMAPHANDLE), 0, ORDER_BGRORGRAY, &loadFileOption, &fileInfo);
if (nRet == SUCCESS)
{
nRet = L_SaveFile(pszDstFile, &bitmap, FILE_WEBP_ANI, 0, 20, SAVEFILE_MULTIPAGE, NULL, NULL, &saveFileOption);
L_FreeBitmap(&bitmap); /* Free bitmap whether L_SaveFile succeeded or not */
}
if (nRet != SUCCESS)
return nRet;
}
return SUCCESS;
}
L_SaveBitmapList
for better compression. The animation encoder has access to all the frames in this case and it optimizes the output file to save only the differences between frames.
struct ANIMOPTIONS
{
L_TCHAR* pszFileName; // Output file name
L_VOID* pDIBits; // pointer to the DIB bits
HBITMAPLIST hList; // bitmap list to be filled with all the animation frames
BITMAPINFO bitmapInfo; // structure describing the DIB bits
L_INT nNextSavePercentage; // progress percentage when I should do the next save
L_INT nPercentageIncrement; // progress percentage increments between each save
};
/* Every time SaveToWebpAniCallback is called, the DIB bits from ANIMOPTIONS.pDIBits get updated
with the output from L_PaintDCEffect. This callback will get called up to 100 times,
but we only want to create about 10 frames. */
static L_INT EXT_CALLBACK SaveToWebpAniCallback(L_INT nPercentComplete, L_VOID* pUserData)
{
ANIMOPTIONS* pOptions = (ANIMOPTIONS*)pUserData;
if (!pUserData)
return ERROR_NULL_PTR;
if (nPercentComplete >= pOptions->nNextSavePercentage)
{
/* turn off the callback so we don't get called buy one of the LEADTOOLS functions */
L_SetStatusCallBack(NULL, NULL, NULL, NULL);
BITMAPHANDLE bitmap;
L_INT nRet = L_CreateBitmap(&bitmap, sizeof(BITMAPHANDLE), TYPE_CONV, pOptions->bitmapInfo.bmiHeader.biWidth, pOptions->bitmapInfo.bmiHeader.biHeight,
pOptions->bitmapInfo.bmiHeader.biBitCount, ORDER_BGR, NULL, BOTTOM_LEFT, NULL, NULL);
if (nRet == SUCCESS)
{
// the size of the BITMAPHANDLE bitmap should be the same as the size of the DIB
assert(bitmap.Size64 == pOptions->bitmapInfo.bmiHeader.biSizeImage);
bitmap.Delay = 200; // use 200ms delay so the animation doesn't paint too quickly
// Copy the DIB bits into our bitmap
L_AccessBitmap(&bitmap);
nRet = (DWORD)L_PutBitmapRow(&bitmap, (L_UCHAR *)pOptions->pDIBits, 0, pOptions->bitmapInfo.bmiHeader.biSizeImage);
L_ReleaseBitmap(&bitmap);
if ((DWORD)nRet == pOptions->bitmapInfo.bmiHeader.biSizeImage)
{
// If the insert is successful, the bitmap will be freed when we destroy the list
// If the insert fails, we need to free the local bitmap to avoid a memory leak
nRet = L_InsertBitmapListItem(pOptions->hList, (L_UINT)-1, &bitmap);
if (nRet != SUCCESS)
L_FreeBitmap(&bitmap);
}
else if (nRet > 0)
nRet = ERROR_INTERNAL;
}
if (nRet == SUCCESS)
{
if (pOptions->nNextSavePercentage < 100)
pOptions->nNextSavePercentage = L_MIN(pOptions->nNextSavePercentage + pOptions->nPercentageIncrement, 100);
else
pOptions->nNextSavePercentage = 101;
}
// Re-enable the callback
L_SetStatusCallBack(SaveToWebpAniCallback, pUserData, NULL, NULL);
return nRet;
}
return SUCCESS;
}
L_INT TestAnimFromSpecialEffects()
{
L_TCHAR* pszSrcFile = L_TEXT("eagle.cmp");
L_TCHAR* pszDstFile = L_TEXT("SpecialEffects-SaveBitmapList.webp");
FILEINFO fileInfo = { sizeof(FILEINFO) };
L_FreeBitmap(&Data.BitmapHandle);
L_INT nRet = L_LoadBitmap(pszSrcFile, &Data.BitmapHandle, sizeof(BITMAPHANDLE), 24, ORDER_BGR, NULL, NULL);
if(nRet != SUCCESS)
return nRet;
L_HDC hdc = CreateCompatibleDC(NULL);
if (!hdc)
return ERROR_GDI_ERROR;
#define PERCENTAGE_INCREMENT 10 // paint every 10%
ANIMOPTIONS options = { pszDstFile, NULL, NULL, {0}, PERCENTAGE_INCREMENT, PERCENTAGE_INCREMENT };
options.bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
options.bitmapInfo.bmiHeader.biWidth = Data.BitmapHandle.Width;
options.bitmapInfo.bmiHeader.biHeight = Data.BitmapHandle.Height;
options.bitmapInfo.bmiHeader.biBitCount = 24;
options.bitmapInfo.bmiHeader.biPlanes = 1;
options.bitmapInfo.bmiHeader.biSizeImage = ((options.bitmapInfo.bmiHeader.biWidth * 3 + 3) & ~3) * options.bitmapInfo.bmiHeader.biHeight;
HBITMAP hDIB = CreateDIBSection(hdc, &options.bitmapInfo, DIB_RGB_COLORS, &options.pDIBits, NULL, 0);
if (!hDIB)
{
DeleteDC(hdc);
return ERROR_NO_MEMORY;
}
HGDIOBJ hOldBitmap = SelectObject(hdc, hDIB);
nRet = L_CreateBitmapList(&options.hList);
if (nRet == SUCCESS)
{
STATUSCALLBACK pOldStatusCallback;
L_VOID* pOldUserData;
L_SetStatusCallBack(SaveToWebpAniCallback, &options, &pOldStatusCallback, &pOldUserData);
RECT rcDest = { 0, 0, Data.BitmapHandle.Width, Data.BitmapHandle.Height };
nRet = L_PaintDCEffect(hdc, &Data.BitmapHandle, NULL, NULL, &rcDest, NULL, SRCCOPY, EFFECT_WIPE_DOWN);
L_SetStatusCallBack(pOldStatusCallback, pOldUserData, NULL, NULL); // restore previous callback
}
if (nRet == SUCCESS)
{
SAVEFILEOPTION saveFileOption;
L_GetDefaultSaveFileOption(&saveFileOption, sizeof(SAVEFILEOPTION));
saveFileOption.GlobalLoop = 5;
saveFileOption.Flags |= ESO_GLOBALLOOP;
saveFileOption.GlobalWidth = Data.BitmapHandle.Width;
saveFileOption.GlobalHeight = Data.BitmapHandle.Height;
nRet = L_SaveBitmapList(pszDstFile, options.hList, FILE_WEBP_ANI, 0, 20, &saveFileOption);
}
L_DestroyBitmapList(options.hList); // free hList and all the bitmaps inside it
SelectObject(hdc, hOldBitmap);
DeleteObject(hDIB); // pDIBits will be invalid after this
return nRet;
}
For a summary of the functions for creating and managing a bitmap list, refer to Creating and Maintaining Lists of Images.
For a summary of the animation playback functions, refer to Playing Animated Images.