From cc2290cb4548ea35143c0bc4ff479029a54b5464 Mon Sep 17 00:00:00 2001 From: Tomas Aparicio Date: Sat, 11 Apr 2015 02:49:11 +0200 Subject: [PATCH] feat(#27, #25): new features --- README.md | 44 ++++++++++++++++--- image.go | 15 +------ image_test.go | 9 +++- options.go | 21 ++++++--- resize.go | 50 ++++++++++----------- resize_test.go | 38 ++++++++++++++++ vips.go | 115 ++++++++++++++++++++++++++++++++++++------------- vips.h | 97 ++++++++++++++++++++++++++++++++++++++--- 8 files changed, 303 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 9e1071f..f2eab8e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # bimg [![Build Status](https://travis-ci.org/h2non/bimg.png)](https://travis-ci.org/h2non/bimg) [![GitHub release](https://img.shields.io/github/tag/h2non/bimg.svg)](https://github.com/h2non/bimg/releases) [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.png)](https://godoc.org/github.com/h2non/bimg) [![Coverage Status](https://coveralls.io/repos/h2non/bimg/badge.svg?branch=master)](https://coveralls.io/r/h2non/bimg?branch=master) -Small [Go](http://golang.org) library for blazing fast and efficient image processing based on [libvips](https://github.com/jcupitt/libvips) using C bindings. It provides a clean, simple and fluent [API](#examples) in pure Go. +Small [Go](http://golang.org) library for blazing fast and efficient image processing based on [libvips](https://github.com/jcupitt/libvips) using C bindings. It provides a clean, simple and fluent [API](https://godoc.org/github.com/h2non/bimg) in pure Go. bimg is designed to be a small and efficient library with a generic and useful set of features. It uses internally libvips, which requires a [low memory footprint](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use) and it's typically 4x faster than using the quickest ImageMagick and GraphicsMagick settings or Go native `image` package, and in some cases it's even 8x faster processing JPEG images. -It can read JPEG, PNG, WEBP and TIFF formats and output to JPEG, PNG and WEBP. It supports common [image transformation](#supported-image-operations) operations such as crop, resize, rotate... and conversion between multiple formats. +It can read JPEG, PNG, WEBP and TIFF formats and output to JPEG, PNG and WEBP. It supports common [image transformation](#supported-image-operations) operations such as crop, resize, rotate, zoom, watermark... and conversion between multiple formats. For getting started, take a look to the [examples](#examples) and [programmatic API](https://godoc.org/github.com/h2non/bimg) documentation. @@ -41,13 +41,14 @@ The [install script](https://github.com/lovell/sharp/blob/master/preinstall.sh) - Resize - Enlarge - Crop -- Rotate (and auto-rotate based on EXIF orientation) -- Flip (and auto-flip based on EXIF metadata) +- Rotate (with auto-rotate based on EXIF orientation) +- Flip (with auto-flip based on EXIF metadata) - Flop - Zoom - Thumbnail - Extract area -- Format conversion +- Watermark (fully customizable text-based) +- Format conversion (with additional quality/compression settings) - EXIF metadata (size, alpha channel, profile, orientation...) ## Performance @@ -165,6 +166,34 @@ if err != nil { bimg.Write("new.jpg", newImage) ``` +#### Watermark + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +options := bimg.Watermark{ + Watermark{ + Text: "Chuck Norris - Copyright (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: bimg.Color{255, 255, 255}, + } +} + +newImage, err := bimg.NewImage(buffer).Watermark() +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +bimg.Write("new.jpg", newImage) +``` + #### Fluent interface ```go @@ -491,7 +520,6 @@ Determines the image type format (jpeg, png, webp or tiff) type Interpolator int ``` - ```go const ( BICUBIC Interpolator = iota @@ -531,6 +559,10 @@ type Options struct { } ``` +## Special Thanks + +- [John Cupitt](https://github.com/jcupitt) + ## License MIT - Tomas Aparicio diff --git a/image.go b/image.go index ea276f5..f32606e 100644 --- a/image.go +++ b/image.go @@ -75,20 +75,9 @@ func (i *Image) Thumbnail(pixels int) ([]byte, error) { return i.Process(options) } -// Insert an image. Alias to Watermark() -func (i *Image) Insert(image []byte, left, top int) ([]byte, error) { - return i.Watermark(image, left, top) -} - // Insert an image to the existent one as watermark -func (i *Image) Watermark(image []byte, left, top int) ([]byte, error) { - options := Options{ - Insert: Insert{ - Buffer: image, - Top: top, - Left: left, - }, - } +func (i *Image) Watermark(w Watermark) ([]byte, error) { + options := Options{Watermark: w} return i.Process(options) } diff --git a/image_test.go b/image_test.go index 7564780..ab0bb08 100644 --- a/image_test.go +++ b/image_test.go @@ -111,8 +111,13 @@ func TestImageWatermark(t *testing.T) { t.Errorf("Cannot process the image: %#v", err) } - insert, _ := Read("fixtures/watermark.png") - buf, err := image.Watermark(insert, 10, 10) + buf, err := image.Watermark(Watermark{ + Text: "Copy me if you can", + Opacity: 0.5, + Width: 200, + DPI: 100, + Background: Color{255, 255, 255}, + }) if err != nil { t.Error(err) } diff --git a/options.go b/options.go index d530e51..598faf0 100644 --- a/options.go +++ b/options.go @@ -55,10 +55,20 @@ const ( VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL ) -type Insert struct { - Top int - Left int - Buffer []byte +// Color represents a traditional RGB color scheme +type Color struct { + R, G, B uint8 +} + +type Watermark struct { + Width int + DPI int + Margin int + Opacity float32 + NoReplicate bool + Text string + Font string + Background Color } type Options struct { @@ -78,9 +88,10 @@ type Options struct { Flip bool Flop bool NoAutoRotate bool + Colorspace bool Rotate Angle - Insert Insert Gravity Gravity + Watermark Watermark Type ImageType Interpolator Interpolator } diff --git a/resize.go b/resize.go index d0970e8..1bfe48d 100644 --- a/resize.go +++ b/resize.go @@ -124,8 +124,8 @@ func Resize(buf []byte, o Options) ([]byte, error) { Compression: o.Compression, } - // Insert an image if necessary - image, err = insertImage(image, imageType, o.Insert, saveOptions) + // watermark + image, err = watermakImage(image, o.Watermark) if err != nil { return nil, err } @@ -198,42 +198,44 @@ func rotateImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, e return image, err } -// WIP -func insertImage(image *C.struct__VipsImage, t ImageType, o Insert, save vipsSaveOptions) (*C.struct__VipsImage, error) { - if len(o.Buffer) == 0 { +func watermakImage(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImage, error) { + if len(w.Text) == 0 { return image, nil } - insert, imageType, err := vipsRead(o.Buffer) - if err != nil { - return nil, err + // Defaults + if len(w.Font) == 0 { + w.Font = "sans 10" } - - if imageType != t { - save.Type = t - buf, err := vipsSave(insert, save) - if err != nil { - return nil, err - } - - insert, imageType, err = vipsRead(buf) - if err != nil { - return nil, err - } + if w.Width == 0 { + w.Width = int(math.Floor(float64(image.Xsize / 8))) + } + if w.DPI == 0 { + w.DPI = 150 + } + if w.Margin == 0 { + w.Margin = w.Width + } + if w.Opacity == 0 { + w.Opacity = 0.25 + } else if w.Opacity > 1 { + w.Opacity = 1 } - debug("Insert image: %#v", insert) + image, err := vipsWatermark(image, w) + if err != nil { + return nil, err + } - return vipsInsert(image, insert, o.Left, o.Top) + return image, nil } func zoomImage(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error) { if zoom == 0 { return image, nil } - zoom += 1 - return vipsZoom(image, zoom) + return vipsZoom(image, zoom+1) } func shrinkImage(image *C.struct__VipsImage, o Options, residual float64, shrink int) (*C.struct__VipsImage, float64, error) { diff --git a/resize_test.go b/resize_test.go index 86e72eb..97b91bf 100644 --- a/resize_test.go +++ b/resize_test.go @@ -45,6 +45,44 @@ func TestRotate(t *testing.T) { } } +func testColorspace(t *testing.T) { + options := Options{Colorspace: true} + buf, _ := Read("fixtures/sky.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != JPEG { + t.Fatal("Image is not jpeg") + } + + err = Write("fixtures/test_color_out.jpg", newImg) + if err != nil { + t.Fatal("Cannot save the image") + } +} + +func TestCorruptedImage(t *testing.T) { + options := Options{Width: 800, Height: 600} + buf, _ := Read("fixtures/corrupt.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != JPEG { + t.Fatal("Image is not jpeg") + } + + err = Write("fixtures/test_corrupt_out.jpg", newImg) + if err != nil { + t.Fatal("Cannot save the image") + } +} + func TestInvalidRotate(t *testing.T) { options := Options{Width: 800, Height: 600, Rotate: 111} buf, _ := Read("fixtures/test.jpg") diff --git a/vips.go b/vips.go index 32c8fc6..27ff941 100644 --- a/vips.go +++ b/vips.go @@ -3,6 +3,7 @@ package bimg /* #cgo pkg-config: vips #include "vips.h" +#include "stdlib.h" */ import "C" @@ -19,16 +20,30 @@ var ( initialized bool = false ) +type VipsMemoryInfo struct { + Memory int64 + MemoryHighwater int64 + Allocations int64 +} + type vipsSaveOptions struct { Quality int Compression int Type ImageType } -type VipsMemoryInfo struct { - Memory int64 - MemoryHighwater int64 - Allocations int64 +type vipsWatermarkOptions struct { + Width C.int + DPI C.int + Margin C.int + NoReplicate C.int + Opacity C.float + Background [3]C.double +} + +type vipsWatermarkTextOptions struct { + Text *C.char + Font *C.char } func init() { @@ -71,12 +86,12 @@ func Shutdown() { } } -// Output to stdout collected data for debugging purposes +// Output to stdout vips collected data. Useful for debugging func VipsDebug() { C.im__print_all() } -// Get memory info stats from vips +// Get memory info stats from vips (cache size, memory allocs...) func VipsMemory() VipsMemoryInfo { return VipsMemoryInfo{ Memory: int64(C.vips_tracked_get_mem()), @@ -85,6 +100,26 @@ func VipsMemory() VipsMemoryInfo { } } +func vipsExifOrientation(image *C.struct__VipsImage) int { + return int(C.vips_exif_orientation(image)) +} + +func vipsHasAlpha(image *C.struct__VipsImage) bool { + return int(C.has_alpha_channel(image)) > 0 +} + +func vipsHasProfile(image *C.struct__VipsImage) bool { + return int(C.has_profile_embed(image)) > 0 +} + +func vipsWindowSize(name string) float64 { + return float64(C.interpolator_window_size(C.CString(name))) +} + +func vipsSpace(image *C.struct__VipsImage) string { + return C.GoString(C.vips_enum_nick_bridge(image)) +} + func vipsRotate(image *C.struct__VipsImage, angle Angle) (*C.struct__VipsImage, error) { var out *C.struct__VipsImage defer C.g_object_unref(C.gpointer(image)) @@ -121,13 +156,54 @@ func vipsZoom(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error return out, nil } -func vipsInsert(image *C.struct__VipsImage, sub *C.struct__VipsImage, left, top int) (*C.struct__VipsImage, error) { +func vipsColorSpace(image *C.struct__VipsImage) (*C.struct__VipsImage, error) { var out *C.struct__VipsImage + var temp *C.struct__VipsImage + var max *C.double + var x *C.int + var y *C.int defer C.g_object_unref(C.gpointer(image)) - defer C.g_object_unref(C.gpointer(sub)) - err := C.vips_insert_bridge(image, sub, &out, C.int(left), C.int(top)) + err := C.vips_colorspace_bridge(image, &out) + if err != 0 { + return nil, catchVipsError() + } + + err = C.vips_hist_find_ndim_bridge(out, &temp) + if err != 0 { + return nil, catchVipsError() + } + + err = C.vips_max_bridge(temp, max, &x, &y) + if err != 0 { + return nil, catchVipsError() + } + debug("MAX VALUE %dx%d", x, y) + + return temp, nil +} + +func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImage, error) { + var out *C.struct__VipsImage + + // Defaults + noReplicate := 0 + if w.NoReplicate { + noReplicate = 1 + } + + text := C.CString(w.Text) + font := C.CString(w.Font) + background := [3]C.double{C.double(w.Background.R), C.double(w.Background.G), C.double(w.Background.B)} + + textOpts := vipsWatermarkTextOptions{text, font} + opts := vipsWatermarkOptions{C.int(w.Width), C.int(w.DPI), C.int(w.Margin), C.int(noReplicate), C.float(w.Opacity), background} + + defer C.free(unsafe.Pointer(text)) + defer C.free(unsafe.Pointer(font)) + + err := C.vips_watermark(image, &out, (*C.watermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.watermarkOptions)(unsafe.Pointer(&opts))) if err != 0 { return nil, catchVipsError() } @@ -247,7 +323,6 @@ func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (* defer C.g_object_unref(C.gpointer(input)) defer C.g_object_unref(C.gpointer(interpolator)) - // Perform affine transformation err := C.vips_affine_interpolator(input, &image, C.double(residual), 0, 0, C.double(residual), interpolator) if err != 0 { return nil, catchVipsError() @@ -288,26 +363,6 @@ func vipsImageType(buf []byte) ImageType { return imageType } -func vipsExifOrientation(image *C.struct__VipsImage) int { - return int(C.vips_exif_orientation(image)) -} - -func vipsHasAlpha(image *C.struct__VipsImage) bool { - return int(C.has_alpha_channel(image)) > 0 -} - -func vipsHasProfile(image *C.struct__VipsImage) bool { - return int(C.has_profile_embed(image)) > 0 -} - -func vipsWindowSize(name string) float64 { - return float64(C.interpolator_window_size(C.CString(name))) -} - -func vipsSpace(image *C.struct__VipsImage) string { - return C.GoString(C.vips_enum_nick_bridge(image)) -} - func catchVipsError() error { s := C.GoString(C.vips_error_buffer()) C.vips_error_clear() diff --git a/vips.h b/vips.h index f2c76a5..0fa7cf9 100644 --- a/vips.h +++ b/vips.h @@ -11,6 +11,20 @@ enum types { MAGICK }; +typedef struct { + char *Text; + char *Font; +} watermarkTextOptions; + +typedef struct { + int Width; + int DPI; + int Margin; + int NoReplicate; + float Opacity; + double Background[3]; +} watermarkOptions; + int vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) { @@ -98,21 +112,28 @@ vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) }; int -vips_insert_bridge(VipsImage *in, VipsImage *sub, VipsImage **out, int left, int top) +vips_colorspace_bridge(VipsImage *in, VipsImage **out) { - return vips_insert(in, sub, out, left, top, NULL); + return vips_colourspace(in, out, VIPS_INTERPRETATION_LAB, NULL); }; int -vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend) +vips_hist_find_ndim_bridge(VipsImage *in, VipsImage **out) { - return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); + return vips_hist_find_ndim(in, out, "bins", 5, NULL); +}; + +int +vips_max_bridge(VipsImage *in, double *out, int **x, int **y) +{ + double ones[3] = { 1, 1, 1 }; + return vips_max(in, ones, "x", x, "y", y, NULL); }; int -vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) +vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend) { - return vips_colourspace(in, out, space, NULL); + return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); }; int @@ -165,3 +186,67 @@ vips_init_image(void *buf, size_t len, int imageType, VipsImage **out) { return code; }; + +int +vips_watermark(VipsImage *in, VipsImage **out, watermarkTextOptions *to, watermarkOptions *o) +{ + double ones[3] = { 1, 1, 1 }; + + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 12); + t[0] = in; + + // Make the mask. + if ( + vips_text(&t[1], to->Text, + "width", o->Width, + "dpi", o->DPI, + "font", to->Font, + NULL) || + vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) || + vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) || + vips_embed(t[3], &t[4], 100, 100, + t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL) + ) { + g_object_unref(base); + return (1); + } + + // Replicate if necessary + if (o->NoReplicate != 1 && ( + vips_replicate(t[4], &t[5], + 1 + t[0]->Xsize / t[4]->Xsize, + 1 + t[0]->Ysize / t[4]->Ysize, NULL) || + vips_crop(t[5], &t[6], 0, 0, + t[0]->Xsize, t[0]->Ysize, NULL) + )) { + g_object_unref(base); + return (1); + } + + // Make the constant image to paint the text with. + if ( + vips_black(&t[7], 1, 1, NULL) || + vips_linear( t[7], &t[8], ones, o->Background, 3, NULL) || + vips_cast(t[8], &t[9], VIPS_FORMAT_UCHAR, NULL) || + vips_copy(t[9], &t[10], + "interpretation", t[0]->Type, + NULL) || + vips_embed(t[10], &t[11], 0, 0, + t[0]->Xsize, t[0]->Ysize, + "extend", VIPS_EXTEND_COPY, + NULL) + ) { + g_object_unref(base); + return (1); + } + + // Blend the mask and text and write to output. + if (vips_ifthenelse(t[6], t[11], t[0], out, "blend", TRUE, NULL)) { + g_object_unref(base); + return (1); + } + + g_object_unref(base); + return (0); +};