Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gravity and rate offset option for watermark image #240

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions image_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package bimg

import (
"bytes"
"fmt"
"image"
"image/color"
"image/png"
"path"
"testing"
)
Expand Down Expand Up @@ -269,6 +273,104 @@ func TestImageWatermarkWithImage(t *testing.T) {
Write("testdata/test_watermark_image_out.jpg", buf)
}

func TestImageWatermarkWithImageGravity(t *testing.T) {
tests := []struct {
name string
wm WatermarkImage
validateFunc func(buf []byte)
}{
{"1nw", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravityNorthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 0, 0, 29, 19) }},
{"2n", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravityNorth}, func(buf []byte) { validateWatermarkArea(t, buf, 45, 0, 74, 19) }},
{"3ne", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravityNorthEast}, func(buf []byte) { validateWatermarkArea(t, buf, 90, 0, 119, 19) }},
{"4w", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravityWest}, func(buf []byte) { validateWatermarkArea(t, buf, 0, 40, 29, 59) }},
{"5c", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravityCentre}, func(buf []byte) { validateWatermarkArea(t, buf, 45, 40, 74, 59) }},
{"6e", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravityEast}, func(buf []byte) { validateWatermarkArea(t, buf, 90, 40, 119, 59) }},
{"7sw", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravitySouthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 0, 80, 29, 99) }},
{"8s", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravitySouth}, func(buf []byte) { validateWatermarkArea(t, buf, 45, 80, 74, 99) }},
{"9se", WatermarkImage{X: 0, Y: 0, Gravity: WatermarkGravitySouthEast}, func(buf []byte) { validateWatermarkArea(t, buf, 90, 80, 119, 99) }},
{"10off10nw", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravityNorthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 10, 10, 39, 29) }},
{"11off10n", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravityNorth}, func(buf []byte) { validateWatermarkArea(t, buf, 55, 10, 84, 29) }},
{"12off10ne", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravityNorthEast}, func(buf []byte) { validateWatermarkArea(t, buf, 80, 10, 109, 29) }},
{"13off10w", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravityWest}, func(buf []byte) { validateWatermarkArea(t, buf, 10, 50, 39, 69) }},
{"14off10c", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravityCentre}, func(buf []byte) { validateWatermarkArea(t, buf, 55, 50, 84, 69) }},
{"15off10e", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravityEast}, func(buf []byte) { validateWatermarkArea(t, buf, 80, 50, 109, 69) }},
{"16off10sw", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravitySouthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 10, 70, 39, 89) }},
{"17off10s", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravitySouth}, func(buf []byte) { validateWatermarkArea(t, buf, 55, 70, 84, 89) }},
{"18off10se", WatermarkImage{X: 10, Y: 10, Gravity: WatermarkGravitySouthEast}, func(buf []byte) { validateWatermarkArea(t, buf, 80, 70, 109, 89) }},
{"19off10cminus", WatermarkImage{X: -10, Y: -10, Gravity: WatermarkGravityCentre}, func(buf []byte) { validateWatermarkArea(t, buf, 35, 30, 64, 49) }},
{"20pnt10nw", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravityNorthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 12, 10, 41, 29) }},
{"21pnt10n", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravityNorth}, func(buf []byte) { validateWatermarkArea(t, buf, 57, 10, 86, 29) }},
{"22pnt10ne", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravityNorthEast}, func(buf []byte) { validateWatermarkArea(t, buf, 78, 10, 107, 29) }},
{"23pnt10w", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravityWest}, func(buf []byte) { validateWatermarkArea(t, buf, 12, 50, 41, 69) }},
{"24pnt10c", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravityCentre}, func(buf []byte) { validateWatermarkArea(t, buf, 57, 50, 86, 69) }},
{"25pnt10e", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravityEast}, func(buf []byte) { validateWatermarkArea(t, buf, 79, 50, 107, 69) }},
{"26pnt10sw", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravitySouthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 12, 70, 41, 89) }},
{"27pnt10s", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravitySouth}, func(buf []byte) { validateWatermarkArea(t, buf, 57, 70, 86, 89) }},
{"28pnt10se", WatermarkImage{XRate: 0.1, YRate: 0.1, Gravity: WatermarkGravitySouthEast}, func(buf []byte) { validateWatermarkArea(t, buf, 78, 70, 107, 89) }},
{"29pnt10cminus", WatermarkImage{XRate: -0.1, YRate: -0.1, Gravity: WatermarkGravityCentre}, func(buf []byte) { validateWatermarkArea(t, buf, 33, 30, 62, 49) }},
{"50protruded1", WatermarkImage{X: -10000, Y: -10000, Gravity: WatermarkGravityNorthWest}, func(buf []byte) { validateWatermarkArea(t, buf, 0, 0, 29, 19) }},
{"51protruded2", WatermarkImage{X: 10000, Y: 10000, Gravity: WatermarkGravityCentre}, func(buf []byte) { validateWatermarkArea(t, buf, 90, 80, 119, 99) }},
{"60oldway", WatermarkImage{Left: 10, Top: 10}, func(buf []byte) { validateWatermarkArea(t, buf, 10, 10, 39, 29) }},
{"61none", WatermarkImage{}, func(buf []byte) { validateWatermarkArea(t, buf, 0, 0, 29, 19) }},
}

for _, test := range tests {
image := initImage("white_1000x1000.png")
watermark, _ := imageBuf("black_30x20.png")

_, err := image.Crop(120, 100, GravityNorth)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}

test.wm.Buf = watermark
_, err = image.WatermarkImage(test.wm)
if err != nil {
t.Error(err)
}

buf, err := image.Colourspace(InterpretationBW)
if err != nil {
t.Error(err)
}

test.validateFunc(buf)

Write("testdata/test_watermark_image_gravity_"+test.name+"_out.png", buf)
}
}

func validateWatermarkArea(t *testing.T, buf []byte, x0, y0, x1, y1 int) {
imgResult, err := png.Decode(bytes.NewReader(buf))
if err != nil {
t.Error(err)
}

imgWidth := imgResult.Bounds().Max.X
imgHeight := imgResult.Bounds().Max.Y
assertPixelColor(t, imgResult, x0, y0, color.Black)
assertPixelColor(t, imgResult, x1, y0, color.Black)
assertPixelColor(t, imgResult, x1, y1, color.Black)
assertPixelColor(t, imgResult, x0, y1, color.Black)

perimeterNW := image.Pt(x0-1, y0-1)
if perimeterNW.X >= 0 && perimeterNW.Y >= 0 {
assertPixelColor(t, imgResult, perimeterNW.X, perimeterNW.Y, color.White)
}
perimeterNE := image.Pt(x1+1, y0-1)
if perimeterNE.X < imgWidth && perimeterNE.Y >= 0 {
assertPixelColor(t, imgResult, perimeterNE.X, perimeterNE.Y, color.White)
}
perimeterSW := image.Pt(x1+1, y1+1)
if perimeterSW.X < imgWidth && perimeterSW.Y < imgHeight {
assertPixelColor(t, imgResult, perimeterSW.X, perimeterSW.Y, color.White)
}
perimeterSE := image.Pt(x0-1, y1+1)
if perimeterSE.X >= 0 && perimeterSE.Y < imgHeight {
assertPixelColor(t, imgResult, perimeterSE.X, perimeterSE.Y, color.White)
}
}

func TestImageWatermarkNoReplicate(t *testing.T) {
image := initImage("test.jpg")
_, err := image.Crop(800, 600, GravityNorth)
Expand Down Expand Up @@ -550,3 +652,11 @@ func assertSize(buf []byte, width, height int) error {
}
return nil
}

func assertPixelColor(t *testing.T, imgResult image.Image, x, y int, colorExpect color.Color) {
ceR, ceG, ceB, ceA := colorExpect.RGBA()
caR, caG, caB, caA := imgResult.At(x, y).RGBA()
if ceR != caR || ceG != caG || ceB != caB || ceA != caA {
t.Error(fmt.Errorf("Expected pixel color (%02X%02X%02X%02X), but actual (%02X%02X%02X%02X) at (%d, %d)", uint8(ceR), uint8(ceG), uint8(ceB), uint8(ceA), uint8(caR), uint8(caG), uint8(caB), uint8(caA), x, y))
}
}
33 changes: 31 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@ const (
GravitySmart
)

// WatermarkGravity represents the image gravity value for watermark.
type WatermarkGravity int

const (
// WatermarkGravityNorthWest represents the north west value used for image gravity orientation.
WatermarkGravityNorthWest WatermarkGravity = iota
// WatermarkGravityNorth represents the north value used for image gravity orientation.
WatermarkGravityNorth
// WatermarkGravityNorthEast represents the south east value used for image gravity orientation.
WatermarkGravityNorthEast
// WatermarkGravityWest represents the west value used for image gravity orientation.
WatermarkGravityWest
// WatermarkGravityCentre represents the centre value used for image gravity orientation.
WatermarkGravityCentre
// WatermarkGravityEast represents the east value used for image gravity orientation.
WatermarkGravityEast
// WatermarkGravitySouthWest represents the south west value used for image gravity orientation.
WatermarkGravitySouthWest
// WatermarkGravitySouth represents the south value used for image gravity orientation.
WatermarkGravitySouth
// WatermarkGravitySouthEast represents the south east value used for image gravity orientation.
WatermarkGravitySouthEast
)

// Interpolator represents the image interpolation value.
type Interpolator int

Expand Down Expand Up @@ -164,9 +188,14 @@ type Watermark struct {

// WatermarkImage represents the image-based watermark supported options.
type WatermarkImage struct {
Left int
Top int
Buf []byte
Left int // Deprecated, use: X
Top int // Deprecated, use: Y
X int
Y int
XRate float32
YRate float32
Gravity WatermarkGravity
Opacity float32
}

Expand Down
84 changes: 83 additions & 1 deletion resizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,19 +357,101 @@ func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.Vi
return image, nil
}

watermark, _, err := vipsRead(w.Buf)
if err != nil {
return nil, err
}

if w.Opacity == 0.0 {
w.Opacity = 1.0
}

image, err := vipsDrawWatermark(image, w)
// for backward compatibility
if (w.Left != 0 || w.Top != 0) && w.X == 0 && w.Y == 0 && w.XRate == 0 && w.YRate == 0 {
w.X, w.Y = w.Left, w.Top
w.Gravity = WatermarkGravityNorthWest
}

var left, top int
if w.XRate != 0 || w.YRate != 0 {
xOffset := int(float32(image.Xsize) * w.XRate)
yOffset := int(float32(image.Ysize) * w.YRate)
left, top = calculateWatermarkImagePosition(int(image.Xsize), int(image.Ysize), int(watermark.Xsize), int(watermark.Ysize), xOffset, yOffset, w.Gravity)
} else {
left, top = calculateWatermarkImagePosition(int(image.Xsize), int(image.Ysize), int(watermark.Xsize), int(watermark.Ysize), w.X, w.Y, w.Gravity)
}

image, err = vipsDrawWatermark(image, watermark, left, top, w.Opacity)
if err != nil {
return nil, err
}

return image, nil
}

func calculateWatermarkImagePosition(destWidth, destHeight, wmWidth, wmHeight, xOffset, yOffset int, gravity WatermarkGravity) (int, int) {
left, top := 0, 0

// Ensure watermark size <= destination size
if wmWidth > destWidth {
wmWidth = destWidth
}
if wmHeight > destHeight {
wmHeight = destHeight
}

switch gravity {
case WatermarkGravityNorthWest:
left = xOffset
top = yOffset
case WatermarkGravityNorth:
left = ((destWidth - wmWidth + 1) / 2) + xOffset
top = yOffset
case WatermarkGravityNorthEast:
left = destWidth - wmWidth - xOffset
top = yOffset
case WatermarkGravityWest:
left = xOffset
top = ((destHeight - wmHeight + 1) / 2) + yOffset
case WatermarkGravityCentre:
left = ((destWidth - wmWidth + 1) / 2) + xOffset
top = ((destHeight - wmHeight + 1) / 2) + yOffset
case WatermarkGravityEast:
left = destWidth - wmWidth - xOffset
top = ((destHeight - wmHeight + 1) / 2) + yOffset
case WatermarkGravitySouthWest:
left = xOffset
top = destHeight - wmHeight - yOffset
case WatermarkGravitySouth:
left = ((destWidth - wmWidth + 1) / 2) + xOffset
top = destHeight - wmHeight - yOffset
case WatermarkGravitySouthEast:
left = destWidth - wmWidth - xOffset
top = destHeight - wmHeight - yOffset
default:
left = xOffset
top = yOffset
}

// correct to fit into destination image rectangle
minLeft := 0
maxLeft := destWidth - wmWidth
minTop := 0
maxTop := destHeight - wmHeight
if left < minLeft {
left = minLeft
} else if left > maxLeft {
left = maxLeft
}
if top < minTop {
top = minTop
} else if top > maxTop {
top = maxTop
}

return left, top
}

func imageFlatten(image *C.VipsImage, imageType ImageType, o Options) (*C.VipsImage, error) {
// Only PNG images are supported for now
if imageType != PNG || o.Background == ColorBlack {
Expand Down
Binary file added testdata/black_30x20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/white_1000x1000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 2 additions & 7 deletions vips.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,15 +685,10 @@ func max(x int) int {
return int(math.Max(float64(x), 0))
}

func vipsDrawWatermark(image *C.VipsImage, o WatermarkImage) (*C.VipsImage, error) {
func vipsDrawWatermark(image, watermark *C.VipsImage, left, top int, opacity float32) (*C.VipsImage, error) {
var out *C.VipsImage

watermark, _, e := vipsRead(o.Buf)
if e != nil {
return nil, e
}

opts := vipsWatermarkImageOptions{C.int(o.Left), C.int(o.Top), C.float(o.Opacity)}
opts := vipsWatermarkImageOptions{C.int(left), C.int(top), C.float(opacity)}

err := C.vips_watermark_image(image, watermark, &out, (*C.WatermarkImageOptions)(unsafe.Pointer(&opts)))

Expand Down
8 changes: 6 additions & 2 deletions vips_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,12 @@ func TestVipsWatermarkWithImage(t *testing.T) {

watermark := readImage("transparent.png")

options := WatermarkImage{Left: 100, Top: 100, Opacity: 1.0, Buf: watermark}
newImg, err := vipsDrawWatermark(image, options)
watermarkImage, _, err := vipsRead(watermark)
if err != nil {
t.Errorf("Cannot add watermark: %s", err)
}

newImg, err := vipsDrawWatermark(image, watermarkImage, 100, 100, 1.0)
if err != nil {
t.Errorf("Cannot add watermark: %s", err)
}
Expand Down