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/color"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
"io"
|
||||
|
@ -346,6 +347,15 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
|
|||
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.
|
||||
// This an internal method and may change.
|
||||
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)
|
||||
}
|
||||
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)
|
||||
return img, err
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ package resources
|
|||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
|
@ -24,6 +25,7 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -641,7 +643,7 @@ func TestImageOperationsGolden(t *testing.T) {
|
|||
resized, err := orig.Resize(resizeSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("resize", rel)
|
||||
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
}
|
||||
|
@ -652,7 +654,15 @@ func TestImageOperationsGolden(t *testing.T) {
|
|||
resized, err := orig.Resize(resizeSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
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), "")
|
||||
}
|
||||
|
||||
|
@ -663,7 +673,6 @@ func TestImageOperationsGolden(t *testing.T) {
|
|||
resized, err := orig.Resize(resizeSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("resize", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
|
@ -671,7 +680,6 @@ func TestImageOperationsGolden(t *testing.T) {
|
|||
resized, err := orig.Fill(fillSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("fill", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
|
@ -679,7 +687,6 @@ func TestImageOperationsGolden(t *testing.T) {
|
|||
resized, err := orig.Fit(fitSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("fit", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
|
@ -713,14 +720,12 @@ func TestImageOperationsGolden(t *testing.T) {
|
|||
resized, err := resized.Filter(filter)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Logf("filter: %v %s", filter, rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
resized, err = resized.Filter(filters[0:4])
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("filter all", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
|
@ -753,24 +758,47 @@ func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
|
|||
f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
img1, _, err := image.Decode(f1)
|
||||
c.Assert(err, qt.IsNil)
|
||||
img2, _, err := image.Decode(f2)
|
||||
c.Assert(err, qt.IsNil)
|
||||
decodeAll := func(f *os.File) []image.Image {
|
||||
var images []image.Image
|
||||
|
||||
nrgba1 := image.NewNRGBA(img1.Bounds())
|
||||
gift.New().Draw(nrgba1, img1)
|
||||
nrgba2 := image.NewNRGBA(img2.Bounds())
|
||||
gift.New().Draw(nrgba2, img2)
|
||||
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
|
||||
}
|
||||
|
||||
if !goldenEqual(nrgba1, nrgba2) {
|
||||
switch fi1.Name() {
|
||||
case "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png",
|
||||
"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png",
|
||||
"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png":
|
||||
c.Log("expectedly differs from golden due to dithering:", fi1.Name())
|
||||
default:
|
||||
c.Errorf("resulting image differs from golden: %s", fi1.Name())
|
||||
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())
|
||||
gift.New().Draw(nrgba1, img1)
|
||||
nrgba2 := image.NewNRGBA(img2.Bounds())
|
||||
gift.New().Draw(nrgba2, img2)
|
||||
|
||||
if !goldenEqual(nrgba1, nrgba2) {
|
||||
switch fi1.Name() {
|
||||
case "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png",
|
||||
"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png",
|
||||
"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png",
|
||||
"giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif":
|
||||
c.Log("expectedly differs from golden due to dithering:", fi1.Name())
|
||||
default:
|
||||
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)
|
||||
|
||||
case GIF:
|
||||
if giphy, ok := img.(Giphy); ok {
|
||||
g := giphy.GIF()
|
||||
return gif.EncodeAll(w, g)
|
||||
}
|
||||
return gif.Encode(w, img, &gif.Options{
|
||||
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) {
|
||||
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
|
||||
switch src.(type) {
|
||||
case *image.RGBA:
|
||||
|
@ -265,7 +290,8 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.
|
|||
default:
|
||||
dst = image.NewNRGBA(bounds)
|
||||
}
|
||||
g.Draw(dst, src)
|
||||
filter.Draw(dst, src)
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
|
@ -376,3 +402,9 @@ type ImageSource interface {
|
|||
DecodeImage() (image.Image, error)
|
||||
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