Skip to content

Commit df25578

Browse files
rfwatsonlafriks
authored andcommitted
Improve handling of non-square avatars (#7025)
* Crop avatar before resizing (#1268) Signed-off-by: Rob Watson <[email protected]> * Fix spelling error Signed-off-by: Rob Watson <[email protected]>
1 parent 5f05aa1 commit df25578

File tree

13 files changed

+454
-19
lines changed

13 files changed

+454
-19
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ require (
9090
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect
9191
github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc
9292
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
93+
github.com/oliamb/cutter v0.2.2
9394
github.com/philhofer/fwd v1.0.0 // indirect
9495
github.com/pkg/errors v0.8.1 // indirect
9596
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc h1:z1PgdCCmYYVL0BoJT
244244
github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs=
245245
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
246246
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
247+
github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
248+
github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
247249
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
248250
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
249251
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

models/user.go

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66
package models
77

88
import (
9-
"bytes"
109
"container/list"
1110
"crypto/md5"
1211
"crypto/sha256"
1312
"crypto/subtle"
1413
"encoding/hex"
1514
"errors"
1615
"fmt"
17-
"image"
1816

1917
// Needed for jpeg support
2018
_ "image/jpeg"
@@ -39,7 +37,6 @@ import (
3937
"github.com/go-xorm/builder"
4038
"github.com/go-xorm/core"
4139
"github.com/go-xorm/xorm"
42-
"github.com/nfnt/resize"
4340
"golang.org/x/crypto/pbkdf2"
4441
"golang.org/x/crypto/ssh"
4542
)
@@ -457,24 +454,11 @@ func (u *User) IsPasswordSet() bool {
457454
// UploadAvatar saves custom avatar for user.
458455
// FIXME: split uploads to different subdirs in case we have massive users.
459456
func (u *User) UploadAvatar(data []byte) error {
460-
imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data))
457+
m, err := avatar.Prepare(data)
461458
if err != nil {
462-
return fmt.Errorf("DecodeConfig: %v", err)
463-
}
464-
if imgCfg.Width > setting.AvatarMaxWidth {
465-
return fmt.Errorf("Image width is to large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth)
466-
}
467-
if imgCfg.Height > setting.AvatarMaxHeight {
468-
return fmt.Errorf("Image height is to large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight)
469-
}
470-
471-
img, _, err := image.Decode(bytes.NewReader(data))
472-
if err != nil {
473-
return fmt.Errorf("Decode: %v", err)
459+
return err
474460
}
475461

476-
m := resize.Resize(avatar.AvatarSize, avatar.AvatarSize, img, resize.NearestNeighbor)
477-
478462
sess := x.NewSession()
479463
defer sess.Close()
480464
if err = sess.Begin(); err != nil {
@@ -497,7 +481,7 @@ func (u *User) UploadAvatar(data []byte) error {
497481
}
498482
defer fw.Close()
499483

500-
if err = png.Encode(fw, m); err != nil {
484+
if err = png.Encode(fw, *m); err != nil {
501485
return fmt.Errorf("Encode: %v", err)
502486
}
503487

modules/avatar/avatar.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@
55
package avatar
66

77
import (
8+
"bytes"
89
"fmt"
910
"image"
1011
"image/color/palette"
12+
// Enable PNG support:
13+
_ "image/png"
1114
"math/rand"
1215
"time"
1316

17+
"code.gitea.io/gitea/modules/setting"
18+
1419
"github.com/issue9/identicon"
20+
"github.com/nfnt/resize"
21+
"github.com/oliamb/cutter"
1522
)
1623

1724
// AvatarSize returns avatar's size
@@ -42,3 +49,46 @@ func RandomImageSize(size int, data []byte) (image.Image, error) {
4249
func RandomImage(data []byte) (image.Image, error) {
4350
return RandomImageSize(AvatarSize, data)
4451
}
52+
53+
// Prepare accepts a byte slice as input, validates it contains an image of an
54+
// acceptable format, and crops and resizes it appropriately.
55+
func Prepare(data []byte) (*image.Image, error) {
56+
imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data))
57+
if err != nil {
58+
return nil, fmt.Errorf("DecodeConfig: %v", err)
59+
}
60+
if imgCfg.Width > setting.AvatarMaxWidth {
61+
return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth)
62+
}
63+
if imgCfg.Height > setting.AvatarMaxHeight {
64+
return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight)
65+
}
66+
67+
img, _, err := image.Decode(bytes.NewReader(data))
68+
if err != nil {
69+
return nil, fmt.Errorf("Decode: %v", err)
70+
}
71+
72+
if imgCfg.Width != imgCfg.Height {
73+
var newSize, ax, ay int
74+
if imgCfg.Width > imgCfg.Height {
75+
newSize = imgCfg.Height
76+
ax = (imgCfg.Width - imgCfg.Height) / 2
77+
} else {
78+
newSize = imgCfg.Width
79+
ay = (imgCfg.Height - imgCfg.Width) / 2
80+
}
81+
82+
img, err = cutter.Crop(img, cutter.Config{
83+
Width: newSize,
84+
Height: newSize,
85+
Anchor: image.Point{ax, ay},
86+
})
87+
if err != nil {
88+
return nil, err
89+
}
90+
}
91+
92+
img = resize.Resize(AvatarSize, AvatarSize, img, resize.NearestNeighbor)
93+
return &img, nil
94+
}

modules/avatar/avatar_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
package avatar
66

77
import (
8+
"io/ioutil"
89
"testing"
910

11+
"code.gitea.io/gitea/modules/setting"
12+
1013
"github.com/stretchr/testify/assert"
1114
)
1215

@@ -17,3 +20,49 @@ func Test_RandomImage(t *testing.T) {
1720
_, err = RandomImageSize(0, []byte("gogs@local"))
1821
assert.Error(t, err)
1922
}
23+
24+
func Test_PrepareWithPNG(t *testing.T) {
25+
setting.AvatarMaxWidth = 4096
26+
setting.AvatarMaxHeight = 4096
27+
28+
data, err := ioutil.ReadFile("testdata/avatar.png")
29+
assert.NoError(t, err)
30+
31+
imgPtr, err := Prepare(data)
32+
assert.NoError(t, err)
33+
34+
assert.Equal(t, 290, (*imgPtr).Bounds().Max.X)
35+
assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y)
36+
}
37+
38+
func Test_PrepareWithJPEG(t *testing.T) {
39+
setting.AvatarMaxWidth = 4096
40+
setting.AvatarMaxHeight = 4096
41+
42+
data, err := ioutil.ReadFile("testdata/avatar.jpeg")
43+
assert.NoError(t, err)
44+
45+
imgPtr, err := Prepare(data)
46+
assert.NoError(t, err)
47+
48+
assert.Equal(t, 290, (*imgPtr).Bounds().Max.X)
49+
assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y)
50+
}
51+
52+
func Test_PrepareWithInvalidImage(t *testing.T) {
53+
setting.AvatarMaxWidth = 5
54+
setting.AvatarMaxHeight = 5
55+
56+
_, err := Prepare([]byte{})
57+
assert.EqualError(t, err, "DecodeConfig: image: unknown format")
58+
}
59+
func Test_PrepareWithInvalidImageSize(t *testing.T) {
60+
setting.AvatarMaxWidth = 5
61+
setting.AvatarMaxHeight = 5
62+
63+
data, err := ioutil.ReadFile("testdata/avatar.png")
64+
assert.NoError(t, err)
65+
66+
_, err = Prepare(data)
67+
assert.EqualError(t, err, "Image width is too large: 10 > 5")
68+
}

modules/avatar/testdata/avatar.jpeg

521 Bytes
Loading

modules/avatar/testdata/avatar.png

159 Bytes
Loading

vendor/github.com/oliamb/cutter/.gitignore

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/oliamb/cutter/.travis.yml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/oliamb/cutter/LICENSE

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/oliamb/cutter/README.md

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)