Skip to content

Commit

Permalink
#95 First shot for implementing Focal Point Cropping (#96)
Browse files Browse the repository at this point in the history
Closes #95 Implementing Focal Point Cropping
* #95 Wrap the filterContext into the ImageFilterContext
* Adding test for FocalPoint cropping
* Address review comments
  • Loading branch information
duergner authored and danpersa committed Aug 2, 2018
1 parent e38864e commit b51b45e
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +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.

### 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
1 change: 1 addition & 0 deletions cmd/skrop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func main() {
skropFilters.NewCrop(),
skropFilters.NewCropByWidth(),
skropFilters.NewCropByHeight(),
skropFilters.NewCropByFocalPoint(),
skropFilters.NewResizeByWidth(),
skropFilters.NewResizeByHeight(),
skropFilters.NewQuality(),
Expand Down
5 changes: 5 additions & 0 deletions eskip/sample.eskip
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ cropByHeight: Path("/images/cropbyheight/:image")
-> cropByHeight(1000, "south")
-> "http://localhost:9090";

cropByFocalPoint: Path("/images/cropbyfocalpoint/:focalPointX/:focalPointY/:image")
-> modPath("^/images/cropbyfocalpoint/\\d+/\\d+", "/images")
-> cropByFocalPoint(0.25,0.25,0.5)
-> "http://localhost:9090";

widthAndQuality: Path("/images/waq/:image")
-> modPath("^/images/waq", "/images")
-> width(1000)
Expand Down
132 changes: 132 additions & 0 deletions filters/cropbyfocalpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package filters

import (
"github.com/zalando-stups/skrop/parse"
"github.com/zalando/skipper/filters"
"gopkg.in/h2non/bimg.v1"
"strconv"
)

// CropByFocalPointName is the name of the filter
const CropByFocalPointName = "cropByFocalPoint"

type cropByFocalPoint struct {
targetX float64
targetY float64
aspectRatio float64
}

// NewCropByFocalPoint creates a new filter of this type
func NewCropByFocalPoint() filters.Spec {
return &cropByFocalPoint{}
}

func (f *cropByFocalPoint) Name() string {
return CropByFocalPointName
}

func (f *cropByFocalPoint) CreateOptions(imageContext *ImageFilterContext) (*bimg.Options, error) {
imageSize, err := imageContext.Image.Size()

if err != nil {
return nil, err
}

focalPointX := imageContext.PathParam("focalPointX")
focalPointY := imageContext.PathParam("focalPointY")

if focalPointX == "" || focalPointY == "" {
return nil, filters.ErrInvalidFilterParameters
}

sourceX, err := strconv.Atoi(focalPointX)

if err != nil {
return nil, err
}

sourceY, err := strconv.Atoi(focalPointY)

if err != nil {
return nil, err
}

right := imageSize.Width - sourceX
bottom := imageSize.Height - sourceY

cropLeftWidth := int(float64(sourceX) / f.targetX)
cropRightWidth := int(float64(right) / (float64(1) - f.targetX))

width := cropRightWidth

if cropLeftWidth < cropRightWidth {
width = cropLeftWidth
}

cropTopHeight := int(float64(sourceY) / f.targetY)
cropBottomHeight := int(float64(bottom) / (float64(1) - f.targetY))

height := cropBottomHeight

if cropTopHeight < cropBottomHeight {
height = int(float64(sourceY) / f.targetY)
}

ratio := float64(height) / float64(width)

if ratio > f.aspectRatio {
height = int(float64(width) * f.aspectRatio)
} else {
width = int(float64(height) / f.aspectRatio)
}

return &bimg.Options{
AreaWidth: width,
AreaHeight: height,
Top: sourceY - int(float64(height) * f.targetY),
Left: sourceX - int(float64(width) * f.targetX)}, nil
}

func (f *cropByFocalPoint) CanBeMerged(other *bimg.Options, self *bimg.Options) bool {
return false
}

func (f *cropByFocalPoint) Merge(other *bimg.Options, self *bimg.Options) *bimg.Options {
return self
}

func (f *cropByFocalPoint) CreateFilter(args []interface{}) (filters.Filter, error) {
var err error

if len(args) < 3 || len(args) > 3 {
return nil, filters.ErrInvalidFilterParameters
}

c := &cropByFocalPoint{}

c.targetX, err = parse.EskipFloatArg(args[0])

if err != nil {
return nil, err
}

c.targetY, err = parse.EskipFloatArg(args[1])

if err != nil {
return nil, err
}

c.aspectRatio, err = parse.EskipFloatArg(args[2])

if err != nil {
return nil, err
}

return c, nil
}

func (f *cropByFocalPoint) Request(ctx filters.FilterContext) {}

func (f *cropByFocalPoint) Response(ctx filters.FilterContext) {
HandleImageResponse(ctx, f)
}
125 changes: 125 additions & 0 deletions filters/cropbyfocalpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package filters

import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zalando-stups/skrop/filters/imagefiltertest"
"github.com/zalando/skipper/filters"
)

func TestNewCropByFocalPoint(t *testing.T) {
name := NewCropByFocalPoint().Name()
assert.Equal(t, "cropByFocalPoint", name)
}

func TestCropByFocalPoint_Name(t *testing.T) {
c := cropByFocalPoint{}
assert.Equal(t, "cropByFocalPoint", c.Name())
}

func TestCropByFocalPoint_CreateOptions(t *testing.T) {
c := cropByFocalPoint{targetX: 0.5, targetY: 0.5, aspectRatio: 1.5}
image := imagefiltertest.LandscapeImage()
fc := createDefaultContext(t, "doesnotmatter.com")
fc.FParams = make(map[string]string)
fc.FParams["focalPointX"] = "500";
fc.FParams["focalPointY"] = "334";

options, _ := c.CreateOptions(buildParameters(fc, image))

assert.Equal(t, 445, options.AreaWidth)
assert.Equal(t, 668, options.AreaHeight)
assert.Equal(t, 0, options.Top)
assert.Equal(t, 278, options.Left)

c = cropByFocalPoint{targetX: 0.5, targetY: 0.25, aspectRatio: 1.5}
image = imagefiltertest.LandscapeImage()
fc = createDefaultContext(t, "doesnotmatter.com")
fc.FParams = make(map[string]string)
fc.FParams["focalPointX"] = "500";
fc.FParams["focalPointY"] = "334";

options, _ = c.CreateOptions(buildParameters(fc, image))

assert.Equal(t, 296, options.AreaWidth)
assert.Equal(t, 445, options.AreaHeight)
assert.Equal(t, 223, options.Top)
assert.Equal(t, 352, options.Left)
}

func TestCropByFocalPoint_CreateOptions_MissingPathParam(t *testing.T) {
c := cropByFocalPoint{targetX: 0.5, targetY: 0.5, aspectRatio: 1.5}
image := imagefiltertest.LandscapeImage()
fc := createDefaultContext(t, "doesnotmatter.com")
fc.FParams = make(map[string]string)
fc.FParams["focalPointY"] = "334";

options, err := c.CreateOptions(buildParameters(fc, image))

assert.Nil(t, options)
assert.Equal(t, filters.ErrInvalidFilterParameters, err)

fc = createDefaultContext(t, "doesnotmatter.com")
fc.FParams = make(map[string]string)
fc.FParams["focalPointX"] = "334";

options, err = c.CreateOptions(buildParameters(fc, image))

assert.Nil(t, options)
assert.Equal(t, filters.ErrInvalidFilterParameters, err)
}

func TestCropByFocalPoint_CreateOptions_InvalidPathParam(t *testing.T) {
c := cropByFocalPoint{targetX: 0.5, targetY: 0.5, aspectRatio: 1.5}
image := imagefiltertest.LandscapeImage()
fc := createDefaultContext(t, "doesnotmatter.com")
fc.FParams = make(map[string]string)
fc.FParams["focalPointX"] = "xyz";
fc.FParams["focalPointY"] = "abc";

options, err := c.CreateOptions(buildParameters(fc, image))

assert.Nil(t, options)
assert.NotNil(t, err)

fc.FParams["focalPointX"] = "100";
fc.FParams["focalPointY"] = "abc";

options, err = c.CreateOptions(buildParameters(fc, image))

assert.Nil(t, options)
assert.NotNil(t, err)
}

func TestCropByFocalPoint_CanBeMerged(t *testing.T) {
ea := transformFromQueryParams{}
assert.Equal(t, false, ea.CanBeMerged(nil, nil))
}

func TestCropByFocalPoint_CreateFilter(t *testing.T) {
imagefiltertest.TestCreate(t, NewCropByFocalPoint, []imagefiltertest.CreateTestItem{{
Msg: "less than 3 args",
Args: nil,
Err: true,
}, {
Msg: "invalid targetX",
Args: []interface{}{"xyz", 0.5, 1.5},
Err: true,
}, {
Msg: "invalid targetY",
Args: []interface{}{0.5, "abc", 1.5},
Err: true,
}, {
Msg: "invalid aspectRatio",
Args: []interface{}{0.5, 0.5, "qwerty"},
Err: true,
}, {
Msg: "3 args",
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},
Err: true,
}})
}
15 changes: 10 additions & 5 deletions filters/imagefilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const (
var (
cropTypeToGravity map[string]bimg.Gravity
cropTypes map[string]bool
stripMetadata bool
stripMetadata bool
)

func init() {
Expand Down Expand Up @@ -67,10 +67,13 @@ type ImageFilter interface {
}

type ImageFilterContext struct {
Image *bimg.Image
Parameters map[string][]string
Image *bimg.Image
Parameters map[string][]string
filterContext *filters.FilterContext
}

func (c *ImageFilterContext) PathParam(key string) string { return (*c.filterContext).PathParam(key) }

func errorResponse() *http.Response {
return &http.Response{
StatusCode: http.StatusInternalServerError,
Expand All @@ -83,9 +86,11 @@ func buildParameters(ctx filters.FilterContext, image *bimg.Image) *ImageFilterC
if ctx != nil {
parameters = ctx.Request().URL.Query()
}

return &ImageFilterContext{
Image: image,
Parameters: parameters,
Image: image,
Parameters: parameters,
filterContext: &ctx,
}
}

Expand Down
14 changes: 14 additions & 0 deletions filters/transformFromQueryParams.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
const (
ExtractArea = "transformFromQueryParams"
cropParameters = "crop"
focalPointCropParameters = "focal_point_crop"
)

type transformFromQueryParams struct{}
Expand All @@ -30,6 +31,19 @@ func (t *transformFromQueryParams) CreateOptions(ctx *ImageFilterContext) (*bimg
// Get crop prams from the request
params, ok := ctx.Parameters[cropParameters]
if !ok {

params, ok = ctx.Parameters[focalPointCropParameters]

if !ok {
return &bimg.Options{}, nil
}

params = strings.Split(params[0], ",")
if len(params) != 5 {
return &bimg.Options{}, nil
}

// TODO do focal point crop
return &bimg.Options{}, nil
}

Expand Down

0 comments on commit b51b45e

Please sign in to comment.