0
Fork 0
mirror of https://github.com/willnorris/imageproxy.git synced 2024-12-30 22:34:18 -05:00

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.
This commit is contained in:
Will Norris 2017-08-31 07:27:35 +00:00
parent 430baac0b0
commit 4533f0c68a
4 changed files with 71 additions and 50 deletions

View file

@ -23,10 +23,10 @@ imageproxy URLs are of the form `http://localhost/{options}/{remote_url}`.
### Options ### ### Options ###
Options are available for resizing, rotation, flipping, and digital signatures Options are available for cropping, resizing, rotation, flipping, and digital
among a few others. Options for are specified as a comma delimited list of signatures among a few others. Options for are specified as a comma delimited
parameters, which can be supplied in any order. Duplicate parameters overwrite list of parameters, which can be supplied in any order. Duplicate parameters
previous values. overwrite previous values.
See the full list of available options at See the full list of available options at
<https://godoc.org/willnorris.com/go/imageproxy#ParseOptions>. <https://godoc.org/willnorris.com/go/imageproxy#ParseOptions>.
@ -60,6 +60,7 @@ x0.15 | 15% original height, proportional width | <a href="https://willnorris
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> 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>
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> 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>
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> 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>
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>
Transformation also works on animated gifs. Here is [this source Transformation also works on animated gifs. Here is [this source
image][material-animation] resized to 200px square and rotated 270 degrees: image][material-animation] resized to 200px square and rotated 270 degrees:

52
data.go
View file

@ -81,10 +81,10 @@ type Options struct {
Format string Format string
// Crop rectangle params // Crop rectangle params
CropX int CropX float64
CropY int CropY float64
CropWidth int CropWidth float64
CropHeight int CropHeight float64
} }
func (o Options) String() string { func (o Options) String() string {
@ -114,16 +114,16 @@ func (o Options) String() string {
opts = append(opts, o.Format) opts = append(opts, o.Format)
} }
if o.CropX != 0 { if o.CropX != 0 {
opts = append(opts, fmt.Sprintf("%s%d", string(optCropX), o.CropX)) opts = append(opts, fmt.Sprintf("%s%v", string(optCropX), o.CropX))
} }
if o.CropY != 0 { if o.CropY != 0 {
opts = append(opts, fmt.Sprintf("%s%d", string(optCropY), o.CropY)) opts = append(opts, fmt.Sprintf("%s%v", string(optCropY), o.CropY))
} }
if o.CropWidth != 0 { if o.CropWidth != 0 {
opts = append(opts, fmt.Sprintf("%s%d", string(optCropWidth), o.CropWidth)) opts = append(opts, fmt.Sprintf("%s%v", string(optCropWidth), o.CropWidth))
} }
if o.CropHeight != 0 { if o.CropHeight != 0 {
opts = append(opts, fmt.Sprintf("%s%d", string(optCropHeight), o.CropHeight)) opts = append(opts, fmt.Sprintf("%s%v", string(optCropHeight), o.CropHeight))
} }
return strings.Join(opts, ",") return strings.Join(opts, ",")
} }
@ -133,7 +133,7 @@ func (o Options) String() string {
// the presence of other fields (like Fit). A non-empty Format value is // the presence of other fields (like Fit). A non-empty Format value is
// assumed to involve a transformation. // assumed to involve a transformation.
func (o Options) transform() bool { func (o Options) transform() bool {
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) 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
} }
// ParseOptions parses str as a list of comma separated transformation options. // ParseOptions parses str as a list of comma separated transformation options.
@ -144,17 +144,19 @@ func (o Options) transform() bool {
// //
// There are four options controlling rectangle crop: // There are four options controlling rectangle crop:
// //
// cx{x} - X coordinate of top left rectangle corner // cx{x} - X coordinate of top left rectangle corner (default: 0)
// cy{y} - Y coordinate of top left rectangle corner // cy{y} - Y coordinate of top left rectangle corner (default: 0)
// cw{width} - rectangle width // cw{width} - rectangle width (default: image width)
// ch{height} - rectangle height // ch{height} - rectangle height (default: image height)
// //
// ch and cw are required to enable crop and they must be positive integers. If // For all options, integer values are interpreted as exact pixel values and
// the rectangle is larger than the image, crop will not be applied. If the // floats between 0 and 1 are interpreted as percentages of the original image
// rectangle does not fit the image in any of the dimensions, it will be moved // size. Negative values for cx and cy are measured from the right and bottom
// to produce an image of given size. Crop is applied before any other // edges of the image, respectively.
// transformations. If the rectangle is smaller than the requested resize and //
// scaleUp is disabled, the image will be of the same size as the rectangle. // If the crop width or height exceed the width or height of the image, the
// crop width or height will be adjusted, preserving the specified cx and cy
// values. Rectangular crop is applied before any other transformations.
// //
// Size and Cropping // Size and Cropping
// //
@ -224,8 +226,8 @@ func (o Options) transform() bool {
// 100,fv,fh - 100 pixels square, flipped horizontal and vertical // 100,fv,fh - 100 pixels square, flipped horizontal and vertical
// 200x,q60 - 200 pixels wide, proportional height, 60% quality // 200x,q60 - 200 pixels wide, proportional height, 60% quality
// 200x,png - 200 pixels wide, converted to PNG format // 200x,png - 200 pixels wide, converted to PNG format
// cw100,ch200 - crop fragment that starts at (0,0), is 100px wide and 200px tall // cw100,ch100 - crop image to 100px square, starting at (0,0)
// cw100,ch200,cx10,cy20 - crop fragment that start at (10,20) is 100px wide and 200px tall // cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall
func ParseOptions(str string) Options { func ParseOptions(str string) Options {
var options Options var options Options
@ -253,16 +255,16 @@ func ParseOptions(str string) Options {
options.Signature = strings.TrimPrefix(opt, optSignaturePrefix) options.Signature = strings.TrimPrefix(opt, optSignaturePrefix)
case strings.HasPrefix(opt, optCropX): case strings.HasPrefix(opt, optCropX):
value := strings.TrimPrefix(opt, optCropX) value := strings.TrimPrefix(opt, optCropX)
options.CropX, _ = strconv.Atoi(value) options.CropX, _ = strconv.ParseFloat(value, 64)
case strings.HasPrefix(opt, optCropY): case strings.HasPrefix(opt, optCropY):
value := strings.TrimPrefix(opt, optCropY) value := strings.TrimPrefix(opt, optCropY)
options.CropY, _ = strconv.Atoi(value) options.CropY, _ = strconv.ParseFloat(value, 64)
case strings.HasPrefix(opt, optCropWidth): case strings.HasPrefix(opt, optCropWidth):
value := strings.TrimPrefix(opt, optCropWidth) value := strings.TrimPrefix(opt, optCropWidth)
options.CropWidth, _ = strconv.Atoi(value) options.CropWidth, _ = strconv.ParseFloat(value, 64)
case strings.HasPrefix(opt, optCropHeight): case strings.HasPrefix(opt, optCropHeight):
value := strings.TrimPrefix(opt, optCropHeight) value := strings.TrimPrefix(opt, optCropHeight)
options.CropHeight, _ = strconv.Atoi(value) options.CropHeight, _ = strconv.ParseFloat(value, 64)
case strings.Contains(opt, optSizeDelimiter): case strings.Contains(opt, optSizeDelimiter):
size := strings.SplitN(opt, optSizeDelimiter, 2) size := strings.SplitN(opt, optSizeDelimiter, 2)
if w := size[0]; w != "" { if w := size[0]; w != "" {

View file

@ -130,31 +130,47 @@ func resizeParams(m image.Image, opt Options) (w, h int, resize bool) {
// cropParams calculates crop rectangle parameters to keep it in image bounds // cropParams calculates crop rectangle parameters to keep it in image bounds
func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) { func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) {
// crop params not set if opt.CropX == 0 && opt.CropY == 0 && opt.CropWidth == 0 && opt.CropHeight == 0 {
if opt.CropHeight <= 0 || opt.CropWidth <= 0 {
return 0, 0, 0, 0, false return 0, 0, 0, 0, false
} }
// width and height of image
imgW := m.Bounds().Max.X - m.Bounds().Min.X imgW := m.Bounds().Max.X - m.Bounds().Min.X
imgH := m.Bounds().Max.Y - m.Bounds().Min.Y imgH := m.Bounds().Max.Y - m.Bounds().Min.Y
x0 = opt.CropX // top left coordinate of crop
y0 = opt.CropY x0 = evaluateFloat(math.Abs(opt.CropX), imgW)
if opt.CropX < 0 {
// crop rectangle out of image bounds horizontally x0 = imgW - x0
// -> moved to point (image_width - rectangle_width) or 0, whichever is larger
if opt.CropX > imgW || opt.CropX+opt.CropWidth > imgW {
x0 = int(math.Max(0, float64(imgW-opt.CropWidth)))
} }
// crop rectangle out of image bounds vertically y0 = evaluateFloat(math.Abs(opt.CropY), imgH)
// -> moved to point (image_height - rectangle_height) or 0, whichever is larger if opt.CropY < 0 {
if opt.CropY > imgH || opt.CropY+opt.CropHeight > imgH { y0 = imgH - y0
y0 = int(math.Max(0, float64(imgH-opt.CropHeight)))
} }
// make rectangle fit the image // width and height of crop
x1 = int(math.Min(float64(imgW), float64(opt.CropX+opt.CropWidth))) w := evaluateFloat(opt.CropWidth, imgW)
y1 = int(math.Min(float64(imgH), float64(opt.CropY+opt.CropHeight))) if w == 0 {
w = imgW
}
h := evaluateFloat(opt.CropHeight, imgH)
if h == 0 {
h = imgH
}
if x0 == 0 && y0 == 0 && w == imgW && h == imgH {
return 0, 0, 0, 0, false
}
// bottom right coordinate of crop
x1 = x0 + w
if x1 > imgW {
x1 = imgW
}
y1 = y0 + h
if y1 > imgH {
y1 = imgH
}
return x0, y0, x1, y1, true return x0, y0, x1, y1, true
} }

View file

@ -81,13 +81,15 @@ func TestCropParams(t *testing.T) {
x0, y0, x1, y1 int x0, y0, x1, y1 int
crop bool crop bool
}{ }{
{Options{CropHeight: 0, CropWidth: 10}, 0, 0, 0, 0, false}, {Options{CropWidth: 10, CropHeight: 0}, 0, 0, 10, 128, true},
{Options{CropHeight: 10, CropWidth: 0}, 0, 0, 0, 0, false}, {Options{CropWidth: 0, CropHeight: 10}, 0, 0, 64, 10, true},
{Options{CropHeight: -1, CropWidth: -1}, 0, 0, 0, 0, false}, {Options{CropWidth: -1, CropHeight: -1}, 0, 0, 0, 0, false},
{Options{CropWidth: 50, CropHeight: 100}, 0, 0, 50, 100, true}, {Options{CropWidth: 50, CropHeight: 100}, 0, 0, 50, 100, true},
{Options{CropWidth: 100, CropHeight: 100}, 0, 0, 64, 100, true}, {Options{CropWidth: 100, CropHeight: 100}, 0, 0, 64, 100, true},
{Options{CropX: 50, CropY: 100, CropWidth: 50, CropHeight: 100}, 14, 28, 64, 128, true}, {Options{CropX: 50, CropY: 100}, 50, 100, 64, 128, true},
{Options{CropX: 50, CropY: 100, CropWidth: 100, CropHeight: 150}, 0, 0, 64, 128, true}, {Options{CropX: 50, CropY: 100, CropWidth: 100, CropHeight: 150}, 50, 100, 64, 128, true},
{Options{CropX: -50, CropY: -50}, 14, 78, 64, 128, true},
{Options{CropY: 0.5, CropWidth: 0.5}, 0, 64, 32, 128, true},
} }
for _, tt := range tests { for _, tt := range tests {
x0, y0, x1, y1, crop := cropParams(src, tt.opt) x0, y0, x1, y1, crop := cropParams(src, tt.opt)