Skip to content

Commit 4533f0c

Browse files
committed
allow crop options to be floats and negative
values between 0 and 1 have the same behavior as the size option - it is treated as a percentage of the original image size. Negative values for cx and cy are calculated from the bottom and right edges of the image.
1 parent 430baac commit 4533f0c

File tree

4 files changed

+70
-49
lines changed

4 files changed

+70
-49
lines changed

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ imageproxy URLs are of the form `http://localhost/{options}/{remote_url}`.
2323

2424
### Options ###
2525

26-
Options are available for resizing, rotation, flipping, and digital signatures
27-
among a few others. Options for are specified as a comma delimited list of
28-
parameters, which can be supplied in any order. Duplicate parameters overwrite
29-
previous values.
26+
Options are available for cropping, resizing, rotation, flipping, and digital
27+
signatures among a few others. Options for are specified as a comma delimited
28+
list of parameters, which can be supplied in any order. Duplicate parameters
29+
overwrite previous values.
3030

3131
See the full list of available options at
3232
<https://godoc.org/willnorris.com/go/imageproxy#ParseOptions>.
@@ -60,6 +60,7 @@ x0.15 | 15% original height, proportional width | <a href="https://willnorris
6060
100,fv,fh | 100px square, flipped horizontal and vertical | <a href="https://willnorris.com/api/imageproxy/100,fv,fh/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/100,fv,fh/https://willnorris.com/2013/12/small-things.jpg" alt="100,fv,fh"></a>
6161
200x,q60 | 200px wide, proportional height, 60% quality | <a href="https://willnorris.com/api/imageproxy/200x,q60/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/200x,q60/https://willnorris.com/2013/12/small-things.jpg" alt="200x,q60"></a>
6262
200x,png | 200px wide, converted to PNG format | <a href="https://willnorris.com/api/imageproxy/200x,png/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/200x,png/https://willnorris.com/2013/12/small-things.jpg" alt="200x,png"></a>
63+
cx175,cw400,ch300,100x | crop to 400x300px starting at (175,0), scale to 100px wide | <a href="https://willnorris.com/api/imageproxy/cx175,cw400,ch300,100x/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/cx175,cw400,ch300,100x/https://willnorris.com/2013/12/small-things.jpg" alt="cx175,cw400,ch300,100x"></a>
6364

6465
Transformation also works on animated gifs. Here is [this source
6566
image][material-animation] resized to 200px square and rotated 270 degrees:

data.go

+27-25
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ type Options struct {
8181
Format string
8282

8383
// Crop rectangle params
84-
CropX int
85-
CropY int
86-
CropWidth int
87-
CropHeight int
84+
CropX float64
85+
CropY float64
86+
CropWidth float64
87+
CropHeight float64
8888
}
8989

9090
func (o Options) String() string {
@@ -114,16 +114,16 @@ func (o Options) String() string {
114114
opts = append(opts, o.Format)
115115
}
116116
if o.CropX != 0 {
117-
opts = append(opts, fmt.Sprintf("%s%d", string(optCropX), o.CropX))
117+
opts = append(opts, fmt.Sprintf("%s%v", string(optCropX), o.CropX))
118118
}
119119
if o.CropY != 0 {
120-
opts = append(opts, fmt.Sprintf("%s%d", string(optCropY), o.CropY))
120+
opts = append(opts, fmt.Sprintf("%s%v", string(optCropY), o.CropY))
121121
}
122122
if o.CropWidth != 0 {
123-
opts = append(opts, fmt.Sprintf("%s%d", string(optCropWidth), o.CropWidth))
123+
opts = append(opts, fmt.Sprintf("%s%v", string(optCropWidth), o.CropWidth))
124124
}
125125
if o.CropHeight != 0 {
126-
opts = append(opts, fmt.Sprintf("%s%d", string(optCropHeight), o.CropHeight))
126+
opts = append(opts, fmt.Sprintf("%s%v", string(optCropHeight), o.CropHeight))
127127
}
128128
return strings.Join(opts, ",")
129129
}
@@ -133,7 +133,7 @@ func (o Options) String() string {
133133
// the presence of other fields (like Fit). A non-empty Format value is
134134
// assumed to involve a transformation.
135135
func (o Options) transform() bool {
136-
return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || (o.CropWidth != 0 && o.CropHeight != 0)
136+
return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || o.CropX != 0 || o.CropY != 0 || o.CropWidth != 0 || o.CropHeight != 0
137137
}
138138

139139
// ParseOptions parses str as a list of comma separated transformation options.
@@ -144,17 +144,19 @@ func (o Options) transform() bool {
144144
//
145145
// There are four options controlling rectangle crop:
146146
//
147-
// cx{x} - X coordinate of top left rectangle corner
148-
// cy{y} - Y coordinate of top left rectangle corner
149-
// cw{width} - rectangle width
150-
// ch{height} - rectangle height
147+
// cx{x} - X coordinate of top left rectangle corner (default: 0)
148+
// cy{y} - Y coordinate of top left rectangle corner (default: 0)
149+
// cw{width} - rectangle width (default: image width)
150+
// ch{height} - rectangle height (default: image height)
151151
//
152-
// ch and cw are required to enable crop and they must be positive integers. If
153-
// the rectangle is larger than the image, crop will not be applied. If the
154-
// rectangle does not fit the image in any of the dimensions, it will be moved
155-
// to produce an image of given size. Crop is applied before any other
156-
// transformations. If the rectangle is smaller than the requested resize and
157-
// scaleUp is disabled, the image will be of the same size as the rectangle.
152+
// For all options, integer values are interpreted as exact pixel values and
153+
// floats between 0 and 1 are interpreted as percentages of the original image
154+
// size. Negative values for cx and cy are measured from the right and bottom
155+
// edges of the image, respectively.
156+
//
157+
// If the crop width or height exceed the width or height of the image, the
158+
// crop width or height will be adjusted, preserving the specified cx and cy
159+
// values. Rectangular crop is applied before any other transformations.
158160
//
159161
// Size and Cropping
160162
//
@@ -224,8 +226,8 @@ func (o Options) transform() bool {
224226
// 100,fv,fh - 100 pixels square, flipped horizontal and vertical
225227
// 200x,q60 - 200 pixels wide, proportional height, 60% quality
226228
// 200x,png - 200 pixels wide, converted to PNG format
227-
// cw100,ch200 - crop fragment that starts at (0,0), is 100px wide and 200px tall
228-
// cw100,ch200,cx10,cy20 - crop fragment that start at (10,20) is 100px wide and 200px tall
229+
// cw100,ch100 - crop image to 100px square, starting at (0,0)
230+
// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall
229231
func ParseOptions(str string) Options {
230232
var options Options
231233

@@ -253,16 +255,16 @@ func ParseOptions(str string) Options {
253255
options.Signature = strings.TrimPrefix(opt, optSignaturePrefix)
254256
case strings.HasPrefix(opt, optCropX):
255257
value := strings.TrimPrefix(opt, optCropX)
256-
options.CropX, _ = strconv.Atoi(value)
258+
options.CropX, _ = strconv.ParseFloat(value, 64)
257259
case strings.HasPrefix(opt, optCropY):
258260
value := strings.TrimPrefix(opt, optCropY)
259-
options.CropY, _ = strconv.Atoi(value)
261+
options.CropY, _ = strconv.ParseFloat(value, 64)
260262
case strings.HasPrefix(opt, optCropWidth):
261263
value := strings.TrimPrefix(opt, optCropWidth)
262-
options.CropWidth, _ = strconv.Atoi(value)
264+
options.CropWidth, _ = strconv.ParseFloat(value, 64)
263265
case strings.HasPrefix(opt, optCropHeight):
264266
value := strings.TrimPrefix(opt, optCropHeight)
265-
options.CropHeight, _ = strconv.Atoi(value)
267+
options.CropHeight, _ = strconv.ParseFloat(value, 64)
266268
case strings.Contains(opt, optSizeDelimiter):
267269
size := strings.SplitN(opt, optSizeDelimiter, 2)
268270
if w := size[0]; w != "" {

transform.go

+31-15
Original file line numberDiff line numberDiff line change
@@ -130,31 +130,47 @@ func resizeParams(m image.Image, opt Options) (w, h int, resize bool) {
130130

131131
// cropParams calculates crop rectangle parameters to keep it in image bounds
132132
func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) {
133-
// crop params not set
134-
if opt.CropHeight <= 0 || opt.CropWidth <= 0 {
133+
if opt.CropX == 0 && opt.CropY == 0 && opt.CropWidth == 0 && opt.CropHeight == 0 {
135134
return 0, 0, 0, 0, false
136135
}
137136

137+
// width and height of image
138138
imgW := m.Bounds().Max.X - m.Bounds().Min.X
139139
imgH := m.Bounds().Max.Y - m.Bounds().Min.Y
140140

141-
x0 = opt.CropX
142-
y0 = opt.CropY
141+
// top left coordinate of crop
142+
x0 = evaluateFloat(math.Abs(opt.CropX), imgW)
143+
if opt.CropX < 0 {
144+
x0 = imgW - x0
145+
}
146+
y0 = evaluateFloat(math.Abs(opt.CropY), imgH)
147+
if opt.CropY < 0 {
148+
y0 = imgH - y0
149+
}
143150

144-
// crop rectangle out of image bounds horizontally
145-
// -> moved to point (image_width - rectangle_width) or 0, whichever is larger
146-
if opt.CropX > imgW || opt.CropX+opt.CropWidth > imgW {
147-
x0 = int(math.Max(0, float64(imgW-opt.CropWidth)))
151+
// width and height of crop
152+
w := evaluateFloat(opt.CropWidth, imgW)
153+
if w == 0 {
154+
w = imgW
148155
}
149-
// crop rectangle out of image bounds vertically
150-
// -> moved to point (image_height - rectangle_height) or 0, whichever is larger
151-
if opt.CropY > imgH || opt.CropY+opt.CropHeight > imgH {
152-
y0 = int(math.Max(0, float64(imgH-opt.CropHeight)))
156+
h := evaluateFloat(opt.CropHeight, imgH)
157+
if h == 0 {
158+
h = imgH
153159
}
154160

155-
// make rectangle fit the image
156-
x1 = int(math.Min(float64(imgW), float64(opt.CropX+opt.CropWidth)))
157-
y1 = int(math.Min(float64(imgH), float64(opt.CropY+opt.CropHeight)))
161+
if x0 == 0 && y0 == 0 && w == imgW && h == imgH {
162+
return 0, 0, 0, 0, false
163+
}
164+
165+
// bottom right coordinate of crop
166+
x1 = x0 + w
167+
if x1 > imgW {
168+
x1 = imgW
169+
}
170+
y1 = y0 + h
171+
if y1 > imgH {
172+
y1 = imgH
173+
}
158174

159175
return x0, y0, x1, y1, true
160176
}

transform_test.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ func TestCropParams(t *testing.T) {
8181
x0, y0, x1, y1 int
8282
crop bool
8383
}{
84-
{Options{CropHeight: 0, CropWidth: 10}, 0, 0, 0, 0, false},
85-
{Options{CropHeight: 10, CropWidth: 0}, 0, 0, 0, 0, false},
86-
{Options{CropHeight: -1, CropWidth: -1}, 0, 0, 0, 0, false},
84+
{Options{CropWidth: 10, CropHeight: 0}, 0, 0, 10, 128, true},
85+
{Options{CropWidth: 0, CropHeight: 10}, 0, 0, 64, 10, true},
86+
{Options{CropWidth: -1, CropHeight: -1}, 0, 0, 0, 0, false},
8787
{Options{CropWidth: 50, CropHeight: 100}, 0, 0, 50, 100, true},
8888
{Options{CropWidth: 100, CropHeight: 100}, 0, 0, 64, 100, true},
89-
{Options{CropX: 50, CropY: 100, CropWidth: 50, CropHeight: 100}, 14, 28, 64, 128, true},
90-
{Options{CropX: 50, CropY: 100, CropWidth: 100, CropHeight: 150}, 0, 0, 64, 128, true},
89+
{Options{CropX: 50, CropY: 100}, 50, 100, 64, 128, true},
90+
{Options{CropX: 50, CropY: 100, CropWidth: 100, CropHeight: 150}, 50, 100, 64, 128, true},
91+
{Options{CropX: -50, CropY: -50}, 14, 78, 64, 128, true},
92+
{Options{CropY: 0.5, CropWidth: 0.5}, 0, 64, 32, 128, true},
9193
}
9294
for _, tt := range tests {
9395
x0, y0, x1, y1, crop := cropParams(src, tt.opt)

0 commit comments

Comments
 (0)