Skip to content

Commit

Permalink
#101 Extending Focal Point crop functionality (#102)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
duergner authored and danpersa committed Sep 9, 2018
1 parent 0c24761 commit dec90b0
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion eskip/sample.eskip
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
51 changes: 43 additions & 8 deletions filters/cropbyfocalpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type cropByFocalPoint struct {
targetX float64
targetY float64
aspectRatio float64
minWidth int
}

// NewCropByFocalPoint creates a new filter of this type
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down
33 changes: 31 additions & 2 deletions filters/cropbyfocalpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
}})
}

0 comments on commit dec90b0

Please sign in to comment.