定义:GIF 文件是一种支持动画的图像格式,它可以包含多个帧(frames)以创建动画效果。GIF 动画不仅仅是简单地连续播放一系列静态图片,它还涉及到复杂的帧间关系和处置方法:
- Disposal Methods:每个帧可以指定一个处置方法,指示在显示下一帧之前如何处理当前帧。常见的处置方法包括:
- DisposalNone (1):不做特殊处理,下一帧直接叠加在当前帧之上。
- DisposalBackground (2):清除当前帧覆盖的区域,并用背景色填充。
- DisposalPrevious (3):恢复到前一帧的状态。(这意味着撤销当前帧所做的任何更改,回到应用当前帧之前的状态)
- 其他:默认跟DisposalNone(1)一样的处理方式
- 透明度和局部更新:某些帧可能仅更新图像的一部分区域,其余部分保持不变或依赖于之前的帧。
- 全局和局部颜色表:每个GIF文件可能有一个全局颜色表,同时每帧也可以有自己的局部颜色表。当提取帧时,需要注意当前帧是否复用了前一帧。
- 背景色:GIF允许指定一个背景色,这对于某些具有透明像素的帧尤为重要。如果一帧的部分区域被设置为透明,会显示该背景色,这里我们在代码的15行会提取背景色。 这些特性使得从 GIF 中提取特定帧变得复杂,因为需要正确处理帧间的依赖关系。 调研结果:go暂无封装好的gif取帧相关库(语言原因(go对于调用gpu进行图像处理的库暂无,一般只能调用c/c++库间接调用) go应用领域原因(go语言针对的大多是云计算 分布式等))
// extractFrame 从GIF图片中提取指定帧
func extractFrame(ctx context.Context, img *gif.GIF, framesToExtract []int, url string) ( errCode int) {
// 初始化错误代码为图片帧提取错误
errCode = constants.ERRSImageFrame
// 初始化结果切片,预分配3个元素的容量
base64Data = make([]string, 0, 3
LenImg := len(img.Image) // 获取GIF图片的帧数
// 初始化全局画布(严格使用GIF的全局尺寸)
canvas := image.NewRGBA(image.Rect(0, 0, img.Config.Width, img.Config.Height))
// 设置初始背景色(根据GIF配置)
var bgColor color.Color = color.Transparent // 默认为透明
if palette, ok := img.Config.ColorModel.(color.Palette); ok && len(palette) > 0 && img.BackgroundIndex >= 0 && int(img.BackgroundIndex) < len(palette) {
// 如果GIF有调色板且背景索引有效,则使用调色板中的背景色
bgColor = palette[img.BackgroundIndex]
}
// 用背景色填充画布
draw.Draw(canvas, canvas.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src)
lenFrameList := len(framesToExtract)
index := 0
// 遍历GIF图片的每一帧
for frameIndex := 0; frameIndex < LenImg; frameIndex++ {
// 创建一个缓冲区来存储图像数据
var buffer bytes.Buffer
srcFrame := img.Image[frameIndex] // 获取当前帧
disposal := img.Disposal[frameIndex] // 获取当前帧的处置方式
bounds := srcFrame.Bounds() // 获取当前帧的边界
// 深拷贝当前画布状态(用于DisposalPrevious恢复)
var prevCanvas *image.RGBA
if disposal == gif.DisposalPrevious {
prevCanvas = image.NewRGBA(canvas.Bounds())
copy(prevCanvas.Pix, canvas.Pix)
}
// 处理当前帧绘制
switch disposal {
case gif.DisposalNone:
// 不进行特殊处理,直接绘制
draw.Draw(canvas, bounds, srcFrame, bounds.Min, draw.Over)
case gif.DisposalBackground:
// 先清空区域为背景色,再绘制当前帧
draw.Draw(canvas, bounds, &image.Uniform{C: bgColor}, image.Point{}, draw.Src)
draw.Draw(canvas, bounds, srcFrame, bounds.Min, draw.Over)
case gif.DisposalPrevious:
// 绘制当前帧到临时画布,然后替换为临时画布
tmpCanvas := image.NewRGBA(canvas.Bounds())
copy(tmpCanvas.Pix, canvas.Pix)
draw.Draw(tmpCanvas, bounds, srcFrame, bounds.Min, draw.Over)
canvas = tmpCanvas
default:
// 对于未知的处置方法,采取与DisposalNone相同的做法
draw.Draw(canvas, bounds, srcFrame, bounds.Min, draw.Over)
}
// 如果当前帧是需要提取的帧之一
if framesToExtract[index] == frameIndex {
// 使用JPEG编码将图像写入缓冲区
if err := jpeg.Encode(&buffer, canvas, nil); err != nil {
// 记录错误日志
......
// 返回错误代码
return nil, errCode
}
// 如果已经提取了所有需要的帧,则退出循环
if index >= lenFrameList-1 {
break
} else {
index++
}
}
// 恢复DisposalPrevious的原始画布
if disposal == gif.DisposalPrevious && prevCanvas != nil {
canvas = prevCanvas
}
}
// 返回成功代码
return constants.CodeSuccess
}
举例子: 你使用下面这种方法得到的图片就是缺少背景色或者是图片白点(你可以使用下面的方法将图片保存到你电脑上)
// 提取GIF图片帧并保留
for _, frameIndex := range framesToExtract {
frame := img.Image[frameIndex]
// 创建一个缓冲区来存储图像数据
var buffer bytes.Buffer
// 使用 JPEG 编码将图像写入缓冲区
if err := jpeg.Encode(&buffer, frame, nil); err != nil {
msg := fmt.Sprintf("failed to encode image frame %s: %d", name, frameIndex)
resource.LoggerService.Warning(ctx, msg)
// 失败了不执行取下一个帧 直接返回错误
return nil, errCode
}
}
举个结果的例子: