diff --git a/README.md b/README.md index 2b4fbe0..d58357a 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Skrop provides a set of filters, which you can use within the routes: * **blur(sigma, min_ampl)** — blurs the image (for info see [here](http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/libvips-convolution.html#vips-gaussblur)) * **imageOverlay(filename, opacity, gravity, opt-top-margin, opt-right-margin, opt-bottom-margin, opt-left-margin)** — puts an image onverlay over the required image * **transformFromQueryParams()** - transforms the image based on the request query parameters (supports only crop for now) e.g: localhost:9090/images/S/big-ben.jpg?crop=120,300,500,300. -* **cropByFocalPoint(targetX, targetY, aspectRatio)** — crops the image based on a focal point on both the source as well as on the target and desired aspect ratio of the target. TargetX and TargetY are the definition of the target image focal point defined as relative values for both width and height, i.e. if the focal point of the target image should be right in the center it would be 0.5 and 0.5. This filter expects two PathParams named **focalPointX** and **focalPointY** which are absolute X and Y coordinates of the focal point in the source image. The resulting image will have the largest size possible with the provided params. +* **cropByFocalPoint(targetX, targetY, aspectRatio, minWidth)** — crops the image based on a focal point on both the source as well as on the target and desired aspect ratio of the target. TargetX and TargetY are the definition of the target image focal point defined as relative values for both width and height, i.e. if the focal point of the target image should be right in the center it would be 0.5 and 0.5. This filter expects two PathParams named **focalPointX** and **focalPointY** which are absolute X and Y coordinates of the focal point in the source image. The fourth parameter is optional; when given the filter will ensure that the resulting image has at least the specified minimum width if not it will crop the biggest possible part based on the focal point. ### About filters The eskip file defines a list of configuration. Every configuration is composed by a route and a list of filters to diff --git a/eskip/sample.eskip b/eskip/sample.eskip index dce4ede..b611eb2 100644 --- a/eskip/sample.eskip +++ b/eskip/sample.eskip @@ -51,7 +51,12 @@ cropByHeight: Path("/images/cropbyheight/:image") cropByFocalPoint: Path("/images/cropbyfocalpoint/:focalPointX/:focalPointY/:image") -> modPath("^/images/cropbyfocalpoint/\\d+/\\d+", "/images") - -> cropByFocalPoint(0.25,0.25,0.5) + -> cropByFocalPoint(0.5,0.5,0.5) + -> "http://localhost:9090"; + +cropByFocalPointBestEffort: Path("/images/cropbyfocalpointminwidth/:focalPointX/:focalPointY/:image") + -> modPath("^/images/cropbyfocalpointminwidth/\\d+/\\d+", "/images") + -> cropByFocalPoint(0.5,0.5,0.5,400) -> "http://localhost:9090"; widthAndQuality: Path("/images/waq/:image") diff --git a/filters/cropbyfocalpoint.go b/filters/cropbyfocalpoint.go index eff0221..176917b 100644 --- a/filters/cropbyfocalpoint.go +++ b/filters/cropbyfocalpoint.go @@ -14,6 +14,7 @@ type cropByFocalPoint struct { targetX float64 targetY float64 aspectRatio float64 + minWidth int } // NewCropByFocalPoint creates a new filter of this type @@ -51,10 +52,34 @@ func (f *cropByFocalPoint) CreateOptions(imageContext *ImageFilterContext) (*bim return nil, err } - right := imageSize.Width - sourceX - bottom := imageSize.Height - sourceY + x := sourceX + y := sourceY + if f.minWidth != -1 { + minHeight := int(f.aspectRatio * float64(f.minWidth)) + + minX := int(float64(f.minWidth) * f.targetX) + maxX := imageSize.Width - int(float64(f.minWidth) * (1 - f.targetX)) + minY := int(float64(minHeight) * f.targetY) + maxY := imageSize.Height - int(float64(minHeight) * (1 - f.targetY)) + + if x < minX { + x = minX + } + if x > maxX { + x = maxX + } + if y < minY { + y = minY + } + if y > maxY { + y = maxY + } + } + + right := imageSize.Width - x + bottom := imageSize.Height - y - cropLeftWidth := int(float64(sourceX) / f.targetX) + cropLeftWidth := int(float64(x) / f.targetX) cropRightWidth := int(float64(right) / (float64(1) - f.targetX)) width := cropRightWidth @@ -63,13 +88,13 @@ func (f *cropByFocalPoint) CreateOptions(imageContext *ImageFilterContext) (*bim width = cropLeftWidth } - cropTopHeight := int(float64(sourceY) / f.targetY) + cropTopHeight := int(float64(y) / f.targetY) cropBottomHeight := int(float64(bottom) / (float64(1) - f.targetY)) height := cropBottomHeight if cropTopHeight < cropBottomHeight { - height = int(float64(sourceY) / f.targetY) + height = int(float64(y) / f.targetY) } ratio := float64(height) / float64(width) @@ -83,8 +108,8 @@ func (f *cropByFocalPoint) CreateOptions(imageContext *ImageFilterContext) (*bim return &bimg.Options{ AreaWidth: width, AreaHeight: height, - Top: sourceY - int(float64(height) * f.targetY), - Left: sourceX - int(float64(width) * f.targetX)}, nil + Top: y - int(float64(height) * f.targetY), + Left: x - int(float64(width) * f.targetX)}, nil } func (f *cropByFocalPoint) CanBeMerged(other *bimg.Options, self *bimg.Options) bool { @@ -98,7 +123,7 @@ func (f *cropByFocalPoint) Merge(other *bimg.Options, self *bimg.Options) *bimg. func (f *cropByFocalPoint) CreateFilter(args []interface{}) (filters.Filter, error) { var err error - if len(args) < 3 || len(args) > 3 { + if len(args) < 3 || len(args) > 4 { return nil, filters.ErrInvalidFilterParameters } @@ -122,6 +147,16 @@ func (f *cropByFocalPoint) CreateFilter(args []interface{}) (filters.Filter, err return nil, err } + if len(args) == 4 { + c.minWidth, err = parse.EskipIntArg(args[3]) + + if err != nil { + return nil, err + } + } else { + c.minWidth = -1 + } + return c, nil } diff --git a/filters/cropbyfocalpoint_test.go b/filters/cropbyfocalpoint_test.go index 2715ac1..a568e6d 100644 --- a/filters/cropbyfocalpoint_test.go +++ b/filters/cropbyfocalpoint_test.go @@ -47,6 +47,31 @@ func TestCropByFocalPoint_CreateOptions(t *testing.T) { assert.Equal(t, 352, options.Left) } +func TestCropByFocalPoint_CreateOptions_MinWidth(t *testing.T) { + c := cropByFocalPoint{targetX: 0.5, targetY: 0.5, aspectRatio: 0.5} + image := imagefiltertest.LandscapeImage() + fc := createDefaultContext(t, "doesnotmatter.com") + fc.FParams = make(map[string]string) + fc.FParams["focalPointX"] = "125"; + fc.FParams["focalPointY"] = "334"; + + options, _ := c.CreateOptions(buildParameters(fc, image)) + + assert.Equal(t, 250, options.AreaWidth) + assert.Equal(t, 125, options.AreaHeight) + assert.Equal(t, 272, options.Top) + assert.Equal(t, 0, options.Left) + + c = cropByFocalPoint{targetX: 0.5, targetY: 0.5, aspectRatio: 0.5, minWidth: 500.0} + + options, _ = c.CreateOptions(buildParameters(fc, image)) + + assert.Equal(t, 500, options.AreaWidth) + assert.Equal(t, 250, options.AreaHeight) + assert.Equal(t, 209, options.Top) + assert.Equal(t, 0, options.Left) +} + func TestCropByFocalPoint_CreateOptions_MissingPathParam(t *testing.T) { c := cropByFocalPoint{targetX: 0.5, targetY: 0.5, aspectRatio: 1.5} image := imagefiltertest.LandscapeImage() @@ -118,8 +143,12 @@ func TestCropByFocalPoint_CreateFilter(t *testing.T) { Args: []interface{}{0.5, 0.5, 1.5}, Err: false, }, { - Msg: "more than 3 args", - Args: []interface{}{0.5, 0.5, 1.5, 1.0}, + Msg: "4 args", + Args: []interface{}{0.5, 0.5, 1.5, 200.0}, + Err: false, + }, { + Msg: "more than 4 args", + Args: []interface{}{0.5, 0.5, 1.5, 200.0, 1.0}, Err: true, }}) }