Skip to content

Commit

Permalink
Add $image.Filter
Browse files Browse the repository at this point in the history
With this you can do variants of this:

```
{{ $img := resources.Get "images/misc/3-jenny.jpg" }}
{{ $img := $img.Resize "300x" }}
{{ $g1 := $img.Filter images.Grayscale }}
{{ $g2 := $img | images.Filter (images.Saturate 30) (images.GaussianBlur 3) }}
```

Fixes gohugoio#6255
  • Loading branch information
bep committed Aug 28, 2019
1 parent f9978ed commit 3a25d52
Show file tree
Hide file tree
Showing 90 changed files with 754 additions and 82 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/bep/debounce v1.2.0
github.com/bep/gitmap v1.1.0
github.com/bep/go-tocss v0.6.0
github.com/disintegration/gift v1.2.1
github.com/disintegration/imaging v1.6.0
github.com/dustin/go-humanize v1.0.0
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
Expand Down
58 changes: 37 additions & 21 deletions resources/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ package resources
import (
"fmt"
"image"
"image/color"
"image/draw"
_ "image/gif"
_ "image/png"
"os"
"strings"

"github.com/gohugoio/hugo/resources/internal"

"github.com/gohugoio/hugo/resources/resource"

_errors "github.com/pkg/errors"

"github.com/disintegration/gift"
"github.com/disintegration/imaging"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/images"
Expand Down Expand Up @@ -82,25 +84,49 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource,
// filter and returns the transformed image. If one of width or height is 0, the image aspect
// ratio is preserved.
func (i *imageResource) Resize(spec string) (resource.Image, error) {
return i.doWithImageConfig("resize", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
return i.Proc.Resize(src, conf)
conf, err := i.decodeImageConfig("resize", spec)
if err != nil {
return nil, err
}

return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
return i.Proc.ApplyFiltersFromConfig(src, conf)
})
}

// Fit scales down the image using the specified resample filter to fit the specified
// maximum width and height.
func (i *imageResource) Fit(spec string) (resource.Image, error) {
return i.doWithImageConfig("fit", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
return i.Proc.Fit(src, conf)
conf, err := i.decodeImageConfig("fit", spec)
if err != nil {
return nil, err
}

return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
return i.Proc.ApplyFiltersFromConfig(src, conf)
})
}

// Fill scales the image to the smallest possible size that will cover the specified dimensions,
// crops the resized image to the specified dimensions using the given anchor point.
// Space delimited config: 200x300 TopLeft
func (i *imageResource) Fill(spec string) (resource.Image, error) {
return i.doWithImageConfig("fill", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
return i.Proc.Fill(src, conf)
conf, err := i.decodeImageConfig("fill", spec)
if err != nil {
return nil, err
}

return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
return i.Proc.ApplyFiltersFromConfig(src, conf)
})
}

func (i *imageResource) Filter(filters ...gift.Filter) (resource.Image, error) {
conf := i.Proc.GetDefaultImageConfig("filter")
conf.Key = internal.HashString(filters)

return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
return i.Proc.Filter(src, filters...)
})
}

Expand All @@ -118,32 +144,22 @@ const imageProcWorkers = 1

var imageProcSem = make(chan bool, imageProcWorkers)

func (i *imageResource) doWithImageConfig(action, spec string, f func(src image.Image, conf images.ImageConfig) (image.Image, error)) (resource.Image, error) {
conf, err := i.decodeImageConfig(action, spec)
if err != nil {
return nil, err
}

func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (resource.Image, error) {
return i.getSpec().imageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) {
imageProcSem <- true
defer func() {
<-imageProcSem
}()

errOp := action
errOp := conf.Action
errPath := i.getSourceFilename()

src, err := i.decodeSource()
if err != nil {
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
}

if conf.Rotate != 0 {
// Rotate it before any scaling to get the dimensions correct.
src = imaging.Rotate(src, float64(conf.Rotate), color.Transparent)
}

converted, err := f(src, conf)
converted, err := f(src)
if err != nil {
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
}
Expand Down Expand Up @@ -222,7 +238,7 @@ func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile
// Do not change for no good reason.
const md5Threshold = 100

key := conf.Key(i.Format)
key := conf.GetKey(i.Format)

// It is useful to have the key in clear text, but when nesting transforms, it
// can easily be too long to read, and maybe even too long
Expand Down
148 changes: 144 additions & 4 deletions resources/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ package resources
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"regexp"
"strconv"
"sync"
"testing"

"github.com/disintegration/gift"

"github.com/gohugoio/hugo/helpers"

"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/resource"

"github.com/google/go-cmp/cmp"

"github.com/gohugoio/hugo/htesting/hqt"
Expand All @@ -35,6 +41,9 @@ var eq = qt.CmpEquals(
cmp.Comparer(func(p1, p2 *resourceAdapter) bool {
return p1.resourceAdapterInner == p2.resourceAdapterInner
}),
cmp.Comparer(func(p1, p2 os.FileInfo) bool {
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
}),
cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }),
cmp.Comparer(func(m1, m2 media.Type) bool {
return m1.Type() == m2.Type()
Expand Down Expand Up @@ -94,7 +103,7 @@ func TestImageTransformBasic(t *testing.T) {
fittedAgain, err = fittedAgain.Fit("10x20")
c.Assert(err, qt.IsNil)
c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f65ba24dc2b7fba0f56d7f104519157.jpg")
assertWidthHeight(fittedAgain, 10, 6)
assertWidthHeight(fittedAgain, 10, 7)

filled, err := image.Fill("200x100 bottomLeft")
c.Assert(err, qt.IsNil)
Expand Down Expand Up @@ -155,7 +164,10 @@ func TestImagePermalinkPublishOrder(t *testing.T) {

t.Run(name, func(t *testing.T) {
c := qt.New(t)
spec := newTestResourceOsFs(c)
spec, workDir := newTestResourceOsFs(c)
defer func() {
os.Remove(workDir)
}()

check1 := func(img resource.Image) {
resizedLink := "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x50_resize_q75_box.jpg"
Expand Down Expand Up @@ -192,7 +204,10 @@ func TestImageTransformConcurrent(t *testing.T) {

c := qt.New(t)

spec := newTestResourceOsFs(c)
spec, workDir := newTestResourceOsFs(c)
defer func() {
os.Remove(workDir)
}()

image := fetchImageForSpec(spec, c, "sunset.jpg")

Expand Down Expand Up @@ -317,6 +332,131 @@ func TestSVGImageContent(t *testing.T) {
c.Assert(content.(string), qt.Contains, `<svg height="100" width="100">`)
}

func TestImageOperationsGolden(t *testing.T) {
c := qt.New(t)
c.Parallel()

devMode := false

testImages := []string{"sunrise.jpg", "gohugoio8.png", "gohugoio24.png"}

spec, workDir := newTestResourceOsFs(c)
defer func() {
if !devMode {
os.Remove(workDir)
}
}()

fmt.Println(workDir)

for _, img := range testImages {

orig := fetchImageForSpec(spec, c, img)
for _, resizeSpec := range []string{"200x100", "600x", "200x r90 q50 Box"} {
resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Log("resize", rel)
c.Assert(rel, qt.Not(qt.Equals), "")
}

for _, fillSpec := range []string{"300x200 Gaussian", "100x100 Center", "300x100 TopLeft NearestNeighbor", "400x200 BottomLeft"} {
resized, err := orig.Fill(fillSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Log("fill", rel)
c.Assert(rel, qt.Not(qt.Equals), "")
}

for _, fitSpec := range []string{"300x200 Linear"} {
resized, err := orig.Fit(fitSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Log("fit", rel)
c.Assert(rel, qt.Not(qt.Equals), "")
}

f := &images.Filters{}

filters := []gift.Filter{
f.Grayscale(),
f.GaussianBlur(6),
f.Saturation(50),
f.Sepia(100),
f.Brightness(30),
f.ColorBalance(10, -10, -10),
f.Colorize(240, 50, 100),
f.Gamma(1.5),
f.UnsharpMask(1, 1, 0),
f.Sigmoid(0.5, 7),
f.Pixelate(5),
f.Invert(),
f.Hue(22),
f.Contrast(32.5),
}

resized, err := orig.Fill("400x200 center")

for _, filter := range filters {
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), "")
}

if devMode {
return
}

dir1 := filepath.Join(workDir, "resources/_gen/images/a")
dir2 := filepath.FromSlash("testdata/golden")

// The two dirs above should now be the same.
d1, err := os.Open(dir1)
c.Assert(err, qt.IsNil)
d2, err := os.Open(dir2)
c.Assert(err, qt.IsNil)

dirinfos1, err := d1.Readdir(-1)
c.Assert(err, qt.IsNil)
dirinfos2, err := d2.Readdir(-1)

c.Assert(err, qt.IsNil)
c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))

for i, fi1 := range dirinfos1 {
if regexp.MustCompile("gauss").MatchString(fi1.Name()) {
continue
}
fi2 := dirinfos2[i]
c.Assert(fi1.Name(), qt.Equals, fi2.Name())
c.Assert(fi1, eq, fi2)
f1, err := os.Open(filepath.Join(dir1, fi1.Name()))
c.Assert(err, qt.IsNil)
f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
c.Assert(err, qt.IsNil)

hash1, err := helpers.MD5FromReader(f1)
c.Assert(err, qt.IsNil)
hash2, err := helpers.MD5FromReader(f2)
c.Assert(err, qt.IsNil)

f1.Close()
f2.Close()

c.Assert(hash1, qt.Equals, hash2)
}

}

func BenchmarkResizeParallel(b *testing.B) {
c := qt.New(b)
img := fetchSunset(c)
Expand Down
Loading

0 comments on commit 3a25d52

Please sign in to comment.