diff --git a/go.mod b/go.mod index 914cf8703..ce86dfc85 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 github.com/dsoprea/go-exif/v3 v3.0.0-20221012082141-d21ac8e2de85 github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d + github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 github.com/fsnotify/fsnotify v1.6.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/jung-kurt/gofpdf v1.16.2 diff --git a/go.sum b/go.sum index bceb44ec8..900e291b9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU= +github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= diff --git a/lib/xgif/test_input1.png b/lib/xgif/test_input1.png new file mode 100644 index 000000000..48ab8a6a1 Binary files /dev/null and b/lib/xgif/test_input1.png differ diff --git a/lib/xgif/test_input2.png b/lib/xgif/test_input2.png new file mode 100644 index 000000000..9ee883c55 Binary files /dev/null and b/lib/xgif/test_input2.png differ diff --git a/lib/xgif/test_output.gif b/lib/xgif/test_output.gif new file mode 100644 index 000000000..d9bfda058 Binary files /dev/null and b/lib/xgif/test_output.gif differ diff --git a/lib/xgif/xgif.go b/lib/xgif/xgif.go new file mode 100644 index 000000000..64823b07a --- /dev/null +++ b/lib/xgif/xgif.go @@ -0,0 +1,56 @@ +package xgif + +import ( + "bytes" + "fmt" + "image" + "image/gif" + "image/png" + + "github.com/ericpauley/go-quantize/quantize" +) + +const INFINITE_LOOP = 0 + +func AnimatePNGs(pngs [][]byte, gifWidth, gifHeight int, animIntervalMs int) ([]byte, error) { + interval := animIntervalMs / 10 // gif animation interval is in 100ths of a second + anim := &gif.GIF{ + LoopCount: INFINITE_LOOP, + Config: image.Config{ + Width: gifWidth, + Height: gifHeight, + }, + } + + for _, pngBytes := range pngs { + pngImage, err := png.Decode(bytes.NewBuffer(pngBytes)) + if err != nil { + return nil, err + } + buf := bytes.NewBuffer(nil) + err = gif.Encode(buf, pngImage, &gif.Options{ + NumColors: 256, // GIFs can have up to 256 colors + 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") + } + anim.Image = append(anim.Image, palettedImg) + 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 +} diff --git a/lib/xgif/xgif_test.go b/lib/xgif/xgif_test.go new file mode 100644 index 000000000..b0f356654 --- /dev/null +++ b/lib/xgif/xgif_test.go @@ -0,0 +1,39 @@ +package xgif + +import ( + "bytes" + _ "embed" + "image/png" + "testing" + + "github.com/stretchr/testify/assert" + "oss.terrastruct.com/util-go/go2" +) + +//go:embed test_input1.png +var test_input1 []byte + +//go:embed test_input2.png +var test_input2 []byte + +//go:embed test_output.gif +var test_output []byte + +func TestPngToGif(t *testing.T) { + board1, err := png.Decode(bytes.NewBuffer(test_input1)) + assert.NoError(t, err) + gifWidth := board1.Bounds().Dx() + gifHeight := board1.Bounds().Dy() + + board2, err := png.Decode(bytes.NewBuffer(test_input2)) + assert.NoError(t, err) + gifWidth = go2.Max(board2.Bounds().Dx(), gifWidth) + gifHeight = go2.Max(board2.Bounds().Dy(), gifHeight) + + boards := [][]byte{test_input1, test_input2} + interval := 1_000 + gifBytes, err := AnimatePNGs(boards, gifWidth, gifHeight, interval) + assert.NoError(t, err) + + assert.Equal(t, test_output, gifBytes) +}