image/gif
| Language | Go (standard library) |
|---|---|
| License | BSD-3-Clause |
| Repository | go.googlesource.com/go |
| Documentation | pkg.go.dev/image/gif |
| Depends on | nothing |
| Read / write | yes / yes; animation, loop count, disposal methods, transparency, local palettes |
image/gif decodes and encodes GIF in pure Go. gif.Decode returns the first frame; gif.DecodeAll returns a *gif.GIF with every frame as an *image.Paletted plus per-frame Delay (1/100 s) and Disposal, the LoopCount and the canvas size in Config. gif.EncodeAll writes a *gif.GIF back.
Frames are returned as stored: each *image.Paletted has its own bounds, which can be a sub-rectangle of the canvas. To display frame N you draw frames 0..N onto a canvas yourself. When encoding a non-paletted image, gif.Encode uses Options.Quantizer (default: the fixed Plan 9 palette) and Options.Drawer (default: Floyd-Steinberg dithering).
Installation#
Part of the standard library:
import "image/gif"
Basic usage: decode all frames of an animated GIF#
package main
import (
"fmt"
"image/gif"
"log"
"os"
)
func main() {
f, err := os.Open("anim.gif")
if err != nil {
log.Fatal(err)
}
defer f.Close()
g, err := gif.DecodeAll(f)
if err != nil {
log.Fatal(err)
}
fmt.Printf("canvas %d x %d, %d frames, loop count %d\n",
g.Config.Width, g.Config.Height, len(g.Image), g.LoopCount)
for i, frame := range g.Image {
fmt.Printf("frame %d: bounds %v, %d colors, delay %d0 ms, disposal %d\n",
i, frame.Bounds(), len(frame.Palette), g.Delay[i], g.Disposal[i])
}
}
LoopCount is 0 for “loop forever”, -1 for “play once” (no Netscape block), and N for N extra repetitions.
How to composite GIF frames into full images#
import (
"image"
"image/draw"
"image/gif"
)
// frames returns one full-canvas RGBA image per animation frame.
func frames(g *gif.GIF) []*image.RGBA {
bounds := image.Rect(0, 0, g.Config.Width, g.Config.Height)
canvas := image.NewRGBA(bounds)
var out []*image.RGBA
for i, frame := range g.Image {
var saved *image.RGBA
if g.Disposal[i] == gif.DisposalPrevious {
saved = image.NewRGBA(bounds)
draw.Draw(saved, bounds, canvas, image.Point{}, draw.Src)
}
draw.Draw(canvas, frame.Bounds(), frame, frame.Bounds().Min, draw.Over)
snapshot := image.NewRGBA(bounds)
draw.Draw(snapshot, bounds, canvas, image.Point{}, draw.Src)
out = append(out, snapshot)
switch g.Disposal[i] {
case gif.DisposalBackground:
draw.Draw(canvas, frame.Bounds(), image.Transparent, image.Point{}, draw.Src)
case gif.DisposalPrevious:
canvas = saved
}
}
return out
}
How to create an animated GIF#
package main
import (
"image"
"image/color"
"image/gif"
"log"
"os"
)
func main() {
palette := color.Palette{color.White, color.Black, color.RGBA{255, 0, 0, 255}}
anim := &gif.GIF{LoopCount: 0} // loop forever
for i := 0; i < 20; i++ {
img := image.NewPaletted(image.Rect(0, 0, 100, 100), palette)
for x := 0; x < 100; x++ {
img.SetColorIndex(x, i*5, 2) // a red line moving down
}
anim.Image = append(anim.Image, img)
anim.Delay = append(anim.Delay, 5) // 50 ms
anim.Disposal = append(anim.Disposal, gif.DisposalNone)
}
f, err := os.Create("anim.gif")
if err != nil {
log.Fatal(err)
}
defer f.Close()
if err := gif.EncodeAll(f, anim); err != nil {
log.Fatal(err)
}
}
How to convert a PNG or JPEG to GIF#
gif.Encode quantizes and dithers non-paletted images. Pass a quantizer for an adaptive palette:
import (
"image"
"image/draw"
"image/gif"
_ "image/jpeg"
_ "image/png"
"os"
)
func toGIF(src image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return gif.Encode(f, src, &gif.Options{
NumColors: 256,
Drawer: draw.FloydSteinberg, // or draw.Src for no dithering
// Quantizer: quantize.MedianCutQuantizer{}, // github.com/ericpauley/go-quantize
})
}
Tips#
- Check
gif.DecodeConfigbeforeDecodeAllon untrusted input; a small file can hold thousands of frames. image.Palettedframes share nothing: each needs its own palette, whichEncodeAllwrites as a local color table when it differs from the global one (GIF.Config.ColorModel).- Delays below 2 (20 ms) are shown as 100 ms by browsers.