diff --git a/config.go b/config.go index 3118c49f..7a9c9d6c 100644 --- a/config.go +++ b/config.go @@ -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() } diff --git a/process.go b/process.go index 6549594f..aa4f4f0e 100644 --- a/process.go +++ b/process.go @@ -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())) } diff --git a/processing_options.go b/processing_options.go index c51e6928..ab69205e 100644 --- a/processing_options.go +++ b/processing_options.go @@ -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)), } diff --git a/vips.h b/vips.h index 40c776e2..031ba907 100644 --- a/vips.h +++ b/vips.h @@ -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); diff --git a/watermark_data.go b/watermark_data.go new file mode 100644 index 00000000..95e9b936 --- /dev/null +++ b/watermark_data.go @@ -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 +}