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>
This commit is contained in:
parent
5f05aa13e0
commit
df2557835b
1
go.mod
1
go.mod
@ -90,6 +90,7 @@ require (
|
|||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect
|
||||||
github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc
|
github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc
|
||||||
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
|
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/philhofer/fwd v1.0.0 // indirect
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
|
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
|
||||||
|
2
go.sum
2
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/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 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
|
||||||
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
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.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 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"container/list"
|
"container/list"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@ -14,7 +13,6 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
|
|
||||||
// Needed for jpeg support
|
// Needed for jpeg support
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
@ -39,7 +37,6 @@ import (
|
|||||||
"github.com/go-xorm/builder"
|
"github.com/go-xorm/builder"
|
||||||
"github.com/go-xorm/core"
|
"github.com/go-xorm/core"
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
"github.com/nfnt/resize"
|
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@ -457,23 +454,10 @@ func (u *User) IsPasswordSet() bool {
|
|||||||
// UploadAvatar saves custom avatar for user.
|
// UploadAvatar saves custom avatar for user.
|
||||||
// FIXME: split uploads to different subdirs in case we have massive users.
|
// FIXME: split uploads to different subdirs in case we have massive users.
|
||||||
func (u *User) UploadAvatar(data []byte) error {
|
func (u *User) UploadAvatar(data []byte) error {
|
||||||
imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
m, err := avatar.Prepare(data)
|
||||||
if err != nil {
|
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()
|
sess := x.NewSession()
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
@ -497,7 +481,7 @@ func (u *User) UploadAvatar(data []byte) error {
|
|||||||
}
|
}
|
||||||
defer fw.Close()
|
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)
|
return fmt.Errorf("Encode: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,13 +5,20 @@
|
|||||||
package avatar
|
package avatar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color/palette"
|
"image/color/palette"
|
||||||
|
// Enable PNG support:
|
||||||
|
_ "image/png"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/issue9/identicon"
|
"github.com/issue9/identicon"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"github.com/oliamb/cutter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AvatarSize returns avatar's size
|
// 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) {
|
func RandomImage(data []byte) (image.Image, error) {
|
||||||
return RandomImageSize(AvatarSize, data)
|
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
|
||||||
|
}
|
||||||
|
@ -5,8 +5,11 @@
|
|||||||
package avatar
|
package avatar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,3 +20,49 @@ func Test_RandomImage(t *testing.T) {
|
|||||||
_, err = RandomImageSize(0, []byte("gogs@local"))
|
_, err = RandomImageSize(0, []byte("gogs@local"))
|
||||||
assert.Error(t, err)
|
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")
|
||||||
|
}
|
||||||
|
BIN
modules/avatar/testdata/avatar.jpeg
vendored
Normal file
BIN
modules/avatar/testdata/avatar.jpeg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 521 B |
BIN
modules/avatar/testdata/avatar.png
vendored
Normal file
BIN
modules/avatar/testdata/avatar.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 159 B |
22
vendor/github.com/oliamb/cutter/.gitignore
generated
vendored
Normal file
22
vendor/github.com/oliamb/cutter/.gitignore
generated
vendored
Normal file
@ -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
|
6
vendor/github.com/oliamb/cutter/.travis.yml
generated
vendored
Normal file
6
vendor/github.com/oliamb/cutter/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.0
|
||||||
|
- 1.1
|
||||||
|
- tip
|
20
vendor/github.com/oliamb/cutter/LICENSE
generated
vendored
Normal file
20
vendor/github.com/oliamb/cutter/LICENSE
generated
vendored
Normal file
@ -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.
|
107
vendor/github.com/oliamb/cutter/README.md
generated
vendored
Normal file
107
vendor/github.com/oliamb/cutter/README.md
generated
vendored
Normal file
@ -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.
|
192
vendor/github.com/oliamb/cutter/cutter.go
generated
vendored
Normal file
192
vendor/github.com/oliamb/cutter/cutter.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -261,6 +261,8 @@ github.com/mschoch/smat
|
|||||||
github.com/msteinert/pam
|
github.com/msteinert/pam
|
||||||
# github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
|
# github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
|
||||||
github.com/nfnt/resize
|
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 v0.2.0
|
||||||
github.com/pelletier/go-buffruneio
|
github.com/pelletier/go-buffruneio
|
||||||
# github.com/philhofer/fwd v1.0.0
|
# github.com/philhofer/fwd v1.0.0
|
||||||
|
Loading…
Reference in New Issue
Block a user