From df2557835b2235b48d1ed979abb1a1d42607e96a Mon Sep 17 00:00:00 2001
From: Rob Watson <rfwatson@users.noreply.github.com>
Date: Sat, 25 May 2019 13:46:14 +0200
Subject: [PATCH] Improve handling of non-square avatars (#7025)

* Crop avatar before resizing (#1268)

Signed-off-by: Rob Watson <rfwatson@users.noreply.github.com>

* Fix spelling error

Signed-off-by: Rob Watson <rfwatson@users.noreply.github.com>
---
 go.mod                                      |   1 +
 go.sum                                      |   2 +
 models/user.go                              |  22 +--
 modules/avatar/avatar.go                    |  50 +++++
 modules/avatar/avatar_test.go               |  49 +++++
 modules/avatar/testdata/avatar.jpeg         | Bin 0 -> 521 bytes
 modules/avatar/testdata/avatar.png          | Bin 0 -> 159 bytes
 vendor/github.com/oliamb/cutter/.gitignore  |  22 +++
 vendor/github.com/oliamb/cutter/.travis.yml |   6 +
 vendor/github.com/oliamb/cutter/LICENSE     |  20 ++
 vendor/github.com/oliamb/cutter/README.md   | 107 +++++++++++
 vendor/github.com/oliamb/cutter/cutter.go   | 192 ++++++++++++++++++++
 vendor/modules.txt                          |   2 +
 13 files changed, 454 insertions(+), 19 deletions(-)
 create mode 100644 modules/avatar/testdata/avatar.jpeg
 create mode 100644 modules/avatar/testdata/avatar.png
 create mode 100644 vendor/github.com/oliamb/cutter/.gitignore
 create mode 100644 vendor/github.com/oliamb/cutter/.travis.yml
 create mode 100644 vendor/github.com/oliamb/cutter/LICENSE
 create mode 100644 vendor/github.com/oliamb/cutter/README.md
 create mode 100644 vendor/github.com/oliamb/cutter/cutter.go

diff --git a/go.mod b/go.mod
index d02765fb10f4..299a4b29f949 100644
--- a/go.mod
+++ b/go.mod
@@ -90,6 +90,7 @@ require (
 	github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect
 	github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc
 	github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
+	github.com/oliamb/cutter v0.2.2
 	github.com/philhofer/fwd v1.0.0 // indirect
 	github.com/pkg/errors v0.8.1 // indirect
 	github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
diff --git a/go.sum b/go.sum
index 6b0a59d5b513..94d332cbc955 100644
--- a/go.sum
+++ b/go.sum
@@ -244,6 +244,8 @@ github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc h1:z1PgdCCmYYVL0BoJT
 github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs=
 github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
 github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
+github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
diff --git a/models/user.go b/models/user.go
index 7c7e81830ec3..f57c5a615d11 100644
--- a/models/user.go
+++ b/models/user.go
@@ -6,7 +6,6 @@
 package models
 
 import (
-	"bytes"
 	"container/list"
 	"crypto/md5"
 	"crypto/sha256"
@@ -14,7 +13,6 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
-	"image"
 
 	// Needed for jpeg support
 	_ "image/jpeg"
@@ -39,7 +37,6 @@ import (
 	"github.com/go-xorm/builder"
 	"github.com/go-xorm/core"
 	"github.com/go-xorm/xorm"
-	"github.com/nfnt/resize"
 	"golang.org/x/crypto/pbkdf2"
 	"golang.org/x/crypto/ssh"
 )
@@ -457,23 +454,10 @@ func (u *User) IsPasswordSet() bool {
 // UploadAvatar saves custom avatar for user.
 // FIXME: split uploads to different subdirs in case we have massive users.
 func (u *User) UploadAvatar(data []byte) error {
-	imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data))
+	m, err := avatar.Prepare(data)
 	if err != nil {
-		return fmt.Errorf("DecodeConfig: %v", err)
+		return err
 	}
-	if imgCfg.Width > setting.AvatarMaxWidth {
-		return fmt.Errorf("Image width is to large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth)
-	}
-	if imgCfg.Height > setting.AvatarMaxHeight {
-		return fmt.Errorf("Image height is to large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight)
-	}
-
-	img, _, err := image.Decode(bytes.NewReader(data))
-	if err != nil {
-		return fmt.Errorf("Decode: %v", err)
-	}
-
-	m := resize.Resize(avatar.AvatarSize, avatar.AvatarSize, img, resize.NearestNeighbor)
 
 	sess := x.NewSession()
 	defer sess.Close()
@@ -497,7 +481,7 @@ func (u *User) UploadAvatar(data []byte) error {
 	}
 	defer fw.Close()
 
-	if err = png.Encode(fw, m); err != nil {
+	if err = png.Encode(fw, *m); err != nil {
 		return fmt.Errorf("Encode: %v", err)
 	}
 
diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go
index f426978b3252..cf3da6df5ed9 100644
--- a/modules/avatar/avatar.go
+++ b/modules/avatar/avatar.go
@@ -5,13 +5,20 @@
 package avatar
 
 import (
+	"bytes"
 	"fmt"
 	"image"
 	"image/color/palette"
+	// Enable PNG support:
+	_ "image/png"
 	"math/rand"
 	"time"
 
+	"code.gitea.io/gitea/modules/setting"
+
 	"github.com/issue9/identicon"
+	"github.com/nfnt/resize"
+	"github.com/oliamb/cutter"
 )
 
 // AvatarSize returns avatar's size
@@ -42,3 +49,46 @@ func RandomImageSize(size int, data []byte) (image.Image, error) {
 func RandomImage(data []byte) (image.Image, error) {
 	return RandomImageSize(AvatarSize, data)
 }
+
+// Prepare accepts a byte slice as input, validates it contains an image of an
+// acceptable format, and crops and resizes it appropriately.
+func Prepare(data []byte) (*image.Image, error) {
+	imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data))
+	if err != nil {
+		return nil, fmt.Errorf("DecodeConfig: %v", err)
+	}
+	if imgCfg.Width > setting.AvatarMaxWidth {
+		return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth)
+	}
+	if imgCfg.Height > setting.AvatarMaxHeight {
+		return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight)
+	}
+
+	img, _, err := image.Decode(bytes.NewReader(data))
+	if err != nil {
+		return nil, fmt.Errorf("Decode: %v", err)
+	}
+
+	if imgCfg.Width != imgCfg.Height {
+		var newSize, ax, ay int
+		if imgCfg.Width > imgCfg.Height {
+			newSize = imgCfg.Height
+			ax = (imgCfg.Width - imgCfg.Height) / 2
+		} else {
+			newSize = imgCfg.Width
+			ay = (imgCfg.Height - imgCfg.Width) / 2
+		}
+
+		img, err = cutter.Crop(img, cutter.Config{
+			Width:  newSize,
+			Height: newSize,
+			Anchor: image.Point{ax, ay},
+		})
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	img = resize.Resize(AvatarSize, AvatarSize, img, resize.NearestNeighbor)
+	return &img, nil
+}
diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go
index 9eff5bc2be94..662d50faddb9 100644
--- a/modules/avatar/avatar_test.go
+++ b/modules/avatar/avatar_test.go
@@ -5,8 +5,11 @@
 package avatar
 
 import (
+	"io/ioutil"
 	"testing"
 
+	"code.gitea.io/gitea/modules/setting"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -17,3 +20,49 @@ func Test_RandomImage(t *testing.T) {
 	_, err = RandomImageSize(0, []byte("gogs@local"))
 	assert.Error(t, err)
 }
+
+func Test_PrepareWithPNG(t *testing.T) {
+	setting.AvatarMaxWidth = 4096
+	setting.AvatarMaxHeight = 4096
+
+	data, err := ioutil.ReadFile("testdata/avatar.png")
+	assert.NoError(t, err)
+
+	imgPtr, err := Prepare(data)
+	assert.NoError(t, err)
+
+	assert.Equal(t, 290, (*imgPtr).Bounds().Max.X)
+	assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y)
+}
+
+func Test_PrepareWithJPEG(t *testing.T) {
+	setting.AvatarMaxWidth = 4096
+	setting.AvatarMaxHeight = 4096
+
+	data, err := ioutil.ReadFile("testdata/avatar.jpeg")
+	assert.NoError(t, err)
+
+	imgPtr, err := Prepare(data)
+	assert.NoError(t, err)
+
+	assert.Equal(t, 290, (*imgPtr).Bounds().Max.X)
+	assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y)
+}
+
+func Test_PrepareWithInvalidImage(t *testing.T) {
+	setting.AvatarMaxWidth = 5
+	setting.AvatarMaxHeight = 5
+
+	_, err := Prepare([]byte{})
+	assert.EqualError(t, err, "DecodeConfig: image: unknown format")
+}
+func Test_PrepareWithInvalidImageSize(t *testing.T) {
+	setting.AvatarMaxWidth = 5
+	setting.AvatarMaxHeight = 5
+
+	data, err := ioutil.ReadFile("testdata/avatar.png")
+	assert.NoError(t, err)
+
+	_, err = Prepare(data)
+	assert.EqualError(t, err, "Image width is too large: 10 > 5")
+}
diff --git a/modules/avatar/testdata/avatar.jpeg b/modules/avatar/testdata/avatar.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..892b7baf78e4f8e8066f26b9b0042bcfefab1c8a
GIT binary patch
literal 521
zcmb7<U2eiK5QJy#m>Am$Yia{;v?mD?Qbha0-8e>%QS}i0dEq8zBLos{0d>4jt8YBB
z<)iGuFa2{5BEm)<$~V@~N)02bWQ;YYs*J1akqs^c@4Ro?DK~9wz2_Onhm>;;llfwn
z7Soi|@Cj-0RAy|<I5oaW|5kZJv~X~mB2*+Dsk|dgRb0x-I{n(Bd?9FpV$A@(uId5f
zpXNuwteDBD<p9LL0Zr7=EkK=ts}ZB_7;zP#?rC{y4J}9VZhNN_-|KV)ZbS<3c>uWX
BK?nc<

literal 0
HcmV?d00001

diff --git a/modules/avatar/testdata/avatar.png b/modules/avatar/testdata/avatar.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0f7922961601b6c812ac62b382d34574c14a4d4
GIT binary patch
literal 159
zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V6Od#Ih<F90I14-?iy0WWg+Z8+Vb&Z8pdfpR
zr>`sfV^(nyK@)?F|CfP6vY8<fCC>S|xv6<2KrRD=b5UwyNotBhd1gt5g1e`0K#E=}
wJ5XHI)5S4F;&Sqz|Nrfo^9~$o5bp`eWLWx`S@8b_%bg&dp00i_>zopr04tR#0RR91

literal 0
HcmV?d00001

diff --git a/vendor/github.com/oliamb/cutter/.gitignore b/vendor/github.com/oliamb/cutter/.gitignore
new file mode 100644
index 000000000000..00268614f045
--- /dev/null
+++ b/vendor/github.com/oliamb/cutter/.gitignore
@@ -0,0 +1,22 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
diff --git a/vendor/github.com/oliamb/cutter/.travis.yml b/vendor/github.com/oliamb/cutter/.travis.yml
new file mode 100644
index 000000000000..70e012b81e44
--- /dev/null
+++ b/vendor/github.com/oliamb/cutter/.travis.yml
@@ -0,0 +1,6 @@
+language: go
+
+go:
+  - 1.0
+  - 1.1
+  - tip
diff --git a/vendor/github.com/oliamb/cutter/LICENSE b/vendor/github.com/oliamb/cutter/LICENSE
new file mode 100644
index 000000000000..5412782c6e9a
--- /dev/null
+++ b/vendor/github.com/oliamb/cutter/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Olivier Amblet
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/github.com/oliamb/cutter/README.md b/vendor/github.com/oliamb/cutter/README.md
new file mode 100644
index 000000000000..b54f9e3616c4
--- /dev/null
+++ b/vendor/github.com/oliamb/cutter/README.md
@@ -0,0 +1,107 @@
+Cutter
+======
+
+A Go library to crop images.
+
+[![Build Status](https://travis-ci.org/oliamb/cutter.png?branch=master)](https://travis-ci.org/oliamb/cutter)
+[![GoDoc](https://godoc.org/github.com/oliamb/cutter?status.png)](https://godoc.org/github.com/oliamb/cutter)
+
+Cutter was initially developped to be able
+to crop image resized using github.com/nfnt/resize.
+
+Usage
+-----
+
+Read the doc on https://godoc.org/github.com/oliamb/cutter
+
+Import package with
+
+```go
+import "github.com/oliamb/cutter"
+```
+
+Package cutter provides a function to crop image.
+
+By default, the original image will be cropped at the
+given size from the top left corner.
+
+```go
+croppedImg, err := cutter.Crop(img, cutter.Config{
+  Width:  250,
+  Height: 500,
+})
+```
+
+Most of the time, the cropped image will share some memory
+with the original, so it should be used read only. You must
+ask explicitely for a copy if nedded.
+
+```go
+croppedImg, err := cutter.Crop(img, cutter.Config{
+  Width:  250,
+  Height: 500,
+  Options: cutter.Copy,
+})
+```
+
+It is possible to specify the top left position:
+
+```go
+croppedImg, err := cutter.Crop(img, cutter.Config{
+  Width:  250,
+  Height: 500,
+  Anchor: image.Point{100, 100},
+  Mode:   cutter.TopLeft, // optional, default value
+})
+```
+
+The Anchor property can represents the center of the cropped image
+instead of the top left corner:
+
+```go
+croppedImg, err := cutter.Crop(img, cutter.Config{
+  Width: 250,
+  Height: 500,
+  Mode: cutter.Centered,
+})
+```
+
+The default crop use the specified dimension, but it is possible
+to use Width and Heigth as a ratio instead. In this case,
+the resulting image will be as big as possible to fit the asked ratio
+from the anchor position.
+
+```go
+croppedImg, err := cutter.Crop(baseImage, cutter.Config{
+  Width: 4,
+  Height: 3,
+  Mode: cutter.Centered,
+  Options: cutter.Ratio&cutter.Copy, // Copy is useless here
+})
+```
+
+About resize
+------------
+This lib only manage crop and won't resize image, but it works great in combination with [github.com/nfnt/resize](https://github.com/nfnt/resize)
+
+Contributing
+------------
+I'd love to see your contributions to Cutter. If you'd like to hack on it: 
+
+- fork the project,
+- hack on it,
+- ensure tests pass,
+- make a pull request
+
+If you plan to modify the API, let's disscuss it first.
+
+Licensing
+---------
+MIT License, Please see the file called LICENSE.
+
+Credits
+-------
+Test Picture: Gopher picture from Heidi Schuyt, http://www.flickr.com/photos/hschuyt/7674222278/,
+© copyright Creative Commons(http://creativecommons.org/licenses/by-nc-sa/2.0/)
+
+Thanks to Urturn(http://www.urturn.com) for the time allocated to develop the library.
diff --git a/vendor/github.com/oliamb/cutter/cutter.go b/vendor/github.com/oliamb/cutter/cutter.go
new file mode 100644
index 000000000000..29d9d2f75882
--- /dev/null
+++ b/vendor/github.com/oliamb/cutter/cutter.go
@@ -0,0 +1,192 @@
+/*
+Package cutter provides a function to crop image.
+
+By default, the original image will be cropped at the
+given size from the top left corner.
+
+		croppedImg, err := cutter.Crop(img, cutter.Config{
+		  Width:  250,
+		  Height: 500,
+		})
+
+Most of the time, the cropped image will share some memory
+with the original, so it should be used read only. You must
+ask explicitely for a copy if nedded.
+
+    croppedImg, err := cutter.Crop(img, cutter.Config{
+      Width:  250,
+      Height: 500,
+      Options: Copy,
+    })
+
+It is possible to specify the top left position:
+
+		croppedImg, err := cutter.Crop(img, cutter.Config{
+		  Width:  250,
+		  Height: 500,
+		  Anchor: image.Point{100, 100},
+		  Mode:   TopLeft, // optional, default value
+		})
+
+The Anchor property can represents the center of the cropped image
+instead of the top left corner:
+
+
+		croppedImg, err := cutter.Crop(img, cutter.Config{
+		  Width: 250,
+		  Height: 500,
+		  Mode: Centered,
+		})
+
+The default crop use the specified dimension, but it is possible
+to use Width and Heigth as a ratio instead. In this case,
+the resulting image will be as big as possible to fit the asked ratio
+from the anchor position.
+
+		croppedImg, err := cutter.Crop(baseImage, cutter.Config{
+		  Width: 4,
+		  Height: 3,
+		  Mode: Centered,
+		  Options: Ratio,
+		})
+*/
+package cutter
+
+import (
+	"image"
+	"image/draw"
+)
+
+// Config is used to defined
+// the way the crop should be realized.
+type Config struct {
+	Width, Height int
+	Anchor        image.Point // The Anchor Point in the source image
+	Mode          AnchorMode  // Which point in the resulting image the Anchor Point is referring to
+	Options       Option
+}
+
+// AnchorMode is an enumeration of the position an anchor can represent.
+type AnchorMode int
+
+const (
+	// TopLeft defines the Anchor Point
+	// as the top left of the cropped picture.
+	TopLeft AnchorMode = iota
+	// Centered defines the Anchor Point
+	// as the center of the cropped picture.
+	Centered = iota
+)
+
+// Option flags to modify the way the crop is done.
+type Option int
+
+const (
+	// Ratio flag is use when Width and Height
+	// must be used to compute a ratio rather
+	// than absolute size in pixels.
+	Ratio Option = 1 << iota
+	// Copy flag is used to enforce the function
+	// to retrieve a copy of the selected pixels.
+	// This disable the use of SubImage method
+	// to compute the result.
+	Copy = 1 << iota
+)
+
+// An interface that is
+// image.Image + SubImage method.
+type subImageSupported interface {
+	SubImage(r image.Rectangle) image.Image
+}
+
+// Crop retrieves an image that is a
+// cropped copy of the original img.
+//
+// The crop is made given the informations provided in config.
+func Crop(img image.Image, c Config) (image.Image, error) {
+	maxBounds := c.maxBounds(img.Bounds())
+	size := c.computeSize(maxBounds, image.Point{c.Width, c.Height})
+	cr := c.computedCropArea(img.Bounds(), size)
+	cr = img.Bounds().Intersect(cr)
+
+	if c.Options&Copy == Copy {
+		return cropWithCopy(img, cr)
+	}
+	if dImg, ok := img.(subImageSupported); ok {
+		return dImg.SubImage(cr), nil
+	}
+	return cropWithCopy(img, cr)
+}
+
+func cropWithCopy(img image.Image, cr image.Rectangle) (image.Image, error) {
+	result := image.NewRGBA(cr)
+	draw.Draw(result, cr, img, cr.Min, draw.Src)
+	return result, nil
+}
+
+func (c Config) maxBounds(bounds image.Rectangle) (r image.Rectangle) {
+	if c.Mode == Centered {
+		anchor := c.centeredMin(bounds)
+		w := min(anchor.X-bounds.Min.X, bounds.Max.X-anchor.X)
+		h := min(anchor.Y-bounds.Min.Y, bounds.Max.Y-anchor.Y)
+		r = image.Rect(anchor.X-w, anchor.Y-h, anchor.X+w, anchor.Y+h)
+	} else {
+		r = image.Rect(c.Anchor.X, c.Anchor.Y, bounds.Max.X, bounds.Max.Y)
+	}
+	return
+}
+
+// computeSize retrieve the effective size of the cropped image.
+// It is defined by Height, Width, and Ratio option.
+func (c Config) computeSize(bounds image.Rectangle, ratio image.Point) (p image.Point) {
+	if c.Options&Ratio == Ratio {
+		// Ratio option is on, so we take the biggest size available that fit the given ratio.
+		if float64(ratio.X)/float64(bounds.Dx()) > float64(ratio.Y)/float64(bounds.Dy()) {
+			p = image.Point{bounds.Dx(), (bounds.Dx() / ratio.X) * ratio.Y}
+		} else {
+			p = image.Point{(bounds.Dy() / ratio.Y) * ratio.X, bounds.Dy()}
+		}
+	} else {
+		p = image.Point{ratio.X, ratio.Y}
+	}
+	return
+}
+
+// computedCropArea retrieve the theorical crop area.
+// It is defined by Height, Width, Mode and
+func (c Config) computedCropArea(bounds image.Rectangle, size image.Point) (r image.Rectangle) {
+	min := bounds.Min
+	switch c.Mode {
+	case Centered:
+		rMin := c.centeredMin(bounds)
+		r = image.Rect(rMin.X-size.X/2, rMin.Y-size.Y/2, rMin.X-size.X/2+size.X, rMin.Y-size.Y/2+size.Y)
+	default: // TopLeft
+		rMin := image.Point{min.X + c.Anchor.X, min.Y + c.Anchor.Y}
+		r = image.Rect(rMin.X, rMin.Y, rMin.X+size.X, rMin.Y+size.Y)
+	}
+	return
+}
+
+func (c *Config) centeredMin(bounds image.Rectangle) (rMin image.Point) {
+	if c.Anchor.X == 0 && c.Anchor.Y == 0 {
+		rMin = image.Point{
+			X: bounds.Dx() / 2,
+			Y: bounds.Dy() / 2,
+		}
+	} else {
+		rMin = image.Point{
+			X: c.Anchor.X,
+			Y: c.Anchor.Y,
+		}
+	}
+	return
+}
+
+func min(a, b int) (r int) {
+	if a < b {
+		r = a
+	} else {
+		r = b
+	}
+	return
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0013ea356f6d..0085f7bbdadd 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -261,6 +261,8 @@ github.com/mschoch/smat
 github.com/msteinert/pam
 # github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
 github.com/nfnt/resize
+# github.com/oliamb/cutter v0.2.2
+github.com/oliamb/cutter
 # github.com/pelletier/go-buffruneio v0.2.0
 github.com/pelletier/go-buffruneio
 # github.com/philhofer/fwd v1.0.0