From 2ff97658544f2ad5f76645f59dc4d2e0df6139d9 Mon Sep 17 00:00:00 2001 From: Mats Linander Date: Fri, 10 Jun 2022 11:35:38 -0400 Subject: [PATCH 1/5] render: better handling of optimized GIFs --- render/image.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/render/image.go b/render/image.go index 37322b9a83..7b53fdfa22 100644 --- a/render/image.go +++ b/render/image.go @@ -71,15 +71,8 @@ func (p *Image) InitFromWebP(data []byte) error { } func (p *Image) InitFromGIF(data []byte) error { - // GIF support is quite limited and can't handle different - // positioned and sized frames, as well as disposal types. - // - // This means that many more optimized GIFs will not render - // correctly, with frame contents jumping around and previous - // frame contents always getting disposed, instead of kept. - // - // Unfortunatley the 'image/gif' package does not even expose - // frame positions, making it hard to implement these features. + // GIF support is a bit limited. Some optimized GIFs will not + // render correctly. // // Consider using WebP instead. img, err := gif.DecodeAll(bytes.NewReader(data)) @@ -88,10 +81,20 @@ func (p *Image) InitFromGIF(data []byte) error { } p.Delay = img.Delay[0] * 10 - for _, im := range img.Image { - imRGBA := image.NewRGBA(image.Rect(0, 0, im.Bounds().Dx(), im.Bounds().Dy())) - draw.Draw(imRGBA, imRGBA.Bounds(), im, image.Point{0, 0}, draw.Src) - p.imgs = append(p.imgs, imRGBA) + + last := image.NewRGBA(image.Rect(0, 0, img.Image[0].Bounds().Dx(), img.Image[0].Bounds().Dy())) + draw.Draw(last, last.Bounds(), img.Image[0], image.ZP, draw.Src) + + for _, src := range img.Image { + + // Note: We're not really handling all disposal + // methods here, but this seems to be good enough. + draw.Draw(last, last.Bounds(), src, image.Point{0, 0}, draw.Over) + frame := *last + frame.Pix = make([]uint8, len(last.Pix)) + copy(frame.Pix, last.Pix) + + p.imgs = append(p.imgs, &frame) } return nil @@ -128,11 +131,11 @@ func (p *Image) Init() error { nw, nh := p.Width, p.Height if nw == 0 { // scale width, maintaining original aspect ratio - nw = int(float64(nh)*(float64(w)/float64(h))) + nw = int(float64(nh) * (float64(w) / float64(h))) } if nh == 0 { // scale height, maintaining original aspect ratio - nh = int(float64(nw)*(float64(h)/float64(w))) + nh = int(float64(nw) * (float64(h) / float64(w))) } for i := 0; i < len(p.imgs); i++ { From 18689ffa58ebb423b7cb62a3a8fe26ab303ef599 Mon Sep 17 00:00:00 2001 From: Mats Linander Date: Fri, 10 Jun 2022 11:45:35 -0400 Subject: [PATCH 2/5] s/Point{0,0}/ZP/ --- encode/encode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encode/encode.go b/encode/encode.go index 4c53da0d36..a30e61d993 100644 --- a/encode/encode.go +++ b/encode/encode.go @@ -151,7 +151,7 @@ func (s *Screens) EncodeGIF(filters ...ImageFilter) ([]byte, error) { palette := quantize.MedianCutQuantizer{}.Quantize(make([]color.Color, 0, 256), im) imPaletted := image.NewPaletted(imRGBA.Bounds(), palette) - draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.Point{0, 0}, draw.Src) + draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.ZP, draw.Src) g.Image = append(g.Image, imPaletted) g.Delay = append(g.Delay, int(s.delay/10)) // in 100ths of a second From ea61aa94a4e939cd19652d9c27ffabff6d8d494e Mon Sep 17 00:00:00 2001 From: Mats Linander Date: Fri, 10 Jun 2022 11:46:45 -0400 Subject: [PATCH 3/5] s/Point{0,0}/ZP/ --- encode/encode.go | 2 +- render/image.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/encode/encode.go b/encode/encode.go index a30e61d993..0318d4286f 100644 --- a/encode/encode.go +++ b/encode/encode.go @@ -151,7 +151,7 @@ func (s *Screens) EncodeGIF(filters ...ImageFilter) ([]byte, error) { palette := quantize.MedianCutQuantizer{}.Quantize(make([]color.Color, 0, 256), im) imPaletted := image.NewPaletted(imRGBA.Bounds(), palette) - draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.ZP, draw.Src) + draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.Point{}, draw.Src) g.Image = append(g.Image, imPaletted) g.Delay = append(g.Delay, int(s.delay/10)) // in 100ths of a second diff --git a/render/image.go b/render/image.go index 7b53fdfa22..40b06741a8 100644 --- a/render/image.go +++ b/render/image.go @@ -89,7 +89,7 @@ func (p *Image) InitFromGIF(data []byte) error { // Note: We're not really handling all disposal // methods here, but this seems to be good enough. - draw.Draw(last, last.Bounds(), src, image.Point{0, 0}, draw.Over) + draw.Draw(last, last.Bounds(), src, image.ZP, draw.Over) frame := *last frame.Pix = make([]uint8, len(last.Pix)) copy(frame.Pix, last.Pix) From 4529df02a4da1704f7c5f1559a210b2483182ea9 Mon Sep 17 00:00:00 2001 From: Mats Linander Date: Fri, 10 Jun 2022 11:47:06 -0400 Subject: [PATCH 4/5] s/Point{0,0}/ZP/ --- encode/encode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encode/encode.go b/encode/encode.go index 0318d4286f..4c53da0d36 100644 --- a/encode/encode.go +++ b/encode/encode.go @@ -151,7 +151,7 @@ func (s *Screens) EncodeGIF(filters ...ImageFilter) ([]byte, error) { palette := quantize.MedianCutQuantizer{}.Quantize(make([]color.Color, 0, 256), im) imPaletted := image.NewPaletted(imRGBA.Bounds(), palette) - draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.Point{}, draw.Src) + draw.Draw(imPaletted, imRGBA.Bounds(), imRGBA, image.Point{0, 0}, draw.Src) g.Image = append(g.Image, imPaletted) g.Delay = append(g.Delay, int(s.delay/10)) // in 100ths of a second From 0aef614b72383c2c765ec622c158945c5e08c90f Mon Sep 17 00:00:00 2001 From: Mats Linander Date: Fri, 10 Jun 2022 12:11:59 -0400 Subject: [PATCH 5/5] update test --- render/image_test.go | 52 ++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/render/image_test.go b/render/image_test.go index 33346b4150..bc4a90a193 100644 --- a/render/image_test.go +++ b/render/image_test.go @@ -12,9 +12,6 @@ import ( // plus sign on a transparent background. const testPNG = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAMCAYAAABbayygAAAAOUlEQVQoU2P8z8Dwn4EIwAhSyMjAwIhPLVgNukKYDciaaawQl6dATkCxmmiFMF8PgGeICnAiYpABACrQO/WD80OVAAAAAElFTkSuQmCC" -// Animated GIF with a few pixels moving around -const testGIF = "R0lGODlhBQAEAPAAAAAAAAAAACH5BAF7AAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAABQAEAAACBgRiaLmLBQAh+QQBewAAACwAAAAABQAEAAACBYRzpqhXACH5BAF7AAAALAAAAAAFAAQAAAIGDG6Qp8wFACH5BAF7AAAALAAAAAAFAAQAAAIGRIBnyMoFADs=" - func TestImage(t *testing.T) { raw, _ := base64.StdEncoding.DecodeString(testPNG) img := &Image{Src: string(raw)} @@ -108,6 +105,20 @@ func TestImageScaleAspectRatioHeight(t *testing.T) { } func TestImageAnimatedGif(t *testing.T) { + // Animated 5x4 GIF with 4 frames: + // + // frame 0: ..x.. + // x.... + // .x... + // ...x. + // + // Subsequent frames shift pixels right, overflowing into the + // next row. + // + // GIF has no disposal method set, and a delay of 1230 ms + + const testGIF = "R0lGODlhBQAEAPAAAAAAAAAAACH5BAF7AAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAABQAEAAACBgRiaLmLBQAh+QQBewAAACwAAAAABQAEAAACBYRzpqhXACH5BAF7AAAALAAAAAAFAAQAAAIGDG6Qp8wFACH5BAF7AAAALAAAAAAFAAQAAAIGRIBnyMoFADs=" + raw, _ := base64.StdEncoding.DecodeString(testGIF) img := &Image{Src: string(raw)} img.Init() @@ -127,23 +138,26 @@ func TestImageAnimatedGif(t *testing.T) { ".x...", "...x.", }, img.Paint(image.Rect(0, 0, 100, 100), 0))) + + // since no disposal method is set, subsequent frames should + // draw on top of first frame assert.Equal(t, nil, checkImage([]string{ - "...x.", - ".x...", - "..x..", - "....x", + "..xx.", + "xx...", + ".xx..", + "...xx", }, img.Paint(image.Rect(0, 0, 100, 100), 1))) assert.Equal(t, nil, checkImage([]string{ - "x...x", - "..x..", - "...x.", - ".....", + "x.xxx", + "xxx..", + ".xxx.", + "...xx", }, img.Paint(image.Rect(0, 0, 100, 100), 2))) assert.Equal(t, nil, checkImage([]string{ - ".x...", - "x..x.", - "....x", - ".....", + "xxxxx", + "xxxx.", + ".xxxx", + "...xx", }, img.Paint(image.Rect(0, 0, 100, 100), 3))) // loops after the last frame @@ -154,9 +168,9 @@ func TestImageAnimatedGif(t *testing.T) { "...x.", }, img.Paint(image.Rect(0, 0, 100, 100), 4))) assert.Equal(t, nil, checkImage([]string{ - "...x.", - ".x...", - "..x..", - "....x", + "..xx.", + "xx...", + ".xx..", + "...xx", }, img.Paint(image.Rect(0, 0, 100, 100), 5))) }