summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/oliamb/cutter/cutter.go
blob: 29d9d2f75882ac84f0ddf762dc2cbd670afacd22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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
}