mirror of
https://github.com/talgo-cloud/bimg.git
synced 2026-03-07 21:48:13 -08:00
Merge pull request #132 from jaume-pinyol/WATERMARK_SUPPORT
Add support for image watermarks
This commit is contained in:
commit
c2c5da7383
9 changed files with 325 additions and 26 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ bin
|
|||
/*.png
|
||||
/*.webp
|
||||
/fixtures/*_out.*
|
||||
/.idea/
|
||||
|
|
|
|||
6
image.go
6
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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
76
resize.go
76
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
28
vips.go
28
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
|
||||
}
|
||||
|
|
|
|||
124
vips.h
124
vips.h
|
|
@ -1,4 +1,5 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <vips/vips.h>
|
||||
#include <vips/foreign.h>
|
||||
#include <vips/vips7compat.h>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
23
vips_test.go
23
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue