diff --git a/.gitignore b/.gitignore index 3cf2565..fbba014 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ bin /*.png /*.webp /fixtures/*_out.* +/.idea/ diff --git a/image.go b/image.go index c383a20..14e4045 100644 --- a/image.go +++ b/image.go @@ -124,6 +124,12 @@ func (i *Image) Watermark(w Watermark) ([]byte, error) { return i.Process(options) } +// WatermarkImage adds image as watermark on the given image. +func (i *Image) WatermarkImage(w WatermarkImage) ([]byte, error) { + options := Options{WatermarkImage: w} + return i.Process(options) +} + // Zoom zooms the image by the given factor. // You should probably call Extract() before. func (i *Image) Zoom(factor int) ([]byte, error) { diff --git a/image_test.go b/image_test.go index dce17ba..5043f09 100644 --- a/image_test.go +++ b/image_test.go @@ -239,7 +239,34 @@ func TestImageWatermark(t *testing.T) { t.Fatal("Image is not jpeg") } - Write("fixtures/test_watermark_out.jpg", buf) + Write("fixtures/test_watermark_text_out.jpg", buf) +} + +func TestImageWatermarkWithImage(t *testing.T) { + image := initImage("test.jpg") + watermark, _ := imageBuf("transparent.png") + + _, err := image.Crop(800, 600, GravityNorth) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + buf, err := image.WatermarkImage(WatermarkImage{Left: 100, Top: 100, Buf: watermark}) + + if err != nil { + t.Error(err) + } + + err = assertSize(buf, 800, 600) + if err != nil { + t.Error(err) + } + + if DetermineImageType(buf) != JPEG { + t.Fatal("Image is not jpeg") + } + + Write("fixtures/test_watermark_image_out.jpg", buf) } func TestImageWatermarkNoReplicate(t *testing.T) { @@ -429,10 +456,14 @@ func TestFluentInterface(t *testing.T) { } func initImage(file string) *Image { - buf, _ := Read(path.Join("fixtures", file)) + buf, _ := imageBuf(file) return NewImage(buf) } +func imageBuf(file string) ([]byte, error) { + return Read(path.Join("fixtures", file)) +} + func assertSize(buf []byte, width, height int) error { size, err := NewImage(buf).Size() if err != nil { diff --git a/options.go b/options.go index 5920ea7..0826fa7 100644 --- a/options.go +++ b/options.go @@ -157,6 +157,14 @@ type Watermark struct { Background Color } +// WatermarkImage represents the image-based watermark supported options. +type WatermarkImage struct { + Left int + Top int + Buf []byte + Opacity float32 +} + // GaussianBlur represents the gaussian image transformation values. type GaussianBlur struct { Sigma float64 @@ -198,6 +206,7 @@ type Options struct { Background Color Gravity Gravity Watermark Watermark + WatermarkImage WatermarkImage Type ImageType Interpolator Interpolator Interpretation Interpretation diff --git a/resize.go b/resize.go index bab21f1..6a1016c 100644 --- a/resize.go +++ b/resize.go @@ -16,11 +16,8 @@ import ( func Resize(buf []byte, o Options) ([]byte, error) { defer C.vips_thread_shutdown() - if len(buf) == 0 { - return nil, errors.New("Image buffer is empty") - } + image, imageType, err := loadImage(buf) - image, imageType, err := vipsRead(buf) if err != nil { return nil, err } @@ -107,7 +104,13 @@ func Resize(buf []byte, o Options) ([]byte, error) { } // Add watermark, if necessary - image, err = watermakImage(image, o.Watermark) + image, err = watermarkImageWithText(image, o.Watermark) + if err != nil { + return nil, err + } + + // Add watermark, if necessary + image, err = watermarkImageWithAnotherImage(image, o.WatermarkImage) if err != nil { return nil, err } @@ -118,17 +121,20 @@ func Resize(buf []byte, o Options) ([]byte, error) { return nil, err } - saveOptions := vipsSaveOptions{ - Quality: o.Quality, - Type: o.Type, - Compression: o.Compression, - Interlace: o.Interlace, - NoProfile: o.NoProfile, - Interpretation: o.Interpretation, + return saveImage(image, o) +} + +func loadImage(buf []byte) (*C.VipsImage, ImageType, error) { + if len(buf) == 0 { + return nil, JPEG, errors.New("Image buffer is empty") } - // Finally get the resultant buffer - return vipsSave(image, saveOptions) + image, imageType, err := vipsRead(buf) + if err != nil { + return nil, JPEG, err + } + + return image, imageType, nil } func applyDefaults(o Options, imageType ImageType) Options { @@ -147,6 +153,19 @@ func applyDefaults(o Options, imageType ImageType) Options { return o } +func saveImage(image *C.VipsImage, o Options) ([]byte, error) { + saveOptions := vipsSaveOptions{ + Quality: o.Quality, + Type: o.Type, + Compression: o.Compression, + Interlace: o.Interlace, + NoProfile: o.NoProfile, + Interpretation: o.Interpretation, + } + // Finally get the resultant buffer + return vipsSave(image, saveOptions) +} + func normalizeOperation(o *Options, inWidth, inHeight int) { if !o.Force && !o.Crop && !o.Embed && !o.Enlarge && o.Rotate == 0 && (o.Width > 0 || o.Height > 0) { o.Force = true @@ -164,7 +183,6 @@ func shouldApplyEffects(o Options) bool { func transformImage(image *C.VipsImage, o Options, shrink int, residual float64) (*C.VipsImage, error) { var err error - // Use vips_shrink with the integral reduction if shrink > 1 { image, residual, err = shrinkImage(image, o, residual, shrink) @@ -242,6 +260,7 @@ func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend, o.Background) break + case o.Top != 0 || o.Left != 0 || o.AreaWidth != 0 || o.AreaHeight != 0: if o.AreaWidth == 0 { o.AreaHeight = o.Width @@ -293,7 +312,7 @@ func rotateAndFlipImage(image *C.VipsImage, o Options) (*C.VipsImage, bool, erro return image, rotated, err } -func watermakImage(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { +func watermarkImageWithText(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { if w.Text == "" { return image, nil } @@ -325,6 +344,31 @@ func watermakImage(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { return image, nil } +func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.VipsImage, error) { + + if len(w.Buf) == 0 { + return image, nil + } + + if w.Opacity == 0.0 { + w.Opacity = 1.0 + } + + watermark, _, err := loadImage(w.Buf) + + if err != nil { + return nil, err + } + + image, err = vipsDrawWatermark(image, watermark, w) + + if err != nil { + return nil, err + } + + return image, nil +} + 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/resize_test.go b/resize_test.go index 98c47f1..c5995f7 100644 --- a/resize_test.go +++ b/resize_test.go @@ -72,7 +72,7 @@ func TestResizeVerticalImage(t *testing.T) { t.Fatalf("Invalid width: %d", size.Width) } - Write("fixtures/test_vertical_"+strconv.Itoa(test.options.Width)+"x"+strconv.Itoa(test.options.Height)+".jpg", image) + Write("fixtures/test_vertical_"+strconv.Itoa(test.options.Width)+"x"+strconv.Itoa(test.options.Height)+"_out.jpg", image) } } @@ -264,7 +264,7 @@ func TestGaussianBlur(t *testing.T) { t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } - Write("fixtures/test_gaussian.jpg", newImg) + Write("fixtures/test_gaussian_out.jpg", newImg) } func TestSharpen(t *testing.T) { @@ -281,7 +281,7 @@ func TestSharpen(t *testing.T) { t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } - Write("fixtures/test_sharpen.jpg", newImg) + Write("fixtures/test_sharpen_out.jpg", newImg) } func TestExtractWithDefaultAxis(t *testing.T) { @@ -298,7 +298,7 @@ func TestExtractWithDefaultAxis(t *testing.T) { t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } - Write("fixtures/test_extract_defaults.jpg", newImg) + Write("fixtures/test_extract_defaults_out.jpg", newImg) } func TestExtractCustomAxis(t *testing.T) { @@ -315,7 +315,7 @@ func TestExtractCustomAxis(t *testing.T) { t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } - Write("fixtures/test_extract_custom_axis.jpg", newImg) + Write("fixtures/test_extract_custom_axis_out.jpg", newImg) } func TestConvert(t *testing.T) { @@ -557,3 +557,42 @@ func BenchmarkWatermarWebp(b *testing.B) { } runBenchmarkResize("test.webp", options, b) } + +func BenchmarkWatermarkImageJpeg(b *testing.B) { + watermark := readFile("transparent.png") + options := Options{ + WatermarkImage: WatermarkImage{ + Buf: watermark, + Opacity: 0.25, + Left: 100, + Top: 100, + }, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkWatermarImagePng(b *testing.B) { + watermark := readFile("transparent.png") + options := Options{ + WatermarkImage: WatermarkImage{ + Buf: watermark, + Opacity: 0.25, + Left: 100, + Top: 100, + }, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkWatermarImageWebp(b *testing.B) { + watermark := readFile("transparent.png") + options := Options{ + WatermarkImage: WatermarkImage{ + Buf: watermark, + Opacity: 0.25, + Left: 100, + Top: 100, + }, + } + runBenchmarkResize("test.webp", options, b) +} diff --git a/vips.go b/vips.go index 1544b4a..d826bbf 100644 --- a/vips.go +++ b/vips.go @@ -67,6 +67,12 @@ type vipsWatermarkOptions struct { Background [3]C.double } +type vipsWatermarkImageOptions struct { + Left C.int + Top C.int + Opacity C.float +} + type vipsWatermarkTextOptions struct { Text *C.char Font *C.char @@ -589,3 +595,25 @@ func vipsSharpen(image *C.VipsImage, o Sharpen) (*C.VipsImage, error) { func max(x int) int { return int(math.Max(float64(x), 0)) } + +func vipsDrawWatermark(image *C.VipsImage, watermark *C.VipsImage, o WatermarkImage) (*C.VipsImage, error) { + var out *C.VipsImage + + if !vipsHasAlpha(image) { + C.vips_add_band(image, &image, C.double(255.0)) + } + + if !vipsHasAlpha(watermark) { + C.vips_add_band(watermark, &watermark, C.double(255.0)) + } + + opts := vipsWatermarkImageOptions{C.int(o.Left), C.int(o.Top), C.float(o.Opacity)} + + err := C.vips_watermark_image(image, watermark, &out, (*C.WatermarkImageOptions)(unsafe.Pointer(&opts))) + + if err != 0 { + return nil, catchVipsError() + } + + return out, nil +} diff --git a/vips.h b/vips.h index e9129b5..febb516 100644 --- a/vips.h +++ b/vips.h @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -45,6 +46,12 @@ typedef struct { double Background[3]; } WatermarkOptions; +typedef struct { + int Left; + int Top; + float Opacity; +} WatermarkImageOptions; + static unsigned long has_profile_embed(VipsImage *image) { return vips_image_get_typeof(image, VIPS_META_ICC_NAME); @@ -448,3 +455,120 @@ vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, doubl return vips_sharpen(in, out, "radius", radius, "x1", x1, "y2", y2, "y3", y3, "m1", m1, "m2", m2, NULL); #endif } + +int +vips_add_band(VipsImage *in, VipsImage **out, double c) { + return vips_bandjoin_const1(in, out, c, NULL); +} + +int +vips_watermark_image(VipsImage *in, VipsImage *sub, VipsImage **out, WatermarkImageOptions *o) { + int bands = in->Bands; + double background[4] = {0.0, 0.0, 0.0, 0.0}; + + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 4); + + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); + + t[0] = in; + t[1] = sub; + + if (vips_embed(t[1], &t[2], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_BACKGROUND, "background", vipsBackground, NULL)) { + g_object_unref(base); + return 1; + } + + //Get Sub bands without alpha + if (vips_extract_band(t[2], &t[3], 0, "n", t[2]->Bands - 1, NULL)) { + g_object_unref(base); + return 1; + } + + //Get Sub Image alpha + if ( + vips_extract_band(t[2], &t[4], t[2]->Bands - 1, "n", 1, NULL) || + vips_linear1(t[4], &t[4], o->Opacity / 255.0, 0.0, NULL) + ) { + g_object_unref(base); + return 1; + } + + //Apply alpha to other sub bands to remove unwanted pixels + if (vips_multiply(t[3], t[4], &t[3], NULL)) { + g_object_unref(base); + return 1; + } + + //Get in bands without alpha + if (vips_extract_band(t[0], &t[5], 0, "n", t[0]->Bands - 1, NULL)) { + g_object_unref(base); + return 1; + } + + //Get in alpha + if ( + vips_extract_band(t[0], &t[6], t[0]->Bands - 1, "n", 1, NULL) || + vips_linear1(t[6], &t[6], 1.0 / 255.0, 0.0, NULL) + ) { + g_object_unref(base); + return 1; + } + + + // Compute normalized output alpha channel: + // + // References: + // - http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending + // - https://github.com/jcupitt/ruby-vips/issues/28#issuecomment-9014826 + // + // out_a = src_a + dst_a * (1 - src_a) + // ^^^^^^^^^^^ + + if ( + vips_linear1(t[4], &t[7], -1.0, 1.0, NULL) || + vips_multiply(t[6], t[7], &t[8], NULL) + ) { + g_object_unref(base); + return 1; + } + + //outAlphaNormalized in t[8] + if (vips_add(t[4], t[8], &t[8], NULL)) { + g_object_unref(base); + return 1; + } + + // Compute output RGB channels: + // + // Wikipedia: + // out_rgb = (src_rgb * src_a + dst_rgb * dst_a * (1 - src_a)) / out_a + // ^^^^^^^^^^^ + // t0 + // + // Omit division by `out_a` since `Compose` is supposed to output a + // premultiplied RGBA image as reversal of premultiplication is handled + // externally. + + if (vips_multiply(t[5], t[7], &t[9], NULL)) { + g_object_unref(base); + return 1; + } + + //outRGBPremultiplied in t[9] + if (vips_add(t[3], t[9], &t[9], NULL)) { + g_object_unref(base); + return 1; + } + + if ( + vips_linear1(t[8], &t[8], 255.0, 0.0, NULL) || + vips_bandjoin2(t[9], t[8], out, NULL) + ) { + g_object_unref(base); + return 1; + } + + g_object_unref(base); + return 0; +} diff --git a/vips_test.go b/vips_test.go index 7001c32..4faf46c 100644 --- a/vips_test.go +++ b/vips_test.go @@ -114,9 +114,26 @@ func TestVipsWatermark(t *testing.T) { t.Errorf("Cannot add watermark: %s", err) } - buf, err := vipsSave(newImg, vipsSaveOptions{Quality: 95}) - if len(buf) == 0 || err != nil { - t.Fatalf("Empty image. %#v", err) + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsWatermarkWithImage(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + watermark, _, _ := vipsRead(readImage("transparent.png")) + + options := WatermarkImage{Left: 100, Top: 100, Opacity: 1.0} + newImg, err := vipsDrawWatermark(image, watermark, options) + if err != nil { + t.Errorf("Cannot add watermark: %s", err) + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") } }