1
0
Fork 0
forgejo/vendor/github.com/oliamb/cutter/cutter.go
Rob Watson df2557835b 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>
2019-05-25 14:46:14 +03:00

192 lines
5.1 KiB
Go

/*
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
}