diff --git a/README.md b/README.md index b3c46fc..856f5a9 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ imageproxy URLs are of the form `http://localhost/{options}/{remote_url}`. ### Options ### -Options are available for resizing, rotation, flipping, and digital signatures -among a few others. Options for are specified as a comma delimited list of -parameters, which can be supplied in any order. Duplicate parameters overwrite -previous values. +Options are available for cropping, resizing, rotation, flipping, and digital +signatures among a few others. Options for are specified as a comma delimited +list of parameters, which can be supplied in any order. Duplicate parameters +overwrite previous values. See the full list of available options at . @@ -60,6 +60,7 @@ x0.15 | 15% original height, proportional width | 100,fv,fh 200x,q60 | 200px wide, proportional height, 60% quality | 200x,q60 200x,png | 200px wide, converted to PNG format | 200x,png +cx175,cw400,ch300,100x | crop to 400x300px starting at (175,0), scale to 100px wide | cx175,cw400,ch300,100x Transformation also works on animated gifs. Here is [this source image][material-animation] resized to 200px square and rotated 270 degrees: diff --git a/data.go b/data.go index 1749e9c..652072e 100644 --- a/data.go +++ b/data.go @@ -81,10 +81,10 @@ type Options struct { Format string // Crop rectangle params - CropX int - CropY int - CropWidth int - CropHeight int + CropX float64 + CropY float64 + CropWidth float64 + CropHeight float64 } func (o Options) String() string { @@ -114,16 +114,16 @@ func (o Options) String() string { opts = append(opts, o.Format) } 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 { - 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 { - 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 { - 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, ",") } @@ -133,7 +133,7 @@ func (o Options) String() string { // the presence of other fields (like Fit). A non-empty Format value is // assumed to involve a transformation. 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. @@ -144,17 +144,19 @@ func (o Options) transform() bool { // // There are four options controlling rectangle crop: // -// cx{x} - X coordinate of top left rectangle corner -// cy{y} - Y coordinate of top left rectangle corner -// cw{width} - rectangle width -// ch{height} - rectangle height +// cx{x} - X coordinate of top left rectangle corner (default: 0) +// cy{y} - Y coordinate of top left rectangle corner (default: 0) +// cw{width} - rectangle width (default: image width) +// ch{height} - rectangle height (default: image height) // -// ch and cw are required to enable crop and they must be positive integers. If -// the rectangle is larger than the image, crop will not be applied. If the -// rectangle does not fit the image in any of the dimensions, it will be moved -// to produce an image of given size. Crop is applied before any other -// 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. +// For all options, integer values are interpreted as exact pixel values and +// floats between 0 and 1 are interpreted as percentages of the original image +// size. Negative values for cx and cy are measured from the right and bottom +// edges of the image, respectively. +// +// 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 // @@ -224,8 +226,8 @@ func (o Options) transform() bool { // 100,fv,fh - 100 pixels square, flipped horizontal and vertical // 200x,q60 - 200 pixels wide, proportional height, 60% quality // 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,ch200,cx10,cy20 - crop fragment that start at (10,20) is 100px wide and 200px tall +// cw100,ch100 - crop image to 100px square, starting at (0,0) +// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall func ParseOptions(str string) Options { var options Options @@ -253,16 +255,16 @@ func ParseOptions(str string) Options { options.Signature = strings.TrimPrefix(opt, optSignaturePrefix) case strings.HasPrefix(opt, optCropX): value := strings.TrimPrefix(opt, optCropX) - options.CropX, _ = strconv.Atoi(value) + options.CropX, _ = strconv.ParseFloat(value, 64) case strings.HasPrefix(opt, optCropY): value := strings.TrimPrefix(opt, optCropY) - options.CropY, _ = strconv.Atoi(value) + options.CropY, _ = strconv.ParseFloat(value, 64) case strings.HasPrefix(opt, optCropWidth): value := strings.TrimPrefix(opt, optCropWidth) - options.CropWidth, _ = strconv.Atoi(value) + options.CropWidth, _ = strconv.ParseFloat(value, 64) case strings.HasPrefix(opt, optCropHeight): value := strings.TrimPrefix(opt, optCropHeight) - options.CropHeight, _ = strconv.Atoi(value) + options.CropHeight, _ = strconv.ParseFloat(value, 64) case strings.Contains(opt, optSizeDelimiter): size := strings.SplitN(opt, optSizeDelimiter, 2) if w := size[0]; w != "" { diff --git a/transform.go b/transform.go index 7de2cf8..0b5b993 100644 --- a/transform.go +++ b/transform.go @@ -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 func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) { - // crop params not set - if opt.CropHeight <= 0 || opt.CropWidth <= 0 { + if opt.CropX == 0 && opt.CropY == 0 && opt.CropWidth == 0 && opt.CropHeight == 0 { return 0, 0, 0, 0, false } + // width and height of image imgW := m.Bounds().Max.X - m.Bounds().Min.X imgH := m.Bounds().Max.Y - m.Bounds().Min.Y - x0 = opt.CropX - y0 = opt.CropY - - // crop rectangle out of image bounds horizontally - // -> 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))) + // top left coordinate of crop + x0 = evaluateFloat(math.Abs(opt.CropX), imgW) + if opt.CropX < 0 { + x0 = imgW - x0 } - // crop rectangle out of image bounds vertically - // -> moved to point (image_height - rectangle_height) or 0, whichever is larger - if opt.CropY > imgH || opt.CropY+opt.CropHeight > imgH { - y0 = int(math.Max(0, float64(imgH-opt.CropHeight))) + y0 = evaluateFloat(math.Abs(opt.CropY), imgH) + if opt.CropY < 0 { + y0 = imgH - y0 } - // make rectangle fit the image - x1 = int(math.Min(float64(imgW), float64(opt.CropX+opt.CropWidth))) - y1 = int(math.Min(float64(imgH), float64(opt.CropY+opt.CropHeight))) + // width and height of crop + w := evaluateFloat(opt.CropWidth, imgW) + 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 } diff --git a/transform_test.go b/transform_test.go index d99fdc6..7fc0e5c 100644 --- a/transform_test.go +++ b/transform_test.go @@ -81,13 +81,15 @@ func TestCropParams(t *testing.T) { x0, y0, x1, y1 int crop bool }{ - {Options{CropHeight: 0, CropWidth: 10}, 0, 0, 0, 0, false}, - {Options{CropHeight: 10, CropWidth: 0}, 0, 0, 0, 0, false}, - {Options{CropHeight: -1, CropWidth: -1}, 0, 0, 0, 0, false}, + {Options{CropWidth: 10, CropHeight: 0}, 0, 0, 10, 128, true}, + {Options{CropWidth: 0, CropHeight: 10}, 0, 0, 64, 10, true}, + {Options{CropWidth: -1, CropHeight: -1}, 0, 0, 0, 0, false}, {Options{CropWidth: 50, CropHeight: 100}, 0, 0, 50, 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, CropWidth: 100, CropHeight: 150}, 0, 0, 64, 128, true}, + {Options{CropX: 50, CropY: 100}, 50, 100, 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 { x0, y0, x1, y1, crop := cropParams(src, tt.opt)