2023-04-13 21:16:53 +00:00
|
|
|
package xgif
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"fmt"
|
|
|
|
|
"image"
|
2023-04-14 13:35:14 +00:00
|
|
|
"image/color"
|
2023-04-13 21:16:53 +00:00
|
|
|
"image/gif"
|
|
|
|
|
"image/png"
|
|
|
|
|
|
|
|
|
|
"github.com/ericpauley/go-quantize/quantize"
|
2023-04-14 13:35:14 +00:00
|
|
|
"oss.terrastruct.com/util-go/go2"
|
2023-04-13 21:16:53 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const INFINITE_LOOP = 0
|
2023-04-14 13:35:14 +00:00
|
|
|
const BG_INDEX uint8 = 255
|
|
|
|
|
|
|
|
|
|
var BG_COLOR = color.White
|
|
|
|
|
|
|
|
|
|
func AnimatePNGs(pngs [][]byte, animIntervalMs int) ([]byte, error) {
|
|
|
|
|
var width, height int
|
|
|
|
|
pngImgs := make([]image.Image, len(pngs))
|
|
|
|
|
for i, pngBytes := range pngs {
|
|
|
|
|
img, err := png.Decode(bytes.NewBuffer(pngBytes))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
pngImgs[i] = img
|
|
|
|
|
bounds := img.Bounds()
|
|
|
|
|
width = go2.Max(width, bounds.Dx())
|
|
|
|
|
height = go2.Max(height, bounds.Dy())
|
|
|
|
|
}
|
2023-04-13 21:16:53 +00:00
|
|
|
|
|
|
|
|
interval := animIntervalMs / 10 // gif animation interval is in 100ths of a second
|
|
|
|
|
anim := &gif.GIF{
|
|
|
|
|
LoopCount: INFINITE_LOOP,
|
|
|
|
|
Config: image.Config{
|
2023-04-14 13:35:14 +00:00
|
|
|
Width: width,
|
|
|
|
|
Height: height,
|
2023-04-13 21:16:53 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-14 13:35:14 +00:00
|
|
|
for _, pngImage := range pngImgs {
|
|
|
|
|
// 1. convert the PNG into a GIF compatible image (Bitmap) by quantizing it to 255 colors
|
2023-04-13 21:16:53 +00:00
|
|
|
buf := bytes.NewBuffer(nil)
|
2023-04-14 13:35:14 +00:00
|
|
|
err := gif.Encode(buf, pngImage, &gif.Options{
|
|
|
|
|
NumColors: 255, // GIFs can have up to 256 colors, so keep 1 slot for white background
|
2023-04-13 21:16:53 +00:00
|
|
|
Quantizer: quantize.MedianCutQuantizer{},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
gifImg, err := gif.Decode(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
palettedImg, ok := gifImg.(*image.Paletted)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("decoded git image could not be cast as *image.Paletted")
|
|
|
|
|
}
|
2023-04-14 13:35:14 +00:00
|
|
|
|
|
|
|
|
// 2. make GIF frames of the same size, keeping images centered and with a white background
|
|
|
|
|
bounds := pngImage.Bounds()
|
|
|
|
|
top := (height - bounds.Dy()) / 2
|
|
|
|
|
bottom := top + bounds.Dy()
|
|
|
|
|
left := (width - bounds.Dx()) / 2
|
|
|
|
|
right := left + bounds.Dx()
|
|
|
|
|
|
|
|
|
|
palettedImg.Palette[BG_INDEX] = BG_COLOR
|
|
|
|
|
frame := image.NewPaletted(image.Rect(0, 0, width, height), palettedImg.Palette)
|
|
|
|
|
for x := 0; x < width; x++ {
|
|
|
|
|
for y := 0; y < height; y++ {
|
|
|
|
|
if x <= left || y <= top || x >= right || y >= bottom {
|
|
|
|
|
frame.SetColorIndex(x, y, BG_INDEX)
|
|
|
|
|
} else {
|
|
|
|
|
frame.SetColorIndex(x, y, palettedImg.ColorIndexAt(x-left, y-top))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
anim.Image = append(anim.Image, frame)
|
2023-04-13 21:16:53 +00:00
|
|
|
anim.Delay = append(anim.Delay, interval)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
|
|
|
err := gif.EncodeAll(buf, anim)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return buf.Bytes(), nil
|
|
|
|
|
}
|