Add animated GIF support

Note that this is for GIFs only (and not Webp).

Fixes #5030
This commit is contained in:
Bjørn Erik Pedersen 2022-06-11 18:52:55 +02:00
parent 2e1c81770a
commit cf12fa6161
6 changed files with 104 additions and 26 deletions

View file

@ -19,6 +19,7 @@ import (
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
"image/gif"
_ "image/gif" _ "image/gif"
_ "image/png" _ "image/png"
"io" "io"
@ -346,6 +347,15 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
return conf, nil return conf, nil
} }
type giphy struct {
image.Image
gif *gif.GIF
}
func (g *giphy) GIF() *gif.GIF {
return g.gif
}
// DecodeImage decodes the image source into an Image. // DecodeImage decodes the image source into an Image.
// This an internal method and may change. // This an internal method and may change.
func (i *imageResource) DecodeImage() (image.Image, error) { func (i *imageResource) DecodeImage() (image.Image, error) {
@ -354,6 +364,14 @@ func (i *imageResource) DecodeImage() (image.Image, error) {
return nil, fmt.Errorf("failed to open image for decode: %w", err) return nil, fmt.Errorf("failed to open image for decode: %w", err)
} }
defer f.Close() defer f.Close()
if i.Format == images.GIF {
g, err := gif.DecodeAll(f)
if err != nil {
return nil, fmt.Errorf("failed to decode gif: %w", err)
}
return &giphy{gif: g, Image: g.Image[0]}, nil
}
img, _, err := image.Decode(f) img, _, err := image.Decode(f)
return img, err return img, err
} }

View file

@ -16,6 +16,7 @@ package resources
import ( import (
"fmt" "fmt"
"image" "image"
"image/gif"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"math/rand" "math/rand"
@ -24,6 +25,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -641,7 +643,7 @@ func TestImageOperationsGolden(t *testing.T) {
resized, err := orig.Resize(resizeSpec) resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Log("resize", rel)
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
} }
@ -652,7 +654,15 @@ func TestImageOperationsGolden(t *testing.T) {
resized, err := orig.Resize(resizeSpec) resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Log("resize", rel) c.Assert(rel, qt.Not(qt.Equals), "")
}
// Animated GIF
orig = fetchImageForSpec(spec, c, "giphy.gif")
for _, resizeSpec := range []string{"200x", "512x"} {
resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
@ -663,7 +673,6 @@ func TestImageOperationsGolden(t *testing.T) {
resized, err := orig.Resize(resizeSpec) resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Log("resize", rel)
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
@ -671,7 +680,6 @@ func TestImageOperationsGolden(t *testing.T) {
resized, err := orig.Fill(fillSpec) resized, err := orig.Fill(fillSpec)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Log("fill", rel)
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
@ -679,7 +687,6 @@ func TestImageOperationsGolden(t *testing.T) {
resized, err := orig.Fit(fitSpec) resized, err := orig.Fit(fitSpec)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Log("fit", rel)
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
@ -713,14 +720,12 @@ func TestImageOperationsGolden(t *testing.T) {
resized, err := resized.Filter(filter) resized, err := resized.Filter(filter)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Logf("filter: %v %s", filter, rel)
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
resized, err = resized.Filter(filters[0:4]) resized, err = resized.Filter(filters[0:4])
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Log("filter all", rel)
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
@ -753,11 +758,31 @@ func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
f2, err := os.Open(filepath.Join(dir2, fi2.Name())) f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
img1, _, err := image.Decode(f1) decodeAll := func(f *os.File) []image.Image {
c.Assert(err, qt.IsNil) var images []image.Image
img2, _, err := image.Decode(f2)
c.Assert(err, qt.IsNil)
if strings.HasSuffix(f.Name(), ".gif") {
gif, err := gif.DecodeAll(f)
c.Assert(err, qt.IsNil)
images = make([]image.Image, len(gif.Image))
for i, img := range gif.Image {
images[i] = img
}
} else {
img, _, err := image.Decode(f)
c.Assert(err, qt.IsNil)
images = append(images, img)
}
return images
}
imgs1 := decodeAll(f1)
imgs2 := decodeAll(f2)
c.Assert(len(imgs1), qt.Equals, len(imgs2))
LOOP:
for i, img1 := range imgs1 {
img2 := imgs2[i]
nrgba1 := image.NewNRGBA(img1.Bounds()) nrgba1 := image.NewNRGBA(img1.Bounds())
gift.New().Draw(nrgba1, img1) gift.New().Draw(nrgba1, img1)
nrgba2 := image.NewNRGBA(img2.Bounds()) nrgba2 := image.NewNRGBA(img2.Bounds())
@ -767,10 +792,13 @@ func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
switch fi1.Name() { switch fi1.Name() {
case "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png", case "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png",
"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png", "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png",
"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png": "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png",
"giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif":
c.Log("expectedly differs from golden due to dithering:", fi1.Name()) c.Log("expectedly differs from golden due to dithering:", fi1.Name())
default: default:
c.Errorf("resulting image differs from golden: %s", fi1.Name()) c.Errorf("resulting image differs from golden: %s", fi1.Name())
break LOOP
}
} }
} }

View file

@ -86,6 +86,10 @@ func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
return encoder.Encode(w, img) return encoder.Encode(w, img)
case GIF: case GIF:
if giphy, ok := img.(Giphy); ok {
g := giphy.GIF()
return gif.EncodeAll(w, g)
}
return gif.Encode(w, img, &gif.Options{ return gif.Encode(w, img, &gif.Options{
NumColors: 256, NumColors: 256,
}) })
@ -252,8 +256,29 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi
} }
func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) { func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
g := gift.New(filters...)
bounds := g.Bounds(src.Bounds()) filter := gift.New(filters...)
if giph, ok := src.(Giphy); ok && len(giph.GIF().Image) > 1 {
g := giph.GIF()
var bounds image.Rectangle
firstFrame := g.Image[0]
tmp := image.NewNRGBA(firstFrame.Bounds())
for i := range g.Image {
gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator)
bounds = filter.Bounds(tmp.Bounds())
dst := image.NewPaletted(bounds, g.Image[i].Palette)
filter.Draw(dst, tmp)
g.Image[i] = dst
}
g.Config.Width = bounds.Dx()
g.Config.Height = bounds.Dy()
return giph, nil
}
bounds := filter.Bounds(src.Bounds())
var dst draw.Image var dst draw.Image
switch src.(type) { switch src.(type) {
case *image.RGBA: case *image.RGBA:
@ -265,7 +290,8 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.
default: default:
dst = image.NewNRGBA(bounds) dst = image.NewNRGBA(bounds)
} }
g.Draw(dst, src) filter.Draw(dst, src)
return dst, nil return dst, nil
} }
@ -376,3 +402,9 @@ type ImageSource interface {
DecodeImage() (image.Image, error) DecodeImage() (image.Image, error)
Key() string Key() string
} }
// Giphy represents a GIF Image that may be animated.
type Giphy interface {
image.Image // The first frame.
GIF() *gif.GIF // All frames.
}

BIN
resources/testdata/giphy.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB