mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-07-02 22:46:50 +02:00
Add gravity to extend option
This commit is contained in:
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Docker image base is changed to Debian 10 for better stability and performance.
|
- Docker image base is changed to Debian 10 for better stability and performance.
|
||||||
|
- `extend` option now supports gravity.
|
||||||
|
|
||||||
## [2.7.0] - 2019-11-13
|
## [2.7.0] - 2019-11-13
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -121,13 +121,14 @@ Default: false
|
|||||||
#### Extend
|
#### Extend
|
||||||
|
|
||||||
```
|
```
|
||||||
extend:%extend
|
extend:%extend:%gravity
|
||||||
ex:%extend
|
ex:%extend:%gravity
|
||||||
```
|
```
|
||||||
|
|
||||||
When set to `1`, `t` or `true`, imgproxy will extend the image if it is smaller than the given size.
|
* When `extend` is set to `1`, `t` or `true`, imgproxy will extend the image if it is smaller than the given size.
|
||||||
|
* `gravity` _(optional)_ accepts the same values as [gravity](#gravity) option, except `sm`. When `gravity` is not set, imgproxy will use `ce` gravity without offsets.
|
||||||
|
|
||||||
Default: false
|
Default: `false:ce:0:0`
|
||||||
|
|
||||||
#### Gravity
|
#### Gravity
|
||||||
|
|
||||||
@ -167,7 +168,7 @@ c:%width:%height:%gravity
|
|||||||
Defines an area of the image to be processed (crop before resize).
|
Defines an area of the image to be processed (crop before resize).
|
||||||
|
|
||||||
* `width` and `height` define the size of the area. When `width` or `height` is set to `0`, imgproxy will use the full width/height of the source image.
|
* `width` and `height` define the size of the area. When `width` or `height` is set to `0`, imgproxy will use the full width/height of the source image.
|
||||||
* `gravity` accepts the same values as [gravity](#gravity) option. When `gravity` is not set, imgproxy will use the value of the [gravity](#gravity) option.
|
* `gravity` _(optional)_ accepts the same values as [gravity](#gravity) option. When `gravity` is not set, imgproxy will use the value of the [gravity](#gravity) option.
|
||||||
|
|
||||||
#### Quality
|
#### Quality
|
||||||
|
|
||||||
|
67
process.go
67
process.go
@ -157,40 +157,48 @@ func calcJpegShink(scale float64, imgtype imageType) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcCrop(width, height, cropWidth, cropHeight int, gravity *gravityOptions) (left, top int) {
|
func calcPosition(width, height, innerWidth, innerHeight int, gravity *gravityOptions, allowOverflow bool) (left, top int) {
|
||||||
if gravity.Type == gravityFocusPoint {
|
if gravity.Type == gravityFocusPoint {
|
||||||
pointX := scaleInt(width, gravity.X)
|
pointX := scaleInt(width, gravity.X)
|
||||||
pointY := scaleInt(height, gravity.Y)
|
pointY := scaleInt(height, gravity.Y)
|
||||||
|
|
||||||
left = maxInt(0, minInt(pointX-cropWidth/2, width-cropWidth))
|
left = pointX - innerWidth/2
|
||||||
top = maxInt(0, minInt(pointY-cropHeight/2, height-cropHeight))
|
top = pointY - innerHeight/2
|
||||||
|
} else {
|
||||||
|
offX, offY := int(gravity.X), int(gravity.Y)
|
||||||
|
|
||||||
return
|
left = (width-innerWidth+1)/2 + offX
|
||||||
|
top = (height-innerHeight+1)/2 + offY
|
||||||
|
|
||||||
|
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
|
||||||
|
top = 0 + offY
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
|
||||||
|
left = width - innerWidth - offX
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
|
||||||
|
top = height - innerHeight - offY
|
||||||
|
}
|
||||||
|
|
||||||
|
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
|
||||||
|
left = 0 + offX
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
offX, offY := int(gravity.X), int(gravity.Y)
|
var minX, maxX, minY, maxY int
|
||||||
|
|
||||||
left = (width-cropWidth+1)/2 + offX
|
if allowOverflow {
|
||||||
top = (height-cropHeight+1)/2 + offY
|
minX, maxX = -innerWidth+1, width-1
|
||||||
|
minY, maxY = -innerHeight+1, height-1
|
||||||
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
|
} else {
|
||||||
top = 0 + offY
|
minX, maxX = 0, width-innerWidth
|
||||||
|
minY, maxY = 0, height-innerHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
|
left = maxInt(minX, minInt(left, maxX))
|
||||||
left = width - cropWidth - offX
|
top = maxInt(minY, minInt(top, maxY))
|
||||||
}
|
|
||||||
|
|
||||||
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
|
|
||||||
top = height - cropHeight - offY
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
|
|
||||||
left = 0 + offX
|
|
||||||
}
|
|
||||||
|
|
||||||
left = maxInt(0, minInt(left, width-cropWidth))
|
|
||||||
top = maxInt(0, minInt(top, height-cropHeight))
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -221,7 +229,7 @@ func cropImage(img *vipsImage, cropWidth, cropHeight int, gravity *gravityOption
|
|||||||
return img.CopyMemory()
|
return img.CopyMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
left, top := calcCrop(imgWidth, imgHeight, cropWidth, cropHeight, gravity)
|
left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, false)
|
||||||
return img.Crop(left, top, cropWidth, cropHeight)
|
return img.Crop(left, top, cropWidth, cropHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +261,9 @@ func prepareWatermark(wm *vipsImage, wmData *imageData, opts *watermarkOptions,
|
|||||||
return wm.Replicate(imgWidth, imgHeight)
|
return wm.Replicate(imgWidth, imgHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wm.Embed(opts.Gravity, imgWidth, imgHeight, opts.OffsetX, opts.OffsetY, rgbColor{0, 0, 0})
|
left, top := calcPosition(imgWidth, imgWidth, wm.Width(), wm.Height(), &opts.Gravity, true)
|
||||||
|
|
||||||
|
return wm.Embed(imgWidth, imgHeight, left, top, rgbColor{0, 0, 0})
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyWatermark(img *vipsImage, wmData *imageData, opts *watermarkOptions, framesCount int) error {
|
func applyWatermark(img *vipsImage, wmData *imageData, opts *watermarkOptions, framesCount int) error {
|
||||||
@ -440,8 +450,9 @@ func transformImage(ctx context.Context, img *vipsImage, data []byte, po *proces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if po.Extend && (po.Width > img.Width() || po.Height > img.Height()) {
|
if po.Extend.Enabled && (po.Width > img.Width() || po.Height > img.Height()) {
|
||||||
if err = img.Embed(gravityCenter, po.Width, po.Height, 0, 0, po.Background); err != nil {
|
offX, offY := calcPosition(po.Width, po.Height, img.Width(), img.Height(), &po.Extend.Gravity, false)
|
||||||
|
if err = img.Embed(po.Width, po.Height, offX, offY, po.Background); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,11 @@ type gravityOptions struct {
|
|||||||
X, Y float64
|
X, Y float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type extendOptions struct {
|
||||||
|
Enabled bool
|
||||||
|
Gravity gravityOptions
|
||||||
|
}
|
||||||
|
|
||||||
type cropOptions struct {
|
type cropOptions struct {
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
@ -99,9 +104,7 @@ type watermarkOptions struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
Opacity float64
|
Opacity float64
|
||||||
Replicate bool
|
Replicate bool
|
||||||
Gravity gravityType
|
Gravity gravityOptions
|
||||||
OffsetX int
|
|
||||||
OffsetY int
|
|
||||||
Scale float64
|
Scale float64
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +115,7 @@ type processingOptions struct {
|
|||||||
Dpr float64
|
Dpr float64
|
||||||
Gravity gravityOptions
|
Gravity gravityOptions
|
||||||
Enlarge bool
|
Enlarge bool
|
||||||
Extend bool
|
Extend extendOptions
|
||||||
Crop cropOptions
|
Crop cropOptions
|
||||||
Format imageType
|
Format imageType
|
||||||
Quality int
|
Quality int
|
||||||
@ -194,6 +197,7 @@ func newProcessingOptions() *processingOptions {
|
|||||||
Height: 0,
|
Height: 0,
|
||||||
Gravity: gravityOptions{Type: gravityCenter},
|
Gravity: gravityOptions{Type: gravityCenter},
|
||||||
Enlarge: false,
|
Enlarge: false,
|
||||||
|
Extend: extendOptions{Enabled: false, Gravity: gravityOptions{Type: gravityCenter}},
|
||||||
Quality: conf.Quality,
|
Quality: conf.Quality,
|
||||||
MaxBytes: 0,
|
MaxBytes: 0,
|
||||||
Format: imageTypeUnknown,
|
Format: imageTypeUnknown,
|
||||||
@ -201,7 +205,7 @@ func newProcessingOptions() *processingOptions {
|
|||||||
Blur: 0,
|
Blur: 0,
|
||||||
Sharpen: 0,
|
Sharpen: 0,
|
||||||
Dpr: 1,
|
Dpr: 1,
|
||||||
Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityCenter},
|
Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityOptions{Type: gravityCenter}},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -416,17 +420,27 @@ func applyEnlargeOption(po *processingOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyExtendOption(po *processingOptions, args []string) error {
|
func applyExtendOption(po *processingOptions, args []string) error {
|
||||||
if len(args) > 1 {
|
if len(args) > 4 {
|
||||||
return fmt.Errorf("Invalid extend arguments: %v", args)
|
return fmt.Errorf("Invalid extend arguments: %v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
po.Extend = parseBoolOption(args[0])
|
po.Extend.Enabled = parseBoolOption(args[0])
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
if err := parseGravity(&po.Extend.Gravity, args[1:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.Extend.Gravity.Type == gravitySmart {
|
||||||
|
return errors.New("extend doesn't support smart gravity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applySizeOption(po *processingOptions, args []string) (err error) {
|
func applySizeOption(po *processingOptions, args []string) (err error) {
|
||||||
if len(args) > 4 {
|
if len(args) > 7 {
|
||||||
return fmt.Errorf("Invalid size arguments: %v", args)
|
return fmt.Errorf("Invalid size arguments: %v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,8 +462,8 @@ func applySizeOption(po *processingOptions, args []string) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) == 4 && len(args[3]) > 0 {
|
if len(args) >= 4 && len(args[3]) > 0 {
|
||||||
if err = applyExtendOption(po, args[3:4]); err != nil {
|
if err = applyExtendOption(po, args[3:]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -472,7 +486,7 @@ func applyResizingTypeOption(po *processingOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyResizeOption(po *processingOptions, args []string) error {
|
func applyResizeOption(po *processingOptions, args []string) error {
|
||||||
if len(args) > 5 {
|
if len(args) > 8 {
|
||||||
return fmt.Errorf("Invalid resize arguments: %v", args)
|
return fmt.Errorf("Invalid resize arguments: %v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,7 +678,7 @@ func applyWatermarkOption(po *processingOptions, args []string) error {
|
|||||||
if args[1] == "re" {
|
if args[1] == "re" {
|
||||||
po.Watermark.Replicate = true
|
po.Watermark.Replicate = true
|
||||||
} else if g, ok := gravityTypes[args[1]]; ok && g != gravityFocusPoint && g != gravitySmart {
|
} else if g, ok := gravityTypes[args[1]]; ok && g != gravityFocusPoint && g != gravitySmart {
|
||||||
po.Watermark.Gravity = g
|
po.Watermark.Gravity.Type = g
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Invalid watermark position: %s", args[1])
|
return fmt.Errorf("Invalid watermark position: %s", args[1])
|
||||||
}
|
}
|
||||||
@ -672,7 +686,7 @@ func applyWatermarkOption(po *processingOptions, args []string) error {
|
|||||||
|
|
||||||
if len(args) > 2 && len(args[2]) > 0 {
|
if len(args) > 2 && len(args[2]) > 0 {
|
||||||
if x, err := strconv.Atoi(args[2]); err == nil {
|
if x, err := strconv.Atoi(args[2]); err == nil {
|
||||||
po.Watermark.OffsetX = x
|
po.Watermark.Gravity.X = float64(x)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Invalid watermark X offset: %s", args[2])
|
return fmt.Errorf("Invalid watermark X offset: %s", args[2])
|
||||||
}
|
}
|
||||||
@ -680,7 +694,7 @@ func applyWatermarkOption(po *processingOptions, args []string) error {
|
|||||||
|
|
||||||
if len(args) > 3 && len(args[3]) > 0 {
|
if len(args) > 3 && len(args[3]) > 0 {
|
||||||
if y, err := strconv.Atoi(args[3]); err == nil {
|
if y, err := strconv.Atoi(args[3]); err == nil {
|
||||||
po.Watermark.OffsetY = y
|
po.Watermark.Gravity.Y = float64(y)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Invalid watermark Y offset: %s", args[3])
|
return fmt.Errorf("Invalid watermark Y offset: %s", args[3])
|
||||||
}
|
}
|
||||||
|
@ -196,6 +196,19 @@ func (s *ProcessingOptionsTestSuite) TestParsePathAdvancedEnlarge() {
|
|||||||
assert.True(s.T(), po.Enlarge)
|
assert.True(s.T(), po.Enlarge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingOptionsTestSuite) TestParsePathAdvancedExtend() {
|
||||||
|
req := s.getRequest("http://example.com/unsafe/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg")
|
||||||
|
ctx, err := parsePath(context.Background(), req)
|
||||||
|
|
||||||
|
require.Nil(s.T(), err)
|
||||||
|
|
||||||
|
po := getProcessingOptions(ctx)
|
||||||
|
assert.Equal(s.T(), true, po.Extend.Enabled)
|
||||||
|
assert.Equal(s.T(), gravitySouth, po.Extend.Gravity.Type)
|
||||||
|
assert.Equal(s.T(), 10.0, po.Extend.Gravity.X)
|
||||||
|
assert.Equal(s.T(), 20.0, po.Extend.Gravity.Y)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathAdvancedGravity() {
|
func (s *ProcessingOptionsTestSuite) TestParsePathAdvancedGravity() {
|
||||||
req := s.getRequest("http://example.com/unsafe/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg")
|
req := s.getRequest("http://example.com/unsafe/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg")
|
||||||
ctx, err := parsePath(context.Background(), req)
|
ctx, err := parsePath(context.Background(), req)
|
||||||
@ -300,9 +313,9 @@ func (s *ProcessingOptionsTestSuite) TestParsePathAdvancedWatermark() {
|
|||||||
|
|
||||||
po := getProcessingOptions(ctx)
|
po := getProcessingOptions(ctx)
|
||||||
assert.True(s.T(), po.Watermark.Enabled)
|
assert.True(s.T(), po.Watermark.Enabled)
|
||||||
assert.Equal(s.T(), gravitySouthEast, po.Watermark.Gravity)
|
assert.Equal(s.T(), gravitySouthEast, po.Watermark.Gravity.Type)
|
||||||
assert.Equal(s.T(), 10, po.Watermark.OffsetX)
|
assert.Equal(s.T(), 10.0, po.Watermark.Gravity.X)
|
||||||
assert.Equal(s.T(), 20, po.Watermark.OffsetY)
|
assert.Equal(s.T(), 20.0, po.Watermark.Gravity.Y)
|
||||||
assert.Equal(s.T(), 0.6, po.Watermark.Scale)
|
assert.Equal(s.T(), 0.6, po.Watermark.Scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
38
vips.go
38
vips.go
@ -489,41 +489,7 @@ func (img *vipsImage) Replicate(width, height int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *vipsImage) Embed(gravity gravityType, width, height int, offX, offY int, bg rgbColor) error {
|
func (img *vipsImage) Embed(width, height int, offX, offY int, bg rgbColor) error {
|
||||||
wmWidth := img.Width()
|
|
||||||
wmHeight := img.Height()
|
|
||||||
|
|
||||||
left := (width-wmWidth+1)/2 + offX
|
|
||||||
top := (height-wmHeight+1)/2 + offY
|
|
||||||
|
|
||||||
if gravity == gravityNorth || gravity == gravityNorthEast || gravity == gravityNorthWest {
|
|
||||||
top = offY
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity == gravityEast || gravity == gravityNorthEast || gravity == gravitySouthEast {
|
|
||||||
left = width - wmWidth - offX
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity == gravitySouth || gravity == gravitySouthEast || gravity == gravitySouthWest {
|
|
||||||
top = height - wmHeight - offY
|
|
||||||
}
|
|
||||||
|
|
||||||
if gravity == gravityWest || gravity == gravityNorthWest || gravity == gravitySouthWest {
|
|
||||||
left = offX
|
|
||||||
}
|
|
||||||
|
|
||||||
if left > width {
|
|
||||||
left = width - wmWidth
|
|
||||||
} else if left < -wmWidth {
|
|
||||||
left = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if top > height {
|
|
||||||
top = height - wmHeight
|
|
||||||
} else if top < -wmHeight {
|
|
||||||
top = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := img.RgbColourspace(); err != nil {
|
if err := img.RgbColourspace(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -536,7 +502,7 @@ func (img *vipsImage) Embed(gravity gravityType, width, height int, offX, offY i
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
if C.vips_embed_go(img.VipsImage, &tmp, C.int(left), C.int(top), C.int(width), C.int(height), &bgc[0], C.int(len(bgc))) != 0 {
|
if C.vips_embed_go(img.VipsImage, &tmp, C.int(offX), C.int(offY), C.int(width), C.int(height), &bgc[0], C.int(len(bgc))) != 0 {
|
||||||
return vipsError()
|
return vipsError()
|
||||||
}
|
}
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
Reference in New Issue
Block a user