mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Add animated GIF support
Note that this is for GIFs only (and not Webp). Fixes #5030
This commit is contained in:
parent
2e1c81770a
commit
cf12fa6161
6 changed files with 104 additions and 26 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
BIN
resources/testdata/giphy.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
BIN
resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif
vendored
Normal file
BIN
resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif
vendored
Normal file
BIN
resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 304 KiB |
Loading…
Reference in a new issue