URI: 
       resource: Add smart cropping - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit 722086b4ed3e77d1aba6724474bec06d08e7de06
   DIR parent 084cf4191b3c1e7590a4223fd9251019ef5d4c21
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Sat,  3 Feb 2018 16:47:35 +0100
       
       resource: Add smart cropping
       
       This commit `smart` as a new and default anchor in `Fill`.
       
       So:
       
       ```html
       {{ $image.Fill "200x200" }}
       ```
       
       Is, with default configuration, the same as:
       
       ```html
       {{ $image.Fill "200x200" "smart" }}
       ```
       
       You can change this default in your `config.toml`:
       
       ```toml
       [imaging]
       [imaging]
       resampleFilter = "box"
       
       quality = 68
       
       anchor = "Smart"
       ```
       
       Fixes #4375
       
       Diffstat:
         M Gopkg.lock                          |      13 ++++++++++++-
         M Gopkg.toml                          |       8 +++++++-
         M resource/image.go                   |      53 ++++++++++++++++++++++++-------
         M resource/image_test.go              |      51 +++++++++++++++++++++++++++----
         A resource/smartcrop.go               |      80 +++++++++++++++++++++++++++++++
         M resource/testhelpers_test.go        |       9 +++++++++
       
       6 files changed, 194 insertions(+), 20 deletions(-)
       ---
   DIR diff --git a/Gopkg.lock b/Gopkg.lock
       @@ -207,6 +207,15 @@
          revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff"
        
        [[projects]]
       +  branch = "master"
       +  name = "github.com/muesli/smartcrop"
       +  packages = [
       +    ".",
       +    "options"
       +  ]
       +  revision = "1db484956b9ef929344e51701299a017beefdaaa"
       +
       +[[projects]]
          name = "github.com/nicksnyder/go-i18n"
          packages = [
            "i18n/bundle",
       @@ -320,6 +329,8 @@
          name = "golang.org/x/image"
          packages = [
            "bmp",
       +    "draw",
       +    "math/f64",
            "riff",
            "tiff",
            "tiff/lzw",
       @@ -381,6 +392,6 @@
        [solve-meta]
          analyzer-name = "dep"
          analyzer-version = 1
       -  inputs-digest = "c80ffe69d34005d8d72a87cc491ce1d9c91272e4b7f8fbd22d4fda8973fa8556"
       +  inputs-digest = "ce63da7f660e0ba60a8ae81f5808f8e685b2055169838fbc3c4d5c418e58b3d1"
          solver-name = "gps-cdcl"
          solver-version = 1
   DIR diff --git a/Gopkg.toml b/Gopkg.toml
       @@ -15,7 +15,7 @@
        [[constraint]]
          branch = "master"
          name = "github.com/bep/gitmap"
       -
       +  
        [[constraint]]
         name = "github.com/chaseadamsio/goorgeous"
         version = "^1.1.0"
       @@ -135,3 +135,9 @@
        [[constraint]]
          name = "github.com/gobwas/glob"
          version = "0.2.2"
       +
       +
       +[[constraint]]
       +  name = "github.com/muesli/smartcrop"
       +  branch = "master"
       +
   DIR diff --git a/resource/image.go b/resource/image.go
       @@ -35,7 +35,6 @@ import (
                _ "image/png"
        
                "github.com/disintegration/imaging"
       -
                // Import webp codec
                "sync"
        
       @@ -56,6 +55,9 @@ type Imaging struct {
        
                // Resample filter used. See https://github.com/disintegration/imaging
                ResampleFilter string
       +
       +        // The anchor used in Fill. Default is "smart", i.e. Smart Crop.
       +        Anchor string
        }
        
        const (
       @@ -157,6 +159,9 @@ func (i *Image) Fit(spec string) (*Image, error) {
        // Space delimited config: 200x300 TopLeft
        func (i *Image) Fill(spec string) (*Image, error) {
                return i.doWithImageConfig("fill", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
       +                if conf.AnchorStr == smartCropIdentifier {
       +                        return smartCrop(src, conf.Width, conf.Height, conf.Anchor, conf.Filter)
       +                }
                        return imaging.Fill(src, conf.Width, conf.Height, conf.Anchor, conf.Filter), nil
                })
        }
       @@ -206,6 +211,13 @@ func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, c
                        conf.Filter = imageFilters[conf.FilterStr]
                }
        
       +        if conf.AnchorStr == "" {
       +                conf.AnchorStr = i.imaging.Anchor
       +                if !strings.EqualFold(conf.AnchorStr, smartCropIdentifier) {
       +                        conf.Anchor = anchorPositions[conf.AnchorStr]
       +                }
       +        }
       +
                key := i.relTargetPathForRel(i.filenameFromConfig(conf), false)
        
                return i.spec.imageCache.getOrCreate(i, key, func(resourceCacheFilename string) (*Image, error) {
       @@ -248,18 +260,22 @@ func (i imageConfig) key() string {
                if i.Rotate != 0 {
                        k += "_r" + strconv.Itoa(i.Rotate)
                }
       -        k += "_" + i.FilterStr + "_" + i.AnchorStr
       -        return k
       -}
       +        anchor := i.AnchorStr
       +        if anchor == smartCropIdentifier {
       +                anchor = anchor + strconv.Itoa(smartCropVersionNumber)
       +        }
        
       -var defaultImageConfig = imageConfig{
       -        Action:    "",
       -        Anchor:    imaging.Center,
       -        AnchorStr: strings.ToLower("Center"),
       +        k += "_" + i.FilterStr
       +
       +        if strings.EqualFold(i.Action, "fill") {
       +                k += "_" + anchor
       +        }
       +
       +        return k
        }
        
        func newImageConfig(width, height, quality, rotate int, filter, anchor string) imageConfig {
       -        c := defaultImageConfig
       +        var c imageConfig
        
                c.Width = width
                c.Height = height
       @@ -287,7 +303,7 @@ func newImageConfig(width, height, quality, rotate int, filter, anchor string) i
        
        func parseImageConfig(config string) (imageConfig, error) {
                var (
       -                c   = defaultImageConfig
       +                c   imageConfig
                        err error
                )
        
       @@ -299,7 +315,9 @@ func parseImageConfig(config string) (imageConfig, error) {
                for _, part := range parts {
                        part = strings.ToLower(part)
        
       -                if pos, ok := anchorPositions[part]; ok {
       +                if part == smartCropIdentifier {
       +                        c.AnchorStr = smartCropIdentifier
       +                } else if pos, ok := anchorPositions[part]; ok {
                                c.Anchor = pos
                                c.AnchorStr = part
                        } else if filter, ok := imageFilters[part]; ok {
       @@ -561,8 +579,19 @@ func decodeImaging(m map[string]interface{}) (Imaging, error) {
                        return i, err
                }
        
       -        if i.Quality <= 0 || i.Quality > 100 {
       +        if i.Quality == 0 {
                        i.Quality = defaultJPEGQuality
       +        } else if i.Quality < 0 || i.Quality > 100 {
       +                return i, errors.New("JPEG quality must be a number between 1 and 100")
       +        }
       +
       +        if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
       +                i.Anchor = smartCropIdentifier
       +        } else {
       +                i.Anchor = strings.ToLower(i.Anchor)
       +                if _, found := anchorPositions[i.Anchor]; !found {
       +                        return i, errors.New("invalid anchor value in imaging config")
       +                }
                }
        
                if i.ResampleFilter == "" {
   DIR diff --git a/resource/image_test.go b/resource/image_test.go
       @@ -82,13 +82,13 @@ func TestImageTransform(t *testing.T) {
                assert.Equal(200, resizedAndRotated.Height())
                assertFileCache(assert, image.spec.Fs, resizedAndRotated.RelPermalink(), 125, 200)
        
       -        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_resize_q75_box_center.jpg", resized.RelPermalink())
       +        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_resize_q68_linear.jpg", resized.RelPermalink())
                assert.Equal(300, resized.Width())
                assert.Equal(200, resized.Height())
        
                fitted, err := resized.Fit("50x50")
                assert.NoError(err)
       -        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_0bda5208a94b50a6e643ad139e0dfa2f.jpg", fitted.RelPermalink())
       +        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_625708021e2bb281c9f1002f88e4753f.jpg", fitted.RelPermalink())
                assert.Equal(50, fitted.Width())
                assert.Equal(31, fitted.Height())
        
       @@ -96,17 +96,24 @@ func TestImageTransform(t *testing.T) {
                fittedAgain, _ := fitted.Fit("10x20")
                fittedAgain, err = fittedAgain.Fit("10x20")
                assert.NoError(err)
       -        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6b3034f4ca91823700bd9ff7a12acf2e.jpg", fittedAgain.RelPermalink())
       +        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f65ba24dc2b7fba0f56d7f104519157.jpg", fittedAgain.RelPermalink())
                assert.Equal(10, fittedAgain.Width())
                assert.Equal(6, fittedAgain.Height())
        
                filled, err := image.Fill("200x100 bottomLeft")
                assert.NoError(err)
       -        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q75_box_bottomleft.jpg", filled.RelPermalink())
       +        assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_bottomleft.jpg", filled.RelPermalink())
                assert.Equal(200, filled.Width())
                assert.Equal(100, filled.Height())
                assertFileCache(assert, image.spec.Fs, filled.RelPermalink(), 200, 100)
        
       +        smart, err := image.Fill("200x100 smart")
       +        assert.NoError(err)
       +        assert.Equal(fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_smart%d.jpg", smartCropVersionNumber), smart.RelPermalink())
       +        assert.Equal(200, smart.Width())
       +        assert.Equal(100, smart.Height())
       +        assertFileCache(assert, image.spec.Fs, smart.RelPermalink(), 200, 100)
       +
                // Check cache
                filledAgain, err := image.Fill("200x100 bottomLeft")
                assert.NoError(err)
       @@ -126,12 +133,12 @@ func TestImageTransformLongFilename(t *testing.T) {
                assert.NoError(err)
                assert.NotNil(resized)
                assert.Equal(200, resized.Width())
       -        assert.Equal("/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_fd0f8b23902abcf4092b68783834f7fe.jpg", resized.RelPermalink())
       +        assert.Equal("/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_65b757a6e14debeae720fe8831f0a9bc.jpg", resized.RelPermalink())
                resized, err = resized.Resize("100x")
                assert.NoError(err)
                assert.NotNil(resized)
                assert.Equal(100, resized.Width())
       -        assert.Equal("/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_5f399e62910070692b3034a925f1b2d7.jpg", resized.RelPermalink())
       +        assert.Equal("/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg", resized.RelPermalink())
        }
        
        func TestDecodeImaging(t *testing.T) {
       @@ -139,6 +146,7 @@ func TestDecodeImaging(t *testing.T) {
                m := map[string]interface{}{
                        "quality":        42,
                        "resampleFilter": "NearestNeighbor",
       +                "anchor":         "topLeft",
                }
        
                imaging, err := decodeImaging(m)
       @@ -146,6 +154,37 @@ func TestDecodeImaging(t *testing.T) {
                assert.NoError(err)
                assert.Equal(42, imaging.Quality)
                assert.Equal("nearestneighbor", imaging.ResampleFilter)
       +        assert.Equal("topleft", imaging.Anchor)
       +
       +        m = map[string]interface{}{}
       +
       +        imaging, err = decodeImaging(m)
       +        assert.NoError(err)
       +        assert.Equal(defaultJPEGQuality, imaging.Quality)
       +        assert.Equal("box", imaging.ResampleFilter)
       +        assert.Equal("smart", imaging.Anchor)
       +
       +        _, err = decodeImaging(map[string]interface{}{
       +                "quality": 123,
       +        })
       +        assert.Error(err)
       +
       +        _, err = decodeImaging(map[string]interface{}{
       +                "resampleFilter": "asdf",
       +        })
       +        assert.Error(err)
       +
       +        _, err = decodeImaging(map[string]interface{}{
       +                "anchor": "asdf",
       +        })
       +        assert.Error(err)
       +
       +        imaging, err = decodeImaging(map[string]interface{}{
       +                "anchor": "Smart",
       +        })
       +        assert.NoError(err)
       +        assert.Equal("smart", imaging.Anchor)
       +
        }
        
        func TestImageWithMetadata(t *testing.T) {
   DIR diff --git a/resource/smartcrop.go b/resource/smartcrop.go
       @@ -0,0 +1,80 @@
       +// Copyright 2017-present The Hugo Authors. All rights reserved.
       +//
       +// Licensed under the Apache License, Version 2.0 (the "License");
       +// you may not use this file except in compliance with the License.
       +// You may obtain a copy of the License at
       +// http://www.apache.org/licenses/LICENSE-2.0
       +//
       +// Unless required by applicable law or agreed to in writing, software
       +// distributed under the License is distributed on an "AS IS" BASIS,
       +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       +// See the License for the specific language governing permissions and
       +// limitations under the License.
       +
       +package resource
       +
       +import (
       +        "image"
       +
       +        "github.com/disintegration/imaging"
       +        "github.com/muesli/smartcrop"
       +)
       +
       +const (
       +        // Do not change.
       +        smartCropIdentifier = "smart"
       +
       +        // This is just a increment, starting on 1. If Smart Crop improves its cropping, we
       +        // need a way to trigger a re-generation of the crops in the wild, so increment this.
       +        smartCropVersionNumber = 1
       +)
       +
       +// Needed by smartcrop
       +type imagingResizer struct {
       +        filter imaging.ResampleFilter
       +}
       +
       +func (r imagingResizer) Resize(img image.Image, width, height uint) image.Image {
       +        return imaging.Resize(img, int(width), int(height), r.filter)
       +}
       +
       +func newSmartCropAnalyzer(filter imaging.ResampleFilter) smartcrop.Analyzer {
       +        return smartcrop.NewAnalyzer(imagingResizer{filter: filter})
       +}
       +
       +func smartCrop(img image.Image, width, height int, anchor imaging.Anchor, filter imaging.ResampleFilter) (*image.NRGBA, error) {
       +
       +        if width <= 0 || height <= 0 {
       +                return &image.NRGBA{}, nil
       +        }
       +
       +        srcBounds := img.Bounds()
       +        srcW := srcBounds.Dx()
       +        srcH := srcBounds.Dy()
       +
       +        if srcW <= 0 || srcH <= 0 {
       +                return &image.NRGBA{}, nil
       +        }
       +
       +        if srcW == width && srcH == height {
       +                return imaging.Clone(img), nil
       +        }
       +
       +        smart := newSmartCropAnalyzer(filter)
       +
       +        rect, err := smart.FindBestCrop(img, width, height)
       +
       +        if err != nil {
       +                return nil, err
       +        }
       +
       +        b := img.Bounds().Intersect(rect)
       +
       +        cropped, err := imaging.Crop(img, b), nil
       +        if err != nil {
       +                return nil, err
       +        }
       +
       +        return imaging.Resize(cropped, width, height, filter), nil
       +
       +}
   DIR diff --git a/resource/testhelpers_test.go b/resource/testhelpers_test.go
       @@ -25,6 +25,15 @@ func newTestResourceSpecForBaseURL(assert *require.Assertions, baseURL string) *
                cfg := viper.New()
                cfg.Set("baseURL", baseURL)
                cfg.Set("resourceDir", "/res")
       +
       +        imagingCfg := map[string]interface{}{
       +                "resampleFilter": "linear",
       +                "quality":        68,
       +                "anchor":         "left",
       +        }
       +
       +        cfg.Set("imaging", imagingCfg)
       +
                fs := hugofs.NewMem(cfg)
        
                s, err := helpers.NewPathSpec(fs, cfg)