0.前言
GIF 是 Graphics Interchange Format (图形交换格式)的缩写,采用 Lempel-Zev-Welch(LZW)压缩算法,最高支持256种颜色。GIF 原本是有专利限制的,不过早就过期了,估计这也是 Qt 等开源库/框架只有对 GIF 解码没有编码操作的原因吧,但 ImageMagick 和 FFmpeg 等是支持 GIF 编解码操作的。
GIF 有 GIF87a 和 GIF89a 两个版本。
GIF87a 版本是 1987 年推出的,一个文件存储一个图像,严格不支持透明像素;GIF87a 采用 LZW 压缩算法,它能够在保持图像质量的前提下将图像尺寸压缩百分之二十到二十五。
GIF89a 版本是 1989 年推出的很有特色的版本,该版本允许一个文件存储多个图像,可实现动画功能,允许某些像素透明。在这个版本中,为 GIF 文档扩充了图形控制区块、备注、说明、应用程序编程接口 4 个区块,并提供了对透明色和多帧动画的支持。
其中 GIF89a 在透明、隔行交错和动画 GIF 方面做出了重大改进。首先是支持透明,GIF89a 允许图片中的某些部分不可见。这项特性非常重要,使得我们在某些场合能够利用这样一种特性来使图像的边缘不再呈现出矩形边框,而变成我们想要的任意形状。这些透明区域,可以很方便地在 Photoshop、Fireworks 中生成并且导出为 GIF89a 格式的 GIF 图片来实现。当然,透明并不意味着边框就不再存在事实上,它是存在的,只不过不显示罢了,这样可以使插入的图片和整体网页更加协调。
以上复制粘贴自百度, GIF 格式示意图如下:
而 giflib 是一个可以用来编解码 GIF 格式文件的 C 语言库,项目主页:http://giflib.sourceforge.net/
(为什么选择 giflib 呢?因为想在 C++ Qt 里生成 GIF 实在没找到好的库,另一个比较轻便的 gif.h 感觉不太好用,翻来覆去才找到 giflib 这个东西。可惜的是网上只有解码的代码,编码的代码几乎没有, giflib 的示例也很简单。最后,我也是折腾了很久才调通了最简单的 GIF 编码接口)
文本用例完整代码(配合 Qt 使用):https://github.com/gongjianbo/QtGifTool
1.MSVC 编译
giflib 下载:https://sourceforge.net/projects/giflib/files/
我做的工程模板,用于生成 MSVC 版的 lib:https://github.com/gongjianbo/GifLib
从下载历史来看,最近下载最多的版本是 5.1.4 和 5.2.1 ,5.2.1 里移除了一个 GifQuantizeBuffer 函数,该函数可以将 rgb 数据转为 256 色数据并生成颜色表,由于没找到其他替代的接口,我编译 5.2.1 的时候又把该接口加上了。这两个版本目录也有点不一样,编译库只需要这 11 个文件就行了(5.1.4 的 lib 里):
一个简单的 CMakeList:
cmake_minimum_required (VERSION 3.8)
project (GifLib
VERSION 5.2.1
)
include_directories(${PROJECT_SOURCE_DIR}/source)
aux_source_directory(${PROJECT_SOURCE_DIR}/source source_files)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR})
file(GLOB head_files "${PROJECT_SOURCE_DIR}/source/*.h")
add_library(GifLib STATIC ${head_files} ${source_files})
install(FILES ${head_files} DESTINATION include)
IF (MSVC)
# add_definitions(-D_CRT_NONSTDC_NO_DEPRECATE)
# add_definitions(-D_CRT_SECURE_NO_WARNINGS)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd")
set(build_flags "-D_CRT_NONSTDC_NO_DEPRECATE -D_CRT_SECURE_NO_WARNINGS")
set_target_properties(GifLib PROPERTIES COMPILE_FLAGS ${build_flags})
ENDIF ()
当然,这时候 MSVC 还是编译不过的,需要把源码里的 unistd.h 注释掉:
#ifndef _WIN32
#include <unistd.h>
#endif // !_WIN32
最后点生成,然后安装,就得到了 lib 和 include ,可以在项目中使用了。
在 Qt 中先测试一下:
INCLUDEPATH += $$PWD/GifLib_VS2019x64/include
LIBS += $$PWD/GifLib_VS2019x64/lib/GifLib.lib
//#include "gif_lib.h"
//测试接口是否可以调用
qDebug()<<"Version:"<<GIFLIB_MAJOR<<GIFLIB_MINOR<<GIFLIB_RELEASE;
qDebug()<<"GifErrorString:"<<GifErrorString(D_GIF_ERR_NOT_GIF_FILE);
2.GIF 转 QImages
主要参照 gif2rgb.c 和 gifbuild.c 两个示例,可以通过高级接口 DGifSlurp/EGifSpew 读写,或者顺序 I/O 操作。
(2022-05-16 补充)解析时,每帧之间的时间间隔可能是不一样的,如果要还原原本的状态,需要保存每帧之间的间隔,我这个 demo 目前只保留了一个间隔值。
此处展示将 GIF 转换为 QImage 列表的基本操作,目前未处理透明:
struct GifInfo
{
int width=0;
int height=0;
int interval=0;
int colors=0;
int loopcount=0;
QList<QImage> images;
};
QString gif_path;
qDebug()<<"gif to image:"<<gif_path;
const QByteArray path_temp = gif_path.toLocal8Bit();
const char *path_cstr = path_temp.constData();
int error_code;
//解码器打开文件
GifFileType *gif_file = DGifOpenFileName(path_cstr, &error_code);
if (gif_file == NULL) {
const char* gif_error = GifErrorString(error_code);
qDebug() << "open error:" << gif_error;
return;
}
//DGifSlurp相对于流式接口更方便点,一次解析所有
error_code = DGifSlurp(gif_file);
if(error_code!=GIF_OK){
qDebug()<<"gif slurp error.";
return;
}
//保存解析结果
GifInfo gif_info;
gif_info.width=gif_file->SWidth;
gif_info.height=gif_file->SHeight;
gif_info.colors=(1 << gif_file->SColorResolution);
//qDebug()<<"screen size(w*h)"<<gif_file->SWidth<<"*"<<gif_file->SHeight;
//qDebug()<<"screen colors"<<(1 << gif_file->SColorResolution)
// <<"screen bgcolor"<<gif_file->SBackGroundColor
// <<"pixel aspect byte"<<(unsigned)gif_file->AspectByte;
//存color index
QVector<GifByteType> screen_buf(gif_file->SWidth*gif_file->SHeight);
memset(screen_buf.data(),gif_file->SBackGroundColor,sizeof(GifByteType)*screen_buf.size());
//暂存上一帧图像
QImage img_temp=QImage(gif_file->SWidth,gif_file->SHeight,QImage::Format_RGB32);
//TransparentColor表示保留上一帧状态的color index
int trans_color = -1;
//逐帧转换为QImage
for (int im = 0; im < gif_file->ImageCount; im++)
{
SavedImage *image = &gif_file->SavedImages[im];
//获取间隔、TransparentColor等信息
for (ExtensionBlock *ep = image->ExtensionBlocks;
ep < image->ExtensionBlocks + image->ExtensionBlockCount;
ep++)
{
bool last = (ep - image->ExtensionBlocks == (image->ExtensionBlockCount - 1));
if (ep->Function == GRAPHICS_EXT_FUNC_CODE)
{
GraphicsControlBlock gcb;
if (DGifExtensionToGCB(ep->ByteCount, ep->Bytes, &gcb) == GIF_ERROR) {
qDebug()<<"invalid graphics control block";
return;
}
//qDebug()<<"disposal mode"<< gcb.DisposalMode;
//qDebug()<<"user input fla"<<(gcb.UserInputFlag ? "on" : "off");
//qDebug()<<"delay"<<gcb.DelayTime;
//qDebug()<<"transparent index"<<gcb.TransparentColor;
trans_color=gcb.TransparentColor;
//DelayTime的单位为10 ms
gif_info.interval=gcb.DelayTime*10;
}
else if (!last
&& ep->Function == APPLICATION_EXT_FUNC_CODE
&& ep->ByteCount >= 11
&& (ep+1)->ByteCount >= 3
&& memcmp(ep->Bytes, "NETSCAPE2.0", 11) == 0)
{
unsigned char *params = (++ep)->Bytes;
unsigned int loopcount = params[1] | (params[2] << 8);
//qDebug()<< "netscape loop "<<loopcount;
//第一帧一般包含循环次数的ExtensionBlock, 0则循环包房
gif_info.loopcount=loopcount;
}
else
{
//其他信息暂不处理
while (!last && ep[1].Function == CONTINUE_EXT_FUNC_CODE) {
++ep;
last = (ep - image->ExtensionBlocks == (image->ExtensionBlockCount - 1));
}
}
}
//image->ImageDesc指定了这帧图的刷新区域,类似于opencv的roi
//qDebug()<<"left"<<image->ImageDesc.Left
// <<"top"<<image->ImageDesc.Top
// <<"width"<<image->ImageDesc.Width
// <<"height"<<image->ImageDesc.Height
// <<"color"<<image->ImageDesc.ColorMap->ColorCount;
if (image->ImageDesc.Interlace)
qDebug()<<"image interlaced";
//如果没有局部色彩管理就用全局的
ColorMapObject *color_map = (image->ImageDesc.ColorMap ? image->ImageDesc.ColorMap : gif_file->SColorMap);
if(color_map == NULL){
qDebug()<<"color map error.";
return;
}
//保留上一帧的图像主要是因为当前可能只刷新部分
QImage img=img_temp.copy();
GifColorType *color_map_entry;
//遍历行列,把每个像素点的颜色填到img
for (int i = image->ImageDesc.Top; i < image->ImageDesc.Top+image->ImageDesc.Height; i++)
{
memcpy(screen_buf.data()+i*gif_file->SWidth+image->ImageDesc.Left,
image->RasterBits+(i-image->ImageDesc.Top)*image->ImageDesc.Width,
image->ImageDesc.Width);
for (int j = image->ImageDesc.Left; j < image->ImageDesc.Left+image->ImageDesc.Width; j++)
{
if(trans_color!=-1&&trans_color==screen_buf[i*gif_file->SWidth+j])
continue;
color_map_entry = &color_map->Colors[screen_buf[i*gif_file->SWidth+j]];
img.setPixelColor(j,i,QColor(
color_map_entry->Red,
color_map_entry->Green,
color_map_entry->Blue));
}
}
img_temp=img;
gif_info.images.push_back(img);
}
//关闭文件
if (DGifCloseFile(gif_file, &error_code) == GIF_ERROR){
const char* gif_error = GifErrorString(error_code);
qDebug() << "close error:" << gif_error;
return;
}
qDebug()<<"gif to image finish";
3.QImages 转 GIF
主要参照 gif2rgb.c 和 gifbuild.c 两个示例,可以通过高级接口 DGifSlurp/EGifSpew 读写,或者顺序 I/O 操作。
此处展示将 QImage 列表转换为 GIF 的基本操作,目前未处理透明:
QList<QImage> gif_list;
QString gif_path;
qDebug()<<"image to gif begin:"<<gif_path;
const QByteArray path_temp = gif_path.toLocal8Bit();
const char *path_cstr = path_temp.constData();
QImage img_first(gif_list.first());
const int gif_width=img_first.width();
const int gif_height=img_first.height();
//qDebug()<<"width"<<gif_width<<"height"<<gif_height;
QVector<GifByteType> red_buffer(gif_width*gif_height);
QVector<GifByteType> green_buffer(gif_width*gif_height);
QVector<GifByteType> blue_buffer(gif_width*gif_height);
//以编码方式打开文件
int error_code;
GifFileType *gif_file=EGifOpenFileName(path_cstr,false,&error_code);
if(gif_file == NULL){
const char* gif_error = GifErrorString(error_code);
qDebug() << "open error:" << gif_error;
return;
}
gif_file->SWidth=gif_width;
gif_file->SHeight=gif_height;
gif_file->SColorResolution=8;
gif_file->SBackGroundColor=0;
gif_file->SColorMap=NULL;
gif_file->SavedImages=NULL;
gif_file->ImageCount=0;
gif_file->ExtensionBlockCount=0;
gif_file->ExtensionBlocks=NULL;
for(int i = 0; i < gif_list.count(); i++) {
QImage img(gif_list.at(i));
SavedImage *image = GifMakeSavedImage(gif_file, NULL);
if(img.width()<gif_width||img.height()<gif_height)
continue;
for(int row=0;row<gif_height;row++)
{
for(int col=0;col<gif_width;col++)
{
red_buffer[row*gif_width+col]=img.pixelColor(col,row).red();
green_buffer[row*gif_width+col]=img.pixelColor(col,row).green();
blue_buffer[row*gif_width+col]=img.pixelColor(col,row).blue();
}
}
int map_size=(1<<gif_file->SColorResolution);
if ((image->ImageDesc.ColorMap = GifMakeMapObject(map_size, NULL)) == NULL){
qDebug()<<"Failed to allocate memory required, aborted.";
return;
}
image->RasterBits = (GifPixelType *)malloc(sizeof(GifPixelType) * gif_width * gif_height);
//5.2.0 GifQuantizeBuffer函数已移除
//但是自己去做rgb转256色太麻烦,所以我又加上了
//map_size调用后会被修改为实际值
if (GifQuantizeBuffer(gif_width, gif_height, &map_size,
red_buffer.data(),
green_buffer.data(),
blue_buffer.data(),
image->RasterBits,
image->ImageDesc.ColorMap->Colors) == GIF_ERROR)
{
qDebug()<<"error";
return;
}
//qDebug()<<"MakeSavedImage"<<i<<"mapsize"<<map_size;
image->ImageDesc.Left = 0;
image->ImageDesc.Top = 0;
image->ImageDesc.Width = gif_width;
image->ImageDesc.Height = gif_height;
image->ImageDesc.Interlace = false;
//image->ImageDesc.ColorMap = color_map;
//image->RasterBits = out_buffer;
GraphicsControlBlock gcb;
gcb.DisposalMode = DISPOSAL_UNSPECIFIED;
gcb.DelayTime = interval/10;
gcb.UserInputFlag = false;
gcb.TransparentColor = NO_TRANSPARENT_COLOR;
//qDebug()<<"GCB To Saved"<<i;
EGifGCBToSavedExtension(&gcb, gif_file, i);
//把循环次数写到第一帧对应的扩展块
if(i==0){
unsigned char params[3] = {1, 0, 0};
//Create a Netscape 2.0 loop block
if (GifAddExtensionBlock(&image->ExtensionBlockCount,
&image->ExtensionBlocks,
APPLICATION_EXT_FUNC_CODE,
11,
(unsigned char *)"NETSCAPE2.0")==GIF_ERROR) {
qDebug()<<"out of memory while adding loop block.";
return;
}
//params[1] = (intval & 0xff);
//params[2] = (intval >> 8) & 0xff;
if (GifAddExtensionBlock(&image->ExtensionBlockCount,
&image->ExtensionBlocks,
0, sizeof(params), params) == GIF_ERROR) {
qDebug()<<"out of memory while adding loop continuation.";
return;
}
}
}
//qDebug()<<gif_file->ImageCount;
int saved_count=gif_file->ImageCount;
SavedImage *saved_images=gif_file->SavedImages;
//关闭GIF并释放相关存储。
EGifSpew(gif_file);
qDebug()<<gif_file->ImageCount;
{
SavedImage *sp;
for (sp = saved_images;
sp < saved_images + saved_count; sp++) {
if (sp->ImageDesc.ColorMap != NULL) {
GifFreeMapObject(sp->ImageDesc.ColorMap);
sp->ImageDesc.ColorMap = NULL;
}
if (sp->RasterBits != NULL)
free((char *)sp->RasterBits);
GifFreeExtensions(&sp->ExtensionBlockCount, &sp->ExtensionBlocks);
}
free((char *)saved_images);
saved_images = NULL;
sp=NULL;
}
qDebug()<<"image to gif finish.";
4.参考
giflib 文档及示例,主要是 gif2rgb.c 和 gifbuild.c 两个示例
博客:https://www.oschina.net/question/12_58381
博客:https://blog.csdn.net/binglingziyu/article/details/112756088