diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-08-26 19:12:41 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-08-28 15:59:54 +0200 |
commit | 823f53c861bb49aecc6104e0add39fc3b0729025 (patch) | |
tree | 64a55d1c41de09b67305ad69a3600f3091d4f1fc /resources | |
parent | f9978ed16476ca6d233a89669c62c798cdf9db9d (diff) |
Add a set of image filters
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 #6255
Diffstat (limited to 'resources')
86 files changed, 761 insertions, 130 deletions
diff --git a/resources/image.go b/resources/image.go index e1a816942..7113284f7 100644 --- a/resources/image.go +++ b/resources/image.go @@ -16,18 +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/imaging" + "github.com/disintegration/gift" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/resources/images" @@ -82,16 +83,26 @@ 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) }) } @@ -99,8 +110,22 @@ func (i *imageResource) Fit(spec string) (resource.Image, error) { // 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...) }) } @@ -118,19 +143,14 @@ 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() @@ -138,17 +158,12 @@ func (i *imageResource) doWithImageConfig(action, spec string, f func(src image. 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} } - if i.Format == imaging.PNG { + if i.Format == images.PNG { // Apply the colour palette from the source if paletted, ok := src.(*image.Paletted); ok { tmp := image.NewPaletted(converted.Bounds(), paletted.Palette) @@ -222,7 +237,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 diff --git a/resources/image_test.go b/resources/image_test.go index 31169444d..330a3af4b 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -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" @@ -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() @@ -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) @@ -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" @@ -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") @@ -317,6 +332,133 @@ 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{"sunset.jpg", "gohugoio8.png", "gohugoio24.png"} + + spec, workDir := newTestResourceOsFs(c) + defer func() { + if !devMode { + os.Remove(workDir) + } + }() + + if devMode { + 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 Smart", "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.Lo |