1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2024-11-24 08:12:38 +02:00

Watermark

This commit is contained in:
DarthSim 2018-10-19 15:47:11 +06:00
parent f5f6b24d4b
commit 3d98a943a4
5 changed files with 452 additions and 42 deletions

View File

@ -20,6 +20,12 @@ func intEnvConfig(i *int, name string) {
}
}
func floatEnvConfig(i *float64, name string) {
if env, err := strconv.ParseFloat(os.Getenv(name), 64); err == nil {
*i = env
}
}
func megaIntEnvConfig(f *int, name string) {
if env, err := strconv.ParseFloat(os.Getenv(name), 64); err == nil {
*f = int(env * 1000000)
@ -144,6 +150,11 @@ type config struct {
BaseURL string
Presets presets
WatermarkData string
WatermarkPath string
WatermarkURL string
WatermarkOpacity float64
}
var conf = config{
@ -161,6 +172,7 @@ var conf = config{
GZipCompression: 5,
ETagEnabled: false,
S3Enabled: false,
WatermarkOpacity: 1,
}
func init() {
@ -223,6 +235,11 @@ func init() {
presetEnvConfig(conf.Presets, "IMGPROXY_PRESETS")
presetFileConfig(conf.Presets, *presetsPath)
strEnvConfig(&conf.WatermarkData, "IMGPROXY_WATERMARK_DATA")
strEnvConfig(&conf.WatermarkPath, "IMGPROXY_WATERMARK_PATH")
strEnvConfig(&conf.WatermarkURL, "IMGPROXY_WATERMARK_URL")
floatEnvConfig(&conf.WatermarkOpacity, "IMGPROXY_WATERMARK_OPACITY")
if len(conf.Key) == 0 {
warning("Key is not defined, so signature checking is disabled")
conf.AllowInsecure = true
@ -300,6 +317,12 @@ func init() {
checkPresets(conf.Presets)
initVips()
if conf.WatermarkOpacity <= 0 {
log.Fatalln("Watermark opacity should be greater than 0")
} else if conf.WatermarkOpacity > 1 {
log.Fatalln("Watermark opacity should be less than or equal to 1")
}
initDownloading()
initVips()
}

View File

@ -22,13 +22,16 @@ var (
vipsTypeSupportLoad = make(map[imageType]bool)
vipsTypeSupportSave = make(map[imageType]bool)
watermark *C.struct__VipsImage
errSmartCropNotSupported = errors.New("Smart crop is not supported by used version of libvips")
)
type cConfig struct {
Quality C.int
JpegProgressive C.int
PngInterlaced C.int
Quality C.int
JpegProgressive C.int
PngInterlaced C.int
WatermarkOpacity C.double
}
var cConf cConfig
@ -89,9 +92,16 @@ func initVips() {
if conf.PngInterlaced {
cConf.PngInterlaced = C.int(1)
}
cConf.WatermarkOpacity = C.double(conf.WatermarkOpacity)
if err := vipsPrepareWatermark(); err != nil {
log.Fatal(err)
}
}
func shutdownVips() {
C.clear_image(&watermark)
C.vips_shutdown()
}
@ -174,31 +184,33 @@ func calcShink(scale float64, imgtype imageType) int {
}
func calcCrop(width, height int, po *processingOptions) (left, top int) {
left = (width - po.Width + 1) / 2
top = (height - po.Height + 1) / 2
if po.Gravity.Type == gravityNorth {
top = 0
}
if po.Gravity.Type == gravityEast {
left = width - po.Width
}
if po.Gravity.Type == gravitySouth {
top = height - po.Height
}
if po.Gravity.Type == gravityWest {
left = 0
}
if po.Gravity.Type == gravityFocusPoint {
pointX := int(float64(width) * po.Gravity.X)
pointY := int(float64(height) * po.Gravity.Y)
left = maxInt(0, minInt(pointX-po.Width/2, width-po.Width))
top = maxInt(0, minInt(pointY-po.Height/2, height-po.Height))
return
}
left = (width - po.Width + 1) / 2
top = (height - po.Height + 1) / 2
if po.Gravity.Type == gravityNorth || po.Gravity.Type == gravityNorthEast || po.Gravity.Type == gravityNorthWest {
top = 0
}
if po.Gravity.Type == gravityEast || po.Gravity.Type == gravityNorthEast || po.Gravity.Type == gravitySouthEast {
left = width - po.Width
}
if po.Gravity.Type == gravitySouth || po.Gravity.Type == gravitySouthEast || po.Gravity.Type == gravitySouthWest {
top = height - po.Height
}
if po.Gravity.Type == gravityWest || po.Gravity.Type == gravityNorthWest || po.Gravity.Type == gravitySouthWest {
left = 0
}
return
@ -354,9 +366,73 @@ func processImage(ctx context.Context) ([]byte, error) {
checkTimeout(ctx)
if po.Watermark.Enabled {
if err = vipsApplyWatermark(&img, &po.Watermark); err != nil {
return nil, err
}
}
return vipsSaveImage(img, po.Format)
}
func vipsPrepareWatermark() error {
data, imgtype, cancel, err := watermarkData()
defer cancel()
if err != nil {
return err
}
if data == nil {
return nil
}
if C.vips_load_buffer(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(imgtype), 1, &watermark) != 0 {
return vipsError()
}
var tmp *C.struct__VipsImage
if cConf.WatermarkOpacity < 1 {
if vipsImageHasAlpha(watermark) {
var alpha *C.struct__VipsImage
defer C.clear_image(&alpha)
if C.vips_extract_band_go(watermark, &tmp, (*watermark).Bands-1, 1) != 0 {
return vipsError()
}
C.swap_and_clear(&alpha, tmp)
if C.vips_extract_band_go(watermark, &tmp, 0, (*watermark).Bands-1) != 0 {
return vipsError()
}
C.swap_and_clear(&watermark, tmp)
if C.vips_linear_go(alpha, &tmp, cConf.WatermarkOpacity, 0) != 0 {
return vipsError()
}
C.swap_and_clear(&alpha, tmp)
if C.vips_bandjoin_go(watermark, alpha, &tmp) != 0 {
return vipsError()
}
C.swap_and_clear(&watermark, tmp)
} else {
if C.vips_bandjoin_const_go(watermark, &tmp, cConf.WatermarkOpacity*255) != 0 {
return vipsError()
}
C.swap_and_clear(&watermark, tmp)
}
}
if tmp = C.vips_image_copy_memory(watermark); tmp == nil {
return vipsError()
}
C.swap_and_clear(&watermark, tmp)
return nil
}
func vipsLoadImage(data []byte, imgtype imageType, shrink int) (*C.struct__VipsImage, error) {
var img *C.struct__VipsImage
if C.vips_load_buffer(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(imgtype), C.int(shrink), &img) != 0 {
@ -549,6 +625,145 @@ func vipsImageCopyMemory(img **C.struct__VipsImage) error {
return nil
}
func vipsReplicateWatermark(width, height C.int) (wm *C.struct__VipsImage, err error) {
var tmp *C.struct__VipsImage
defer C.clear_image(&tmp)
if C.vips_replicate_go(watermark, &tmp, 1+width/watermark.Xsize, 1+height/watermark.Ysize) != 0 {
err = vipsError()
return
}
if C.vips_extract_area_go(tmp, &wm, 0, 0, width, height) != 0 {
err = vipsError()
return
}
return
}
func vipsEmbedWatermark(gravity gravityType, width, height C.int, offX, offY C.int) (wm *C.struct__VipsImage, err error) {
wmWidth := watermark.Xsize
wmHeight := watermark.Ysize
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 C.vips_embed_go(watermark, &wm, left, top, width, height) != 0 {
err = vipsError()
return
}
return
}
func vipsApplyWatermark(img **C.struct__VipsImage, opts *watermarkOptions) error {
if watermark == nil {
return nil
}
var wm, wmAlpha, tmp *C.struct__VipsImage
var err error
defer C.clear_image(&wm)
defer C.clear_image(&wmAlpha)
imgW := (*img).Xsize
imgH := (*img).Ysize
if opts.Replicate {
if wm, err = vipsReplicateWatermark(imgW, imgH); err != nil {
return err
}
} else {
if wm, err = vipsEmbedWatermark(opts.Gravity, imgW, imgH, C.int(opts.OffsetX), C.int(opts.OffsetY)); err != nil {
return err
}
}
if C.vips_extract_band_go(wm, &tmp, (*wm).Bands-1, 1) != 0 {
return vipsError()
}
C.swap_and_clear(&wmAlpha, tmp)
if C.vips_extract_band_go(wm, &tmp, 0, (*wm).Bands-1) != 0 {
return vipsError()
}
C.swap_and_clear(&wm, tmp)
if C.vips_image_guess_interpretation(*img) != C.vips_image_guess_interpretation(wm) {
if C.vips_colourspace_go(wm, &tmp, C.vips_image_guess_interpretation(*img)) != 0 {
return vipsError()
}
C.swap_and_clear(&wm, tmp)
}
if opts.Opacity < 1 {
if C.vips_linear_go(wmAlpha, &tmp, C.double(opts.Opacity), 0) != 0 {
return vipsError()
}
C.swap_and_clear(&wmAlpha, tmp)
}
var imgAlpha *C.struct__VipsImage
defer C.clear_image(&imgAlpha)
hasAlpha := vipsImageHasAlpha(*img)
if hasAlpha {
if C.vips_extract_band_go(*img, &imgAlpha, (**img).Bands-1, 1) != 0 {
return vipsError()
}
if C.vips_extract_band_go(*img, &tmp, 0, (**img).Bands-1) != 0 {
return vipsError()
}
C.swap_and_clear(img, tmp)
}
if C.vips_ifthenelse_go(wmAlpha, wm, *img, &tmp) != 0 {
return vipsError()
}
C.swap_and_clear(img, tmp)
if hasAlpha {
if C.vips_bandjoin_go(*img, imgAlpha, &tmp) != 0 {
return vipsError()
}
C.swap_and_clear(img, tmp)
}
return nil
}
func vipsError() error {
return errors.New(C.GoString(C.vips_error_buffer()))
}

View File

@ -47,21 +47,29 @@ const (
gravityEast
gravitySouth
gravityWest
gravityNorthWest
gravityNorthEast
gravitySouthWest
gravitySouthEast
gravitySmart
gravityFocusPoint
)
var gravityTypes = map[string]gravityType{
"ce": gravityCenter,
"no": gravityNorth,
"ea": gravityEast,
"so": gravitySouth,
"we": gravityWest,
"sm": gravitySmart,
"fp": gravityFocusPoint,
"ce": gravityCenter,
"no": gravityNorth,
"ea": gravityEast,
"so": gravitySouth,
"we": gravityWest,
"nowe": gravityNorthWest,
"noea": gravityNorthEast,
"sowe": gravitySouthWest,
"soea": gravitySouthEast,
"sm": gravitySmart,
"fp": gravityFocusPoint,
}
type gravity struct {
type gravityOptions struct {
Type gravityType
X, Y float64
}
@ -89,17 +97,29 @@ const (
hexColorShortFormat = "%1x%1x%1x"
)
type watermarkOptions struct {
Enabled bool
Opacity float64
Replicate bool
Gravity gravityType
OffsetX int
OffsetY int
}
type processingOptions struct {
Resize resizeType
Width int
Height int
Gravity gravity
Enlarge bool
Format imageType
Flatten bool
Background color
Blur float32
Sharpen float32
Resize resizeType
Width int
Height int
Gravity gravityOptions
Enlarge bool
Format imageType
Flatten bool
Background color
Blur float32
Sharpen float32
Watermark watermarkOptions
UsedPresets []string
}
@ -411,6 +431,43 @@ func applyPresetOption(po *processingOptions, args []string) error {
return nil
}
func applyWatermarkOption(po *processingOptions, args []string) error {
if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 {
po.Watermark.Enabled = o > 0
po.Watermark.Opacity = o
} else {
return fmt.Errorf("Invalid watermark opacity: %s", args[0])
}
if len(args) > 1 {
if args[1] == "re" {
po.Watermark.Replicate = true
} else if g, ok := gravityTypes[args[1]]; ok && g != gravityFocusPoint {
po.Watermark.Gravity = g
} else {
return fmt.Errorf("Invalid watermark position: %s", args[1])
}
}
if len(args) > 2 {
if x, err := strconv.Atoi(args[2]); err == nil {
po.Watermark.OffsetX = x
} else {
return fmt.Errorf("Invalid watermark X offset: %s", args[2])
}
}
if len(args) > 3 {
if y, err := strconv.Atoi(args[3]); err == nil {
po.Watermark.OffsetY = y
} else {
return fmt.Errorf("Invalid watermark Y offset: %s", args[3])
}
}
return nil
}
func applyFormatOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid format arguments: %v", args)
@ -480,6 +537,10 @@ func applyProcessingOption(po *processingOptions, name string, args []string) er
if err := applySharpenOption(po, args); err != nil {
return err
}
case "watermark", "wm":
if err := applyWatermarkOption(po, args); err != nil {
return err
}
case "preset", "pr":
if err := applyPresetOption(po, args); err != nil {
return err
@ -534,11 +595,12 @@ func defaultProcessingOptions(acceptHeader string) (*processingOptions, error) {
Resize: resizeFit,
Width: 0,
Height: 0,
Gravity: gravity{Type: gravityCenter},
Gravity: gravityOptions{Type: gravityCenter},
Enlarge: false,
Format: imageTypeJPEG,
Blur: 0,
Sharpen: 0,
Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityCenter},
UsedPresets: make([]string, 0, len(conf.Presets)),
}

35
vips.h
View File

@ -203,6 +203,41 @@ vips_extract_area_go(VipsImage *in, VipsImage **out, int left, int top, int widt
return vips_extract_area(in, out, left, top, width, height, NULL);
}
int
vips_replicate_go(VipsImage *in, VipsImage **out, int across, int down) {
return vips_replicate(in, out, across, down, NULL);
}
int
vips_embed_go(VipsImage *in, VipsImage **out, int x, int y, int width, int height) {
return vips_embed(in, out, x, y, width, height, NULL);
}
int
vips_extract_band_go(VipsImage *in, VipsImage **out, int band, int band_num) {
return vips_extract_band(in, out, band, "n", band_num, NULL);
}
int
vips_bandjoin_go (VipsImage *in1, VipsImage *in2, VipsImage **out) {
return vips_bandjoin2(in1, in2, out, NULL);
}
int
vips_bandjoin_const_go (VipsImage *in, VipsImage **out, double c) {
return vips_bandjoin_const1(in, out, c, NULL);
}
int
vips_linear_go (VipsImage *in, VipsImage **out, double a, double b) {
return vips_linear1(in, out, a, b, NULL);
}
int
vips_ifthenelse_go(VipsImage *cond, VipsImage *in1, VipsImage *in2, VipsImage **out) {
return vips_ifthenelse(cond, in1, in2, out, "blend", TRUE, NULL);
}
int
vips_jpegsave_go(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) {
return vips_jpegsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL);

75
watermark_data.go Normal file
View File

@ -0,0 +1,75 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
)
func watermarkData() ([]byte, imageType, context.CancelFunc, error) {
if len(conf.WatermarkData) > 0 {
data, imgtype, err := base64WatermarkData()
return data, imgtype, func() {}, err
}
if len(conf.WatermarkPath) > 0 {
data, imgtype, err := fileWatermarkData()
return data, imgtype, func() {}, err
}
if len(conf.WatermarkURL) > 0 {
return remoteWatermarkData()
}
return nil, imageTypeUnknown, func() {}, nil
}
func base64WatermarkData() ([]byte, imageType, error) {
data, err := base64.StdEncoding.DecodeString(conf.WatermarkData)
if err != nil {
return nil, imageTypeUnknown, fmt.Errorf("Can't decode watermark data: %s", err)
}
imgtype, err := checkTypeAndDimensions(bytes.NewReader(data))
if err != nil {
return nil, imageTypeUnknown, fmt.Errorf("Can't decode watermark: %s", err)
}
return data, imgtype, nil
}
func fileWatermarkData() ([]byte, imageType, error) {
f, err := os.Open(conf.WatermarkPath)
if err != nil {
return nil, imageTypeUnknown, fmt.Errorf("Can't read watermark: %s", err)
}
imgtype, err := checkTypeAndDimensions(f)
if err != nil {
return nil, imageTypeUnknown, fmt.Errorf("Can't decode watermark: %s", err)
}
// Return to the beginning of the file
f.Seek(0, 0)
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, imageTypeUnknown, fmt.Errorf("Can't read watermark: %s", err)
}
return data, imgtype, nil
}
func remoteWatermarkData() ([]byte, imageType, context.CancelFunc, error) {
ctx := context.WithValue(context.Background(), imageURLCtxKey, conf.WatermarkURL)
ctx, cancel, err := downloadImage(ctx)
if err != nil {
return nil, imageTypeUnknown, cancel, fmt.Errorf("Can't download watermark: %s", err)
}
return getImageData(ctx).Bytes(), getImageType(ctx), cancel, err
}