Go image/gif 包详解
概述
image/gif 包提供 GIF 图像的编码和解码功能。它支持静态 GIF 图像和 animated GIF(多帧动画)的处理。该包提供了 Encode、Decode 等核心函数,以及 GIF、Options 等结构体,广泛用于创建和读取 GIF 图像及动画。
包导入
import "image/gif"
基本使用
1. 编码静态 GIF 图像
package main
import (
"image"
"image/color"
"image/gif"
"os"
)
func main() {
// 创建图像
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
// 填充红色
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
img.Set(x, y, color.RGBA{255, 0, 0, 255})
}
}
// 编码为 GIF
file, _ := os.Create("output.gif")
defer file.Close()
gif.Encode(file, img, nil)
}
2. 解码 GIF 图像
package main
import (
"fmt"
"image/gif"
"os"
)
func main() {
// 打开 GIF 文件
file, _ := os.Open("input.gif")
defer file.Close()
// 解码 GIF
g, _ := gif.DecodeAll(file)
// 打印帧数
fmt.Printf("GIF 帧数:%d\n", len(g.Image))
fmt.Printf("第一帧尺寸:%dx%d\n", g.Image[0].Bounds().Dx(), g.Image[0].Bounds().Dy())
}
3. 创建 GIF 动画
package main
import (
"image"
"image/color"
"image/gif"
"os"
)
func main() {
// 创建多帧动画
var frames []*image.Paletted
var delays []int
// 生成 10 帧
for i := 0; i < 10; i++ {
img := image.NewPaletted(image.Rect(0, 0, 100, 100), palette.WebSafe)
// 填充不同颜色
c := color.RGBA{uint8(i * 25), 0, 0, 255}
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
img.Set(x, y, c)
}
}
frames = append(frames, img)
delays = append(delays, 10) // 每帧 10ms
}
// 编码动画
file, _ := os.Create("animation.gif")
defer file.Close()
gif.EncodeAll(file, &gif.GIF{
Image: frames,
Delay: delays,
})
}
一、核心函数
Decode
定义:
func Decode(r io.Reader) (image.Image, error)
说明:
- 功能:解码 GIF 图像,返回第一帧
- 参数:
r- io.Reader(如文件、字节流) - 返回值:
image.Image- 解码后的图像(第一帧)error- 错误信息
- 注意:只返回 GIF 的第一帧,不返回动画信息
示例:
package main
import (
"fmt"
"image/gif"
"os"
)
func main() {
file, _ := os.Open("input.gif")
defer file.Close()
// 解码第一帧
img, err := gif.Decode(file)
if err != nil {
fmt.Println("解码失败:", err)
return
}
// 获取图像尺寸
bounds := img.Bounds()
fmt.Printf("图像尺寸:%dx%d\n", bounds.Dx(), bounds.Dy())
}
DecodeAll
定义:
func DecodeAll(r io.Reader) (*GIF, error)
说明:
- 功能:解码完整的 GIF 图像(包括所有帧和动画信息)
- 参数:
r- io.Reader - 返回值:
*GIF- 包含所有帧和动画信息的结构体指针error- 错误信息
- 用途:读取 GIF 动画、获取所有帧、延迟信息等
示例:
package main
import (
"fmt"
"image/gif"
"os"
)
func main() {
file, _ := os.Open("animation.gif")
defer file.Close()
// 解码所有帧
g, err := gif.DecodeAll(file)
if err != nil {
fmt.Println("解码失败:", err)
return
}
// 打印动画信息
fmt.Printf("总帧数:%d\n", len(g.Image))
fmt.Printf("循环次数:%d (0=无限循环)\n", g.LoopCount)
fmt.Printf("背景色索引:%d\n", g.BackgroundIndex)
// 打印每帧信息
for i, frame := range g.Image {
fmt.Printf("帧 %d: 尺寸=%dx%d, 延迟=%dms\n",
i,
frame.Bounds().Dx(),
frame.Bounds().Dy(),
g.Delay[i]*10) // 延迟单位是 1/100 秒
}
}
Encode
定义:
func Encode(w io.Writer, img image.Image, o *Options) error
说明:
- 功能:将图像编码为 GIF 格式
- 参数:
w- io.Writer(如文件、字节流)img- 要编码的图像o- 编码选项(可为 nil,使用默认值)
- 返回值:
error- 错误信息 - 自动量化:如果图像不是调色板图像,会自动进行颜色量化
示例:
package main
import (
"image"
"image/color"
"image/gif"
"os"
)
func main() {
// 创建图像
img := image.NewRGBA(image.Rect(0, 0, 200, 200))
// 绘制渐变
for y := 0; y < 200; y++ {
for x := 0; x < 200; x++ {
img.Set(x, y, color.RGBA{
R: uint8(x),
G: uint8(y),
B: 128,
A: 255,
})
}
}
// 编码为 GIF(使用默认选项)
file, _ := os.Create("gradient.gif")
defer file.Close()
err := gif.Encode(file, img, nil)
if err != nil {
panic(err)
}
}
EncodeAll
定义:
func EncodeAll(w io.Writer, g *GIF) error
说明:
- 功能:编码完整的 GIF 动画(多帧)
- 参数:
w- io.Writerg- 包含所有帧和动画信息的*GIF结构体
- 返回值:
error- 错误信息 - 用途:创建 GIF 动画
示例:
package main
import (
"image"
"image/color/palette"
"image/draw"
"image/gif"
"os"
)
func main() {
// 创建动画帧
var frames []*image.Paletted
var delays []int
// 加载源图像
src := loadYourImage() // 假设已定义
// 创建 5 帧旋转动画
for i := 0; i < 5; i++ {
frame := image.NewPaletted(src.Bounds(), palette.Plan9)
// 这里应该进行图像旋转,简化示例
draw.Draw(frame, frame.Bounds(), src, image.Point{}, draw.Src)
frames = append(frames, frame)
delays = append(delays, 20) // 20ms = 0.2 秒
}
// 编码动画
file, _ := os.Create("rotation.gif")
defer file.Close()
err := gif.EncodeAll(file, &gif.GIF{
Image: frames,
Delay: delays,
})
if err != nil {
panic(err)
}
}
二、结构体
GIF
定义:
type GIF struct {
Image []*image.Paletted // 图像帧数组
Delay []int // 每帧延迟(单位:1/100 秒)
Disposal []byte // 每帧处理方式
BackgroundIndex byte // 背景色索引
LoopCount int // 循环次数(0=无限循环)
Config image.Config // 图像配置
}
字段说明:
| 字段 | 类型 | 描述 | 示例值 |
|---|---|---|---|
Image | []*image.Paletted | 所有帧的图像数据 | 3 帧动画 = 3 个元素 |
Delay | []int | 每帧延迟时间(1/100 秒) | 10 = 0.1 秒 |
Disposal | []byte | 每帧处理方式 | 0=不处理,1=保留,2=恢复背景色 |
BackgroundIndex | byte | 背景色在调色板中的索引 | 0 |
LoopCount | int | 循环次数 | 0=无限循环,1=播放 1 次 |
Config | image.Config | 图像配置(尺寸等) | - |
Disposal 取值说明:
| 值 | 名称 | 说明 |
|---|---|---|
| 0 | DisposalNone | 不处理,新帧叠加在旧帧上 |
| 1 | DisposalBackground | 用背景色填充帧区域 |
| 2 | DisposalPrevious | 恢复到上一帧状态 |
示例 - 创建完整 GIF 动画:
package main
import (
"image"
"image/color"
"image/color/palette"
"image/gif"
"os"
)
func main() {
// 创建 3 帧动画
frames := make([]*image.Paletted, 3)
delays := make([]int, 3)
disposal := make([]byte, 3)
for i := 0; i < 3; i++ {
// 创建帧
img := image.NewPaletted(image.Rect(0, 0, 100, 100), palette.WebSafe)
// 绘制不同位置的红点
cx := 50 + int(float64(i-1)*30)
for y := 45; y <= 55; y++ {
for x := cx - 5; x <= cx + 5; x++ {
img.SetColorIndex(x, y, 1) // 红色索引
}
}
frames[i] = img
delays[i] = 50 // 0.5 秒
disposal[i] = 1 // 每帧后恢复背景
}
// 创建 GIF
g := &gif.GIF{
Image: frames,
Delay: delays,
Disposal: disposal,
BackgroundIndex: 0,
LoopCount: 0, // 无限循环
}
// 编码
file, _ := os.Create("bouncing_ball.gif")
defer file.Close()
gif.EncodeAll(file, g)
}
Options
定义:
type Options struct {
NumColors int // 调色板中的颜色数量(最多 256)
Quantizer color.Quantizer // 颜色量化器
Drawer color.Drawer // 颜色绘制器
}
字段说明:
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
NumColors | int | 256 | 调色板颜色数量(1-256) |
Quantizer | color.Quantizer | nil | 颜色量化器(nil 使用中位切割) |
Drawer | color.Drawer | nil | 颜色绘制器(nil 使用默认) |
使用示例:
package main
import (
"image"
"image/color/palette"
"image/draw"
"image/gif"
"os"
)
func main() {
// 加载真彩色图像
src := loadYourImage()
// 创建选项
opts := &gif.Options{
NumColors: 64, // 只使用 64 色
Quantizer: nil, // 使用默认量化器
Drawer: draw.FloydSteinberg, // 使用抖动
}
// 编码
file, _ := os.Create("optimized.gif")
defer file.Close()
gif.Encode(file, src, opts)
}
三、常量
Disposal 常量
定义:
const (
DisposalNone = 0x00 // 不处理
DisposalBackground = 0x01 // 恢复背景色
DisposalPrevious = 0x02 // 恢复上一帧
)
使用示例:
package main
import (
"image"
"image/gif"
"os"
)
func main() {
// 创建两帧
frame1 := createFrame1()
frame2 := createFrame2()
// 设置不同的 Disposal 方式
g := &gif.GIF{
Image: []*image.Paletted{frame1, frame2},
Delay: []int{50, 50},
Disposal: []byte{gif.DisposalNone, gif.DisposalBackground},
LoopCount: 0,
}
file, _ := os.Create("animation.gif")
defer file.Close()
gif.EncodeAll(file, g)
}
四、典型示例
示例 1:将 PNG 转换为 GIF
package main
import (
"image/gif"
"image/png"
"os"
)
func pngToGIF(inputPath, outputPath string) error {
// 打开 PNG
file, err := os.Open(inputPath)
if err != nil {
return err
}
defer file.Close()
// 解码 PNG
img, err := png.Decode(file)
if err != nil {
return err
}
// 编码为 GIF
outFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.Encode(outFile, img, &gif.Options{
NumColors: 256,
})
}
示例 2:创建简单的加载动画
package main
import (
"image"
"image/color"
"image/color/palette"
"image/gif"
"os"
)
func createLoadingAnimation() {
frames := make([]*image.Paletted, 8)
delays := make([]int, 8)
// 创建 8 帧旋转动画
for i := 0; i < 8; i++ {
img := image.NewPaletted(image.Rect(0, 0, 64, 64), palette.WebSafe)
// 绘制旋转的点
angle := float64(i) * 3.14159 / 4
cx, cy := 32, 32
radius := 20
for j := 0; j < 4; j++ {
a := angle + float64(j)*3.14159/2
x := cx + int(float64(radius)*math.Cos(a))
y := cy + int(float64(radius)*math.Sin(a))
if x >= 0 && x < 64 && y >= 0 && y < 64 {
img.SetColorIndex(x, y, uint8(j+1))
}
}
frames[i] = img
delays[i] = 10 // 0.1 秒
}
file, _ := os.Create("loading.gif")
defer file.Close()
gif.EncodeAll(file, &gif.GIF{
Image: frames,
Delay: delays,
LoopCount: 0, // 无限循环
})
}
示例 3:读取 GIF 并提取所有帧
package main
import (
"fmt"
"image/gif"
"image/png"
"os"
"path/filepath"
)
func extractGIFFrames(gifPath, outputDir string) error {
// 打开 GIF
file, err := os.Open(gifPath)
if err != nil {
return err
}
defer file.Close()
// 解码所有帧
g, err := gif.DecodeAll(file)
if err != nil {
return err
}
// 保存每一帧为 PNG
for i, frame := range g.Image {
framePath := filepath.Join(outputDir, fmt.Sprintf("frame_%03d.png", i))
frameFile, err := os.Create(framePath)
if err != nil {
return err
}
err = png.Encode(frameFile, frame)
frameFile.Close()
if err != nil {
return err
}
fmt.Printf("已保存:%s\n", framePath)
}
return nil
}
示例 4:优化 GIF 文件大小
package main
import (
"image/gif"
"image/png"
"os"
)
func optimizeGIF(inputPath, outputPath string) error {
// 打开 GIF
file, err := os.Open(inputPath)
if err != nil {
return err
}
defer file.Close()
// 解码
g, err := gif.DecodeAll(file)
if err != nil {
return err
}
// 重新编码(减少颜色数量)
outFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outFile.Close()
// 使用更少的颜色和优化选项
opts := &gif.Options{
NumColors: 128, // 减少到 128 色
Quantizer: nil, // 默认量化器
}
// 如果是单帧
if len(g.Image) == 1 {
return gif.Encode(outFile, g.Image[0], opts)
}
// 如果是多帧,需要手动处理
// 这里简化处理,实际应该保留所有帧
return gif.Encode(outFile, g.Image[0], opts)
}
示例 5:创建文字滚动动画
package main
import (
"image"
"image/color"
"image/color/palette"
"image/draw"
"image/gif"
"os"
)
func createScrollingText(text string) {
const (
width = 200
height = 50
frames = 20
)
images := make([]*image.Paletted, frames)
delays := make([]int, frames)
for i := 0; i < frames; i++ {
img := image.NewPaletted(image.Rect(0, 0, width, height), palette.WebSafe)
// 填充黑色背景
draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{0, 0, 0, 255}}, image.Point{}, draw.Src)
// 这里应该使用 font 包绘制文字
// 简化示例:绘制移动的白色方块
x := (width - i*10) % (width + 20)
if x > width-20 {
x = -20
}
for y := 15; y <= 35; y++ {
for dx := 0; dx < 20; dx++ {
img.Set(x+dx, y, color.RGBA{255, 255, 255, 255})
}
}
images[i] = img
delays[i] = 5 // 0.05 秒
}
file, _ := os.Create("scrolling.gif")
defer file.Close()
gif.EncodeAll(file, &gif.GIF{
Image: images,
Delay: delays,
LoopCount: 0,
})
}
示例 6:GIF 帧信息分析
package main
import (
"fmt"
"image/gif"
"os"
)
func analyzeGIF(gifPath string) error {
file, err := os.Open(gifPath)
if err != nil {
return err
}
defer file.Close()
g, err := gif.DecodeAll(file)
if err != nil {
return err
}
fmt.Println("=== GIF 信息 ===")
fmt.Printf("尺寸:%dx%d\n", g.Config.Width, g.Config.Height)
fmt.Printf("总帧数:%d\n", len(g.Image))
fmt.Printf("循环次数:%d", g.LoopCount)
if g.LoopCount == 0 {
fmt.Println(" (无限循环)")
} else {
fmt.Println()
}
fmt.Printf("背景色索引:%d\n", g.BackgroundIndex)
fmt.Println("\n=== 帧详情 ===")
totalDuration := 0
for i, frame := range g.Image {
bounds := frame.Bounds()
delay := g.Delay[i] * 10 // 转换为毫秒
totalDuration += delay
fmt.Printf("帧 %d:\n", i)
fmt.Printf(" 尺寸:%dx%d\n", bounds.Dx(), bounds.Dy())
fmt.Printf(" 偏移:(%d, %d)\n", bounds.Min.X, bounds.Min.Y)
fmt.Printf(" 延迟:%dms\n", delay)
if i < len(g.Disposal) {
fmt.Printf(" 处理方式:%d\n", g.Disposal[i])
}
}
fmt.Printf("\n总时长:%dms (%.2f 秒)\n", totalDuration, float64(totalDuration)/1000)
return nil
}
五、最佳实践
1. 选择合适的颜色数量
// 场景 1:简单图形/图标 -> 较少颜色
opts1 := &gif.Options{NumColors: 32}
// 场景 2:照片/复杂图像 -> 更多颜色
opts2 := &gif.Options{NumColors: 256}
// 场景 3:平衡文件大小和质量
opts3 := &gif.Options{NumColors: 128}
2. 使用抖动优化质量
import (
"image/draw"
"image/gif"
)
// 使用 Floyd-Steinberg 抖动
opts := &gif.Options{
NumColors: 256,
Drawer: draw.FloydSteinberg,
}
gif.Encode(file, img, opts)
3. 优化动画文件大小
// 技巧 1:减少帧数
// 从 60fps 降到 24fps 或 15fps
// 技巧 2:减少颜色数量
opts := &gif.Options{NumColors: 64}
// 技巧 3:减小图像尺寸
// 在编码前缩放图像
// 技巧 4:使用合适的 Disposal 方式
// 避免不必要的背景恢复
4. 正确处理动画循环
// 无限循环
g.LoopCount = 0
// 播放指定次数
g.LoopCount = 3 // 播放 3 次
// 只播放一次
g.LoopCount = 1
5. 内存管理
// 对于大型 GIF,注意内存使用
file, _ := os.Open("large.gif")
defer file.Close()
g, err := gif.DecodeAll(file)
if err != nil {
// 处理错误
}
// 使用完后及时释放
// Go 的垃圾回收会自动处理
六、与其他包配合
1. 与 image/draw 配合
package main
import (
"image"
"image/color/palette"
"image/draw"
"image/gif"
"image/jpeg"
"os"
)
func jpegToAnimatedGIF(jpegPaths []string, outputPath string) error {
frames := make([]*image.Paletted, len(jpegPaths))
delays := make([]int, len(jpegPaths))
for i, path := range jpegPaths {
// 打开 JPEG
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
src, err := jpeg.Decode(file)
if err != nil {
return err
}
// 转换为调色板图像
frame := image.NewPaletted(src.Bounds(), palette.Plan9)
draw.Draw(frame, frame.Bounds(), src, src.Bounds().Min, draw.FloydSteinberg)
frames[i] = frame
delays[i] = 10 // 0.1 秒
}
// 编码 GIF
outFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.EncodeAll(outFile, &gif.GIF{
Image: frames,
Delay: delays,
LoopCount: 0,
})
}
2. 与 image/color/palette 配合
package main
import (
"image"
"image/color/palette"
"image/draw"
"image/gif"
"os"
)
func createWithCustomPalette(inputPath, outputPath string) error {
// 打开图像
file, err := os.Open(inputPath)
if err != nil {
return err
}
defer file.Close()
src, err := image.Decode(file)
if err != nil {
return err
}
// 使用 Plan9 调色板
dst := image.NewPaletted(src.Bounds(), palette.Plan9)
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.FloydSteinberg)
// 编码为 GIF
outFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.Encode(outFile, dst, nil)
}
3. 与 image/png 配合
package main
import (
"image/gif"
"image/png"
"os"
)
func pngToGIF(inputPath, outputPath string) error {
// 打开 PNG
file, err := os.Open(inputPath)
if err != nil {
return err
}
defer file.Close()
// 解码 PNG
img, err := png.Decode(file)
if err != nil {
return err
}
// 编码为 GIF
outFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.Encode(outFile, img, &gif.Options{
NumColors: 256,
})
}
4. 与 bytes 包配合(内存操作)
package main
import (
"bytes"
"image"
"image/gif"
)
// 编码到内存
func encodeToMemory(img image.Image) ([]byte, error) {
var buf bytes.Buffer
err := gif.Encode(&buf, img, nil)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// 从内存解码
func decodeFromMemory(data []byte) (image.Image, error) {
buf := bytes.NewReader(data)
return gif.Decode(buf)
}
七、快速参考
函数总览
| 函数名 | 参数 | 返回值 | 描述 |
|---|---|---|---|
Decode | r io.Reader | (image.Image, error) | 解码 GIF 第一帧 |
DecodeAll | r io.Reader | (*GIF, error) | 解码完整 GIF(所有帧) |
Encode | w io.Writer, img image.Image, o *Options | error | 编码静态 GIF |
EncodeAll | w io.Writer, g *GIF | error | 编码 GIF 动画 |
结构体总览
| 结构体名 | 字段 | 描述 |
|---|---|---|
GIF | Image, Delay, Disposal, BackgroundIndex, LoopCount, Config | GIF 动画数据结构 |
Options | NumColors, Quantizer, Drawer | 编码选项 |
常量总览
| 常量名 | 值 | 描述 |
|---|---|---|
DisposalNone | 0x00 | 不处理 |
DisposalBackground | 0x01 | 恢复背景色 |
DisposalPrevious | 0x02 | 恢复上一帧 |
GIF 结构体字段详解
| 字段 | 类型 | 单位/范围 | 说明 |
|---|---|---|---|
Image | []*image.Paletted | - | 所有帧的图像数据 |
Delay | []int | 1/100 秒 | 每帧延迟时间 |
Disposal | []byte | 0-2 | 每帧处理方式 |
BackgroundIndex | byte | 0-255 | 背景色索引 |
LoopCount | int | 0=无限 | 循环次数 |
Config | image.Config | - | 图像配置 |
Options 配置建议
| 场景 | NumColors | Quantizer | Drawer |
|---|---|---|---|
| 简单图标 | 16-32 | nil | nil |
| 图形/图表 | 32-64 | nil | nil |
| 照片 | 128-256 | nil | FloydSteinberg |
| 高质量照片 | 256 | nil | FloydSteinberg |
八、注意事项
1. 颜色限制
// GIF 最多支持 256 色
opts := &gif.Options{
NumColors: 256, // ✓ 最大 256
}
opts := &gif.Options{
NumColors: 512, // ✗ 错误:超过 256
}
2. 延迟时间单位
// Delay 的单位是 1/100 秒(厘秒)
g.Delay = []int{10} // 0.1 秒 = 100ms
g.Delay = []int{50} // 0.5 秒 = 500ms
g.Delay = []int{100} // 1.0 秒 = 1000ms
// 转换公式:毫秒 = Delay * 10
3. 帧尺寸一致性
// 所有帧应该有相同的尺寸
frame1 := image.NewPaletted(image.Rect(0, 0, 100, 100), palette)
frame2 := image.NewPaletted(image.Rect(0, 0, 100, 100), palette) // ✓ 相同
frame3 := image.NewPaletted(image.Rect(0, 0, 50, 50), palette) // ✗ 不同,可能导致问题
4. LoopCount 语义
g.LoopCount = 0 // ✓ 无限循环(GIF89a 规范)
g.LoopCount = 1 // ✓ 播放 1 次(总共播放 1 次)
g.LoopCount = 2 // ✓ 播放 2 次(总共播放 2 次)
5. 内存使用
// 大型 GIF 可能占用大量内存
// 例如:100 帧 1920x1080 的 GIF
// 建议:
// 1. 减少帧数
// 2. 减小尺寸
// 3. 使用流式处理(如果可能)
// 4. 及时释放资源
6. 透明度处理
// GIF 支持 1 位透明度(完全透明或完全不透明)
// 不支持 alpha 通道的半透明
// 在调色板中设置透明色索引
// 使用 TransparentIndex 字段(在编码器内部处理)
九、完整示例:GIF 处理工具
package main
import (
"flag"
"fmt"
"image"
"image/color/palette"
"image/draw"
"image/gif"
"image/jpeg"
"image/png"
"os"
)
// GIFTool GIF 处理工具
type GIFTool struct {
inputPath string
outputPath string
numColors int
useDither bool
}
// NewGIFTool 创建工具实例
func NewGIFTool(inputPath, outputPath string, numColors int, useDither bool) *GIFTool {
return &GIFTool{
inputPath: inputPath,
outputPath: outputPath,
numColors: numColors,
useDither: useDither,
}
}
// ConvertJPEGToGIF 转换 JPEG 到 GIF
func (t *GIFTool) ConvertJPEGToGIF() error {
// 打开 JPEG
file, err := os.Open(t.inputPath)
if err != nil {
return err
}
defer file.Close()
img, err := jpeg.Decode(file)
if err != nil {
return err
}
// 创建选项
opts := &gif.Options{
NumColors: t.numColors,
}
if t.useDither {
opts.Drawer = draw.FloydSteinberg
}
// 编码 GIF
outFile, err := os.Create(t.outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.Encode(outFile, img, opts)
}
// ConvertPNGToGIF 转换 PNG 到 GIF
func (t *GIFTool) ConvertPNGToGIF() error {
file, err := os.Open(t.inputPath)
if err != nil {
return err
}
defer file.Close()
img, err := png.Decode(file)
if err != nil {
return err
}
opts := &gif.Options{
NumColors: t.numColors,
}
if t.useDither {
opts.Drawer = draw.FloydSteinberg
}
outFile, err := os.Create(t.outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.Encode(outFile, img, opts)
}
// Info 显示 GIF 信息
func (t *GIFTool) Info() error {
file, err := os.Open(t.inputPath)
if err != nil {
return err
}
defer file.Close()
g, err := gif.DecodeAll(file)
if err != nil {
return err
}
fmt.Printf("文件:%s\n", t.inputPath)
fmt.Printf("尺寸:%dx%d\n", g.Config.Width, g.Config.Height)
fmt.Printf("帧数:%d\n", len(g.Image))
fmt.Printf("循环:%d", g.LoopCount)
if g.LoopCount == 0 {
fmt.Println(" (无限)")
} else {
fmt.Println()
}
totalDuration := 0
for i, delay := range g.Delay {
totalDuration += delay * 10
fmt.Printf("帧 %d: %dms\n", i, delay*10)
}
fmt.Printf("总时长:%dms\n", totalDuration)
return nil
}
// CreateAnimation 从图像序列创建动画
func (t *GIFTool) CreateAnimation(imagePaths []string) error {
frames := make([]*image.Paletted, len(imagePaths))
delays := make([]int, len(imagePaths))
for i, path := range imagePaths {
file, err := os.Open(path)
if err != nil {
return err
}
img, err := png.Decode(file)
file.Close()
if err != nil {
return err
}
// 转换为调色板图像
frame := image.NewPaletted(img.Bounds(), palette.Plan9)
draw.Draw(frame, frame.Bounds(), img, img.Bounds().Min, draw.FloydSteinberg)
frames[i] = frame
delays[i] = 10 // 0.1 秒
}
outFile, err := os.Create(t.outputPath)
if err != nil {
return err
}
defer outFile.Close()
return gif.EncodeAll(outFile, &gif.GIF{
Image: frames,
Delay: delays,
LoopCount: 0,
})
}
func main() {
// 命令行参数
inputPath := flag.String("i", "", "输入文件路径")
outputPath := flag.String("o", "", "输出文件路径")
numColors := flag.Int("colors", 256, "颜色数量")
useDither := flag.Bool("dither", false, "使用抖动")
action := flag.String("action", "convert", "操作:convert, info, animate")
flag.Parse()
tool := NewGIFTool(*inputPath, *outputPath, *numColors, *useDither)
var err error
switch *action {
case "convert":
err = tool.ConvertPNGToGIF()
case "info":
err = tool.Info()
case "animate":
// 需要额外的图像路径参数
imagePaths := flag.Args()
err = tool.CreateAnimation(imagePaths)
}
if err != nil {
fmt.Fprintf(os.Stderr, "错误:%v\n", err)
os.Exit(1)
}
fmt.Println("完成!")
}
最后更新: 2026-04-04
Go 版本: 1.21+
包文档: https://pkg.go.dev/image/gif