// main.go package main import ( "errors" "flag" "fmt" "image" "image/color" "image/draw" "image/gif" "image/png" "os" "path/filepath" ) func main() { inPath := stringFlagAliases( []string{"in", "i"}, "", "Input GIF path (required)", ) outDir := stringFlagAliases( []string{"out", "o"}, "frames", "Output directory", ) prefix := stringFlagAliases( []string{"prefix", "p"}, "frame_", "Output filename prefix", ) compose := boolFlagAliases( []string{"compose", "c"}, true, "Compose frames (honor disposal); false = raw stored frames", ) flag.Parse() if *inPath == "" { fatal(errors.New("missing -in (or -i)")) } if err := os.MkdirAll(*outDir, 0o755); err != nil { fatal(fmt.Errorf("create out dir: %w", err)) } f, err := os.Open(*inPath) if err != nil { fatal(fmt.Errorf("open input: %w", err)) } defer f.Close() g, err := gif.DecodeAll(f) if err != nil { fatal(fmt.Errorf("decode gif: %w", err)) } if len(g.Image) == 0 { fatal(errors.New("gif has no frames")) } if *compose { if err := writeCompositedFrames(g, *outDir, *prefix); err != nil { fatal(err) } } else { if err := writeRawFrames(g, *outDir, *prefix); err != nil { fatal(err) } } fmt.Printf("Wrote %d frames to %s\n", len(g.Image), *outDir) } // Registers multiple flag names that all write to the same *string variable. func stringFlagAliases(names []string, def, usage string) *string { if len(names) == 0 { panic("stringFlagAliases: no names provided") } v := new(string) *v = def for _, n := range names { flag.StringVar(v, n, def, usage) } return v } // Registers multiple flag names that all write to the same *bool variable. func boolFlagAliases(names []string, def bool, usage string) *bool { if len(names) == 0 { panic("boolFlagAliases: no names provided") } v := new(bool) *v = def for _, n := range names { flag.BoolVar(v, n, def, usage) } return v } func writeRawFrames(g *gif.GIF, outDir, prefix string) error { for i, frame := range g.Image { outPath := filepath.Join(outDir, fmt.Sprintf("%s%04d.png", prefix, i)) if err := writePNG(outPath, frame); err != nil { return fmt.Errorf("write frame %d: %w", i, err) } } return nil } // Composites frames to match what you'd visually see when playing the GIF. func writeCompositedFrames(g *gif.GIF, outDir, prefix string) error { canvasRect := image.Rect(0, 0, g.Config.Width, g.Config.Height) canvas := image.NewRGBA(canvasRect) // Fill initial background. bg := gifBackgroundColor(g) draw.Draw(canvas, canvasRect, image.NewUniform(bg), image.Point{}, draw.Src) var prevCanvas *image.RGBA for i, palFrame := range g.Image { disposal := byte(0) if len(g.Disposal) > i { disposal = g.Disposal[i] } // Save for "restore to previous" if disposal == gif.DisposalPrevious { prevCanvas = cloneRGBA(canvas) } else { prevCanvas = nil } // Draw this frame onto the canvas. draw.Draw(canvas, palFrame.Bounds(), palFrame, palFrame.Bounds().Min, draw.Over) // Output the composited image for this frame. outPath := filepath.Join(outDir, fmt.Sprintf("%s%04d.png", prefix, i)) if err := writePNG(outPath, canvas); err != nil { return fmt.Errorf("write composited frame %d: %w", i, err) } // Apply disposal *after* output. switch disposal { case gif.DisposalBackground: draw.Draw(canvas, palFrame.Bounds(), image.NewUniform(bg), image.Point{}, draw.Src) case gif.DisposalPrevious: if prevCanvas != nil { copy(canvas.Pix, prevCanvas.Pix) } default: // DisposalNone / Unspecified: keep canvas as-is } } return nil } func gifBackgroundColor(g *gif.GIF) color.Color { // BackgroundIndex refers to the GIF's global palette if present. // In practice, DecodeAll gives each frame a palette; we can use frame 0. if len(g.Image) == 0 || g.Image[0] == nil { return color.Transparent } pal := g.Image[0].Palette idx := int(g.BackgroundIndex) if idx >= 0 && idx < len(pal) { return pal[idx] } return color.Transparent } func writePNG(path string, img image.Image) error { tmp := path + ".tmp" f, err := os.Create(tmp) if err != nil { return err } if err := png.Encode(f, img); err != nil { _ = f.Close() _ = os.Remove(tmp) return err } if err := f.Close(); err != nil { _ = os.Remove(tmp) return err } return os.Rename(tmp, path) } func cloneRGBA(src *image.RGBA) *image.RGBA { dst := image.NewRGBA(src.Rect) copy(dst.Pix, src.Pix) return dst } func fatal(err error) { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) }