From 8857970e695434d6f298770493fcb95f378cb1d2 Mon Sep 17 00:00:00 2001 From: tsu1980 Date: Sat, 28 Apr 2018 16:18:35 +0900 Subject: [PATCH 1/2] Add gravity and rate offset option for watermark image --- image_test.go | 113 +++++++++++++++++++++++++++++++++++ options.go | 33 +++++++++- resizer.go | 84 +++++++++++++++++++++++++- testdata/black_30x20.png | Bin 0 -> 873 bytes testdata/white_1000x1000.png | Bin 0 -> 5234 bytes vips.go | 9 +-- vips_test.go | 8 ++- 7 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 testdata/black_30x20.png create mode 100644 testdata/white_1000x1000.png diff --git a/image_test.go b/image_test.go index 5af0431d..9924aeba 100644 --- a/image_test.go +++ b/image_test.go @@ -1,7 +1,11 @@ package bimg import ( + "bytes" "fmt" + "image" + "image/color" + "image/png" "path" "testing" ) @@ -269,6 +273,106 @@ 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) { + t.Helper() + + 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) @@ -550,3 +654,12 @@ func assertSize(buf []byte, width, height int) error { } return nil } + +func assertPixelColor(t *testing.T, imgResult image.Image, x, y int, colorExpect color.Color) { + t.Helper() + 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)) + } +} diff --git a/options.go b/options.go index 17a1cb4c..68206f7b 100644 --- a/options.go +++ b/options.go @@ -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 @@ -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 } diff --git a/resizer.go b/resizer.go index 98a95ae7..4f7b7a9d 100644 --- a/resizer.go +++ b/resizer.go @@ -357,12 +357,31 @@ 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 } @@ -370,6 +389,69 @@ func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.Vi 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 { diff --git a/testdata/black_30x20.png b/testdata/black_30x20.png new file mode 100644 index 0000000000000000000000000000000000000000..b23f659f3540b69ae3cb37dbd832c2c582b3e5be GIT binary patch literal 873 zcmV-v1D5=WP)6ciK{6%`g178e&67#J8C85tTH8XFrM92^`S9UUGX9v>ecARr(iAt53nA|oRs zBqSsyB_$>%CMPE+C@3f?DJd!{Dl021EG#T7EiEoCE-x=HFfcGNF)=bSGBYzXG&D3d zH8nOiHa9mnI5;>tIXOByIy*Z%JUl!-Jv}}?K0iM{KtMo2K|w-7LPJACL_|bIMMXwN zMn^|SNJvOYNl8jdN=r*iOiWBoO-)WtPESuyP*6}&QBhJ-Qd3h?R8&+|RaI72R##V7 zSXfwDSy@_IT3cINTwGjTU0q&YUSD5dU|?WjVPRroVq;@tWMpJzWo2e&W@l$-XlQ6@ zX=!R|YHMq2Y;0_8ZEbFDZf|dIaBy&OadC2Ta&vQYbaZreb#-=jc6WDoczAeud3kzz zdV70&e0+R;eSLm@et&;|fPjF3fq{a8f`fyDgoK2Jg@uNOhKGlTh=_=ZiHVAeii?Yj zjEszpjg5|uj*pLzkdTm(k&%*;l9Q8@l$4Z}m6ev3mY0{8n3$NEnVFiJnwy)OoSdAU zot>VZo}ZteprD|kp`oIpqNAguq@<*!rKP5(rl+T;sHmu^si~@}s;jH3tgNi9t*x%E zuCK4Ju&}VPv9YqUva_?Zw6wIfwY9dkwzs#pxVX5vxw*Q!y1To(yu7@dCU$jHda$;ryf%FD~k%*@Qq&CSlv&d<-!(9qD) z(b3Y<($mw^)YR0~)z#M4*4Nk9*x1lt)=I7_<=;-L_>FMg~>g((4?Ck9A?d|UF?(gsK@bK{Q@$vHV^7Hfa z^z`)g_4W4l_V@Sq`1ttw`T6?#`uqF){QUg={r&#_{{R2~;;>zP00009a7bBm000ie z000ie0hKEb8vpF6XfIa{K0&D;QyY+h400000NkvXXu0mjfSM!$U literal 0 HcmV?d00001 diff --git a/testdata/white_1000x1000.png b/testdata/white_1000x1000.png new file mode 100644 index 0000000000000000000000000000000000000000..b10b38637e78a5a25b0d3c5ba60f3e247e53e372 GIT binary patch literal 5234 zcmeAS@N?(olHy`uVBq!ia0y~yV15C@985rwLkFEV11Zh|kH}&M2EHR8%s5q>Pnv;2 zM8(s^F{EP7+iM4T4=4y6*l_7@xt-es&WUe0Kb@I1lb?a1;�?P_u>cI|c@UggcB3 z3I}#DGdMI9vNJR=JmzCyU_2(vz`)c4RKy}t&cMK7Fsg7gct#V&XjT|4DMri3(OO}& z(j09nj5d%)n}wsT=Fz^wXb)+$13B7l9vv(g9U>VWK^j8iW@ZfuTxT~w+gTe~DWM4fe9(lV literal 0 HcmV?d00001 diff --git a/vips.go b/vips.go index fb179013..07ac1a01 100644 --- a/vips.go +++ b/vips.go @@ -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))) diff --git a/vips_test.go b/vips_test.go index d8bd2b23..db6f901c 100644 --- a/vips_test.go +++ b/vips_test.go @@ -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) } From c879a1092d0e67a047a706e449f819dfbe76822a Mon Sep 17 00:00:00 2001 From: tsu1980 Date: Mon, 30 Apr 2018 19:43:19 +0900 Subject: [PATCH 2/2] Remove t.Helper() --- image_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/image_test.go b/image_test.go index 9924aeba..87439b84 100644 --- a/image_test.go +++ b/image_test.go @@ -341,8 +341,6 @@ func TestImageWatermarkWithImageGravity(t *testing.T) { } func validateWatermarkArea(t *testing.T, buf []byte, x0, y0, x1, y1 int) { - t.Helper() - imgResult, err := png.Decode(bytes.NewReader(buf)) if err != nil { t.Error(err) @@ -656,7 +654,6 @@ func assertSize(buf []byte, width, height int) error { } func assertPixelColor(t *testing.T, imgResult image.Image, x, y int, colorExpect color.Color) { - t.Helper() ceR, ceG, ceB, ceA := colorExpect.RGBA() caR, caG, caB, caA := imgResult.At(x, y).RGBA() if ceR != caR || ceG != caG || ceB != caB || ceA != caA {