From dec90b0b180ef21b2f900da5d515e2861808a3d2 Mon Sep 17 00:00:00 2001 From: Michael Duergner Date: Sun, 9 Sep 2018 20:58:47 +0200 Subject: [PATCH] #101 Extending Focal Point crop functionality (#102) * The focal point crop filter will now accept a fourth parameter which allows to specify a minimum width for the resulting crop. If specified the focal point will be moved if necessary to ensure the resulting imagee has the desired dimensions. fixes #101 --- README.md | 2 +- eskip/sample.eskip | 7 ++++- filters/cropbyfocalpoint.go | 51 +++++++++++++++++++++++++++----- filters/cropbyfocalpoint_test.go | 33 +++++++++++++++++++-- 4 files changed, 81 insertions(+), 12 deletions(-) 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, }}) }