mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	* 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>
		
			
				
	
	
		
			193 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			193 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
 | 
						|
}
 |