]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add animated GIF support
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 11 Jun 2022 16:52:55 +0000 (18:52 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 12 Jun 2022 16:17:08 +0000 (18:17 +0200)
Note that this is for GIFs only (and not Webp).

Fixes #5030

resources/image.go
resources/image_test.go
resources/images/image.go
resources/testdata/giphy.gif [new file with mode: 0644]
resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif [new file with mode: 0644]
resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif [new file with mode: 0644]

index 3eda709d9b1e0d5f33b3b1b120a9c7d339b96f7d..8551cc2ab48722834c5809347f82586e95985fb9 100644 (file)
@@ -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
 }
index 8f7c95e157b6d482a606f56fa2b07701467defb5..153a4e8c4529142e9645e88d885d1005d3a1a6ac 100644 (file)
@@ -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
+
+                       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
+               }
 
-               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":
-                               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
+                               }
                        }
                }
 
index b12f03b4e33c50efc05130469c33d952c69b7f6e..4ffbaa2295aac3058b52f75a8d5ac2826e732b1f 100644 (file)
@@ -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 (file)
index 0000000..f82b32c
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 (file)
index 0000000..ca82643
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 (file)
index 0000000..590d2a7
Binary files /dev/null and b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box.gif differ