diff --git a/resources/image.go b/resources/image.go index 3eda709d9..8551cc2ab 100644 --- a/resources/image.go +++ b/resources/image.go @@ -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 } diff --git a/resources/image_test.go b/resources/image_test.go index 8f7c95e15..153a4e8c4 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -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 + } } } diff --git a/resources/images/image.go b/resources/images/image.go index b12f03b4e..4ffbaa229 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -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. +} diff --git a/resources/testdata/giphy.gif b/resources/testdata/giphy.gif new file mode 100644 index 000000000..f82b32cbe Binary files /dev/null and b/resources/testdata/giphy.gif differ diff --git a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif new file mode 100644 index 000000000..ca826432c Binary files /dev/null and b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif differ diff --git a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif new file mode 100644 index 000000000..590d2a780 Binary files /dev/null and b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif differ