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