GO处理GIF图片(建议直接python处理)

88 阅读4分钟

定义: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
		}
	}

举个结果的例子:在这里插入图片描述