diff --git a/.travis.yml b/.travis.yml index 3c20085..aa82785 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,4 @@ go: - release - tip before_install: - - go get github.com/axw/gocov/gocov - - go get github.com/mattn/goveralls - - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi - curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash - -script: - - $HOME/gopath/bin/goveralls -service=travis-ci diff --git a/README.md b/README.md index f2eab8e..60d6c17 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,44 @@ -# 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) +# bimg [![Build Status](https://travis-ci.org/h2non/bimg.png)](https://travis-ci.org/h2non/bimg) [![GitHub release](http://img.shields.io/github/tag/h2non/bimg.svg?style=flat-square)](https://github.com/h2non/bimg/releases) [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.svg)](https://godoc.org/github.com/h2non/bimg) -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. +Small [Go](http://golang.org) package for fast high-level image processing using [libvips](https://github.com/jcupitt/libvips) via C bindings, providing a simple, elegant and fluent [programmatic API](#examples). -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. +bimg was designed to be a small and efficient library supporting a common set of [image operations](#supported-image-operations) such as crop, resize, rotate, zoom or watermark. It can read JPEG, PNG, WEBP and TIFF formats and output to JPEG, PNG and WEBP, including conversion between them. -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. +bimg uses internally libvips, a powerful library written in C for image processing 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. -For getting started, take a look to the [examples](#examples) and [programmatic API](https://godoc.org/github.com/h2non/bimg) documentation. +If you're looking for an HTTP based image processing solution, see [imaginary](https://github.com/h2non/imaginary). -bimg was heavily inspired in [sharp](https://github.com/lovell/sharp), -its homologous package built for node.js by [Lovell Fuller](https://github.com/lovell). +bimg was heavily inspired in [sharp](https://github.com/lovell/sharp), its homologous package built for [node.js](http://nodejs.org). -**Note**: bimg is still beta. Pull request and issues are highly appreciated +## Contents + +- [Supported image operations](#supported-image-operations) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Performance](#performance) +- [Benchmark](#benchmark) +- [Examples](#examples) +- [Debugging](#debugging) +- [API](#api) +- [Credits](#credits) + +## Supported image operations + +- Resize +- Enlarge +- Crop +- Rotate (with auto-rotate based on EXIF orientation) +- Flip (with auto-flip based on EXIF metadata) +- Flop +- Zoom +- Thumbnail +- Extract area +- Watermark (text only) +- Gaussian blur effect +- Custom output color space (RGB, grayscale...) +- Format conversion (with additional quality/compression settings) +- EXIF metadata (size, alpha channel, profile, orientation...) ## Prerequisites @@ -34,22 +59,14 @@ Run the following script as `sudo` (supports OSX, Debian/Ubuntu, Redhat, Fedora, curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash - ``` +If you wanna take the advantage of [OpenSlide](http://openslide.org/), simply add `--with-openslide` to enable it: +```bash +curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -s --with-openslide +``` + The [install script](https://github.com/lovell/sharp/blob/master/preinstall.sh) requires `curl` and `pkg-config` -## Supported image operations - -- Resize -- Enlarge -- Crop -- Rotate (with auto-rotate based on EXIF orientation) -- Flip (with auto-flip based on EXIF metadata) -- Flop -- Zoom -- Thumbnail -- Extract area -- Watermark (fully customizable text-based) -- Format conversion (with additional quality/compression settings) -- EXIF metadata (size, alpha channel, profile, orientation...) +For platform specific installations, see [Mac OS](https://github.com/lovell/sharp/blob/master/README.md#mac-os-tips) tips or [Windows](https://github.com/lovell/sharp/blob/master/README.md#windows) tips ## Performance @@ -59,23 +76,32 @@ Here you can see some performance test comparisons for multiple scenarios: - [libvips speed and memory usage](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use) - [sharp performance tests](https://github.com/lovell/sharp#the-task) -#### Benchmarks +## Benchmark -Tested using Go 1.4 and libvips-7.42.3 in OSX i7 2.7Ghz +Tested using Go 1.5.1 and libvips-7.42.3 in OSX i7 2.7Ghz ``` -PASS -BenchmarkResizeLargeJpeg 30 46652408 ns/op -BenchmarkResizePng 20 57387902 ns/op -BenchmarkResizeWebP 500 2453220 ns/op -BenchmarkConvertToJpeg 30 35556414 ns/op -BenchmarkCrop 30 51768475 ns/op -BenchmarkExtract 30 50866406 ns/op -ok 9.424s +BenchmarkRotateJpeg-8 20 64686945 ns/op +BenchmarkResizeLargeJpeg-8 20 63390416 ns/op +BenchmarkResizePng-8 100 18147294 ns/op +BenchmarkResizeWebP-8 100 20836741 ns/op +BenchmarkConvertToJpeg-8 100 12831812 ns/op +BenchmarkConvertToPng-8 10 128901422 ns/op +BenchmarkConvertToWebp-8 10 204027990 ns/op +BenchmarkCropJpeg-8 30 59068572 ns/op +BenchmarkCropPng-8 10 117303259 ns/op +BenchmarkCropWebP-8 10 107060659 ns/op +BenchmarkExtractJpeg-8 50 30708919 ns/op +BenchmarkExtractPng-8 3000 595546 ns/op +BenchmarkExtractWebp-8 3000 386379 ns/op +BenchmarkZoomJpeg-8 10 160005424 ns/op +BenchmarkZoomPng-8 30 44561047 ns/op +BenchmarkZoomWebp-8 10 126732678 ns/op +BenchmarkWatermarkJpeg-8 20 79006133 ns/op +BenchmarkWatermarPng-8 200 8197291 ns/op +BenchmarkWatermarWebp-8 30 49360369 ns/op ``` -## API - -### Examples +## Examples ```go import ( @@ -140,6 +166,46 @@ if bimg.NewImage(newImage).Type() == "png" { } ``` +#### Force resize + +Force resize operation without perserving the aspect ratio: + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).ForceResize(1000, 500) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +size := bimg.Size(newImage) +if size.Width != 1000 || size.Height != 500 { + fmt.Fprintln(os.Stderr, "Incorrect image size") +} +``` + +#### Custom colour space (black & white) + +```go +buffer, err := bimg.Read("image.jpg") +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +newImage, err := bimg.NewImage(buffer).Colourspace(bimg.INTERPRETATION_B_W) +if err != nil { + fmt.Fprintln(os.Stderr, err) +} + +colourSpace, _ := bimg.ImageInterpretation(newImage) +if colourSpace != bimg.INTERPRETATION_B_W { + fmt.Fprintln(os.Stderr, "Invalid colour space") +} +``` + #### Custom options See [Options](https://godoc.org/github.com/h2non/bimg#Options) struct to discover all the available fields @@ -151,6 +217,7 @@ options := bimg.Options{ Crop: true, Quality: 95, Rotate: 180, + Interlace: true, } buffer, err := bimg.Read("image.jpg") @@ -174,19 +241,17 @@ 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}, - } +watermark := bimg.Watermark{ + Text: "Chuck Norris (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() +newImage, err := bimg.NewImage(buffer).Watermark(watermark) if err != nil { fmt.Fprintln(os.Stderr, err) } @@ -220,349 +285,36 @@ if err != nil { bimg.Write("new.jpg", newImage) ``` -#### func DetermineImageTypeName +## Debugging -```go -func DetermineImageTypeName(buf []byte) string +Run the process passing the `DEBUG` environment variable ``` -Determines the image type format by name (jpeg, png, webp or tiff) - -#### func Initialize - -```go -func Initialize() -``` -Explicit thread-safe start of libvips. You should only call this function if you -previously shutdown libvips - -#### func IsTypeNameSupported - -```go -func IsTypeNameSupported(t string) bool -``` -Check if a given image type name is supported - -#### func IsTypeSupported - -```go -func IsTypeSupported(t ImageType) bool -``` -Check if a given image type is supported - -#### func Read - -```go -func Read(path string) ([]byte, error) +DEBUG=bimg ./app ``` -#### func Resize - -```go -func Resize(buf []byte, o Options) ([]byte, error) +Enable libvips traces (note that a lot of data will be written in stdout): +``` +VIPS_TRACE=1 ./app ``` -#### func Shutdown +## API -```go -func Shutdown() -``` -Explicit thread-safe libvips shutdown. Call this to drop caches. If libvips was -already initialized, the function is no-op +See [godoc reference](https://godoc.org/github.com/h2non/bimg) for detailed API documentation. -#### func VipsDebug +## Credits -```go -func VipsDebug() -``` -Output to stdout collected data for debugging purposes - -#### func Write - -```go -func Write(path string, buf []byte) error -``` - -#### type Angle - -```go -type Angle int -``` - - -```go -const ( - D0 Angle = C.VIPS_ANGLE_D0 - D90 Angle = C.VIPS_ANGLE_D90 - D180 Angle = C.VIPS_ANGLE_D180 - D270 Angle = C.VIPS_ANGLE_D270 -) -``` - -#### type Direction - -```go -type Direction int -``` - - -```go -const ( - HORIZONTAL Direction = C.VIPS_DIRECTION_HORIZONTAL - VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL -) -``` - -#### type Gravity - -```go -type Gravity int -``` - - -```go -const ( - CENTRE Gravity = iota - NORTH - EAST - SOUTH - WEST -) -``` - -#### type Image - -```go -type Image struct { -} -``` - - -#### func NewImage - -```go -func NewImage(buf []byte) *Image -``` -Creates a new image - -#### func (*Image) Convert - -```go -func (i *Image) Convert(t ImageType) ([]byte, error) -``` -Convert image to another format - -#### func (*Image) Crop - -```go -func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) -``` -Crop the image to the exact size specified - -#### func (*Image) CropByHeight - -```go -func (i *Image) CropByHeight(height int) ([]byte, error) -``` -Crop an image by height (auto width) - -#### func (*Image) CropByWidth - -```go -func (i *Image) CropByWidth(width int) ([]byte, error) -``` -Crop an image by width (auto height) - -#### func (*Image) Enlarge - -```go -func (i *Image) Enlarge(width, height int) ([]byte, error) -``` -Enlarge the image from the by X/Y axis - -#### func (*Image) Extract - -```go -func (i *Image) Extract(top, left, width, height int) ([]byte, error) -``` -Extract area from the by X/Y axis - -#### func (*Image) Flip - -```go -func (i *Image) Flip() ([]byte, error) -``` -Flip the image about the vertical Y axis - -#### func (*Image) Flop - -```go -func (i *Image) Flop() ([]byte, error) -``` -Flop the image about the horizontal X axis - -#### func (*Image) Metadata - -```go -func (i *Image) Metadata() (ImageMetadata, error) -``` -Get image metadata (size, alpha channel, profile, EXIF rotation) - -#### func (*Image) Process - -```go -func (i *Image) Process(o Options) ([]byte, error) -``` -Transform the image by custom options - -#### func (*Image) Resize - -```go -func (i *Image) Resize(width, height int) ([]byte, error) -``` -Resize the image to fixed width and height - -#### func (*Image) Rotate - -```go -func (i *Image) Rotate(a Angle) ([]byte, error) -``` -Rotate the image by given angle degrees (0, 90, 180 or 270) - -#### func (*Image) Size - -```go -func (i *Image) Size() (ImageSize, error) -``` -Get image size - -#### func (*Image) Thumbnail - -```go -func (i *Image) Thumbnail(pixels int) ([]byte, error) -``` -Thumbnail the image by the a given width by aspect ratio 4:4 - -#### func (*Image) Type - -```go -func (i *Image) Type() string -``` -Get image type format (jpeg, png, webp, tiff) - -#### type ImageMetadata - -```go -type ImageMetadata struct { - Orientation int - Channels int - Alpha bool - Profile bool - Type string - Space string - Size ImageSize -} -``` - - -#### func Metadata - -```go -func Metadata(buf []byte) (ImageMetadata, error) -``` -Extract the image metadata (size, type, alpha channel, profile, EXIF -orientation...) - -#### type ImageSize - -```go -type ImageSize struct { - Width int - Height int -} -``` - - -#### func Size - -```go -func Size(buf []byte) (ImageSize, error) -``` -Get the image size by width and height pixels - -#### type ImageType - -```go -type ImageType int -``` - - -```go -const ( - UNKNOWN ImageType = iota - JPEG - WEBP - PNG - TIFF - MAGICK -) -``` - -#### func DetermineImageType - -```go -func DetermineImageType(buf []byte) ImageType -``` -Determines the image type format (jpeg, png, webp or tiff) - -#### type Interpolator - -```go -type Interpolator int -``` - -```go -const ( - BICUBIC Interpolator = iota - BILINEAR - NOHALO -) -``` - -#### func (Interpolator) String - -```go -func (i Interpolator) String() string -``` - -#### type Options - -```go -type Options struct { - Height int - Width int - AreaHeight int - AreaWidth int - Top int - Left int - Extend int - Quality int - Compression int - Crop bool - Enlarge bool - Embed bool - Flip bool - Flop bool - Rotate Angle - Gravity Gravity - Type ImageType - Interpolator Interpolator -} -``` - -## Special Thanks +People who recurrently contributed to improve `bimg` in some way. - [John Cupitt](https://github.com/jcupitt) +- [Yoan Blanc](https://github.com/greut) +- [Christophe Eblé](https://github.com/chreble) +- [Brant Fitzsimmons](https://github.com/bfitzsimmons) +- [Thomas Meson](https://github.com/zllak) + +Thank you! ## License MIT - Tomas Aparicio + +[![views](https://sourcegraph.com/api/repos/github.com/h2non/bimg/.counters/views.svg)](https://sourcegraph.com/github.com/h2non/bimg) diff --git a/debug.go b/debug.go index 9f6fcea..c781668 100644 --- a/debug.go +++ b/debug.go @@ -1,62 +1,5 @@ package bimg -import ( - "github.com/dustin/go-humanize" - . "github.com/tj/go-debug" - "runtime" - "strconv" - "time" -) +import . "github.com/tj/go-debug" var debug = Debug("bimg") - -// Print Go memory and garbage collector stats. Useful for debugging -func PrintMemoryStats() { - log := Debug("memory") - mem := memoryStats() - - log("\u001b[33m---- Memory Dump Stats ----\u001b[39m") - log("Allocated: %s", humanize.Bytes(mem.Alloc)) - log("Total Allocated: %s", humanize.Bytes(mem.TotalAlloc)) - log("Memory Allocations: %d", mem.Mallocs) - log("Memory Frees: %d", mem.Frees) - log("Heap Allocated: %s", humanize.Bytes(mem.HeapAlloc)) - log("Heap System: %s", humanize.Bytes(mem.HeapSys)) - log("Heap In Use: %s", humanize.Bytes(mem.HeapInuse)) - log("Heap Idle: %s", humanize.Bytes(mem.HeapIdle)) - log("Heap OS Related: %s", humanize.Bytes(mem.HeapReleased)) - log("Heap Objects: %s", humanize.Bytes(mem.HeapObjects)) - log("Stack In Use: %s", humanize.Bytes(mem.StackInuse)) - log("Stack System: %s", humanize.Bytes(mem.StackSys)) - log("Stack Span In Use: %s", humanize.Bytes(mem.MSpanInuse)) - log("Stack Cache In Use: %s", humanize.Bytes(mem.MCacheInuse)) - log("Next GC cycle: %s", humanizeNano(mem.NextGC)) - log("Last GC cycle: %s", humanize.Time(time.Unix(0, int64(mem.LastGC)))) - log("\u001b[33m---- End Memory Dump ----\u001b[39m") -} - -func memoryStats() runtime.MemStats { - var mem runtime.MemStats - runtime.ReadMemStats(&mem) - return mem -} - -func humanizeNano(n uint64) string { - var suffix string - - switch { - case n > 1e9: - n /= 1e9 - suffix = "s" - case n > 1e6: - n /= 1e6 - suffix = "ms" - case n > 1e3: - n /= 1e3 - suffix = "us" - default: - suffix = "ns" - } - - return strconv.Itoa(int(n)) + suffix -} diff --git a/file.go b/file.go index b6c5b04..e84fb7a 100644 --- a/file.go +++ b/file.go @@ -1,23 +1,9 @@ package bimg -import ( - "io/ioutil" - "os" -) +import "io/ioutil" func Read(path string) ([]byte, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - buf, err := ioutil.ReadAll(file) - if err != nil { - return nil, err - } - - return buf, nil + return ioutil.ReadFile(path) } func Write(path string, buf []byte) error { diff --git a/image.go b/image.go index f32606e..6b6e179 100644 --- a/image.go +++ b/image.go @@ -4,6 +4,11 @@ type Image struct { buffer []byte } +// Creates a new image +func NewImage(buf []byte) *Image { + return &Image{buf} +} + // Resize the image to fixed width and height func (i *Image) Resize(width, height int) ([]byte, error) { options := Options{ @@ -14,6 +19,27 @@ func (i *Image) Resize(width, height int) ([]byte, error) { return i.Process(options) } +// Force resize with custom size (aspect ratio won't be maintained) +func (i *Image) ForceResize(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Force: true, + } + return i.Process(options) +} + +// Resize the image to fixed width and height with additional crop transformation +func (i *Image) ResizeAndCrop(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Embed: true, + Crop: true, + } + return i.Process(options) +} + // Extract area from the by X/Y axis func (i *Image) Extract(top, left, width, height int) ([]byte, error) { options := Options{ @@ -22,10 +48,15 @@ func (i *Image) Extract(top, left, width, height int) ([]byte, error) { AreaWidth: width, AreaHeight: height, } + + if top == 0 && left == 0 { + options.Top = -1 + } + return i.Process(options) } -// Enlarge the image from the by X/Y axis +// Enlarge the image by width and height. Aspect ratio is maintained func (i *Image) Enlarge(width, height int) ([]byte, error) { options := Options{ Width: width, @@ -35,6 +66,17 @@ func (i *Image) Enlarge(width, height int) ([]byte, error) { return i.Process(options) } +// Enlarge the image by width and height with additional crop transformation +func (i *Image) EnlargeAndCrop(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Enlarge: true, + Crop: true, + } + return i.Process(options) +} + // Crop the image to the exact size specified func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) { options := Options{ @@ -75,15 +117,16 @@ func (i *Image) Thumbnail(pixels int) ([]byte, error) { return i.Process(options) } -// Insert an image to the existent one as watermark +// Add text as watermark on the given image func (i *Image) Watermark(w Watermark) ([]byte, error) { options := Options{Watermark: w} return i.Process(options) } -// Zoom the image by the given factor -func (i *Image) Zoom(level int) ([]byte, error) { - options := Options{Zoom: level} +// Zoom the image by the given factor. +// You should probably call Extract() before +func (i *Image) Zoom(factor int) ([]byte, error) { + options := Options{Zoom: factor} return i.Process(options) } @@ -111,6 +154,12 @@ func (i *Image) Convert(t ImageType) ([]byte, error) { return i.Process(options) } +// Colour space conversion +func (i *Image) Colourspace(c Interpretation) ([]byte, error) { + options := Options{Interpretation: c} + return i.Process(options) +} + // Transform the image by custom options func (i *Image) Process(o Options) ([]byte, error) { image, err := Resize(i.buffer, o) @@ -126,6 +175,17 @@ func (i *Image) Metadata() (ImageMetadata, error) { return Metadata(i.buffer) } +// Get the image interpretation type +// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation +func (i *Image) Interpretation() (Interpretation, error) { + return ImageInterpretation(i.buffer) +} + +// Check if the current image has a valid colourspace +func (i *Image) ColourspaceIsSupported() (bool, error) { + return ColourspaceIsSupported(i.buffer) +} + // Get image type format (jpeg, png, webp, tiff) func (i *Image) Type() string { return DetermineImageTypeName(i.buffer) @@ -140,8 +200,3 @@ func (i *Image) Size() (ImageSize, error) { func (i *Image) Image() []byte { return i.buffer } - -// Creates a new image -func NewImage(buf []byte) *Image { - return &Image{buf} -} diff --git a/image_test.go b/image_test.go index ab0bb08..b0faff5 100644 --- a/image_test.go +++ b/image_test.go @@ -20,13 +20,27 @@ func TestImageResize(t *testing.T) { Write("fixtures/test_resize_out.jpg", buf) } -func TestImageExtract(t *testing.T) { - buf, err := initImage("test.jpg").Extract(100, 100, 300, 300) +func TestImageResizeAndCrop(t *testing.T) { + buf, err := initImage("test.jpg").ResizeAndCrop(300, 200) if err != nil { t.Errorf("Cannot process the image: %#v", err) } - err = assertSize(buf, 300, 300) + err = assertSize(buf, 300, 200) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_resize_crop_out.jpg", buf) +} + +func TestImageExtract(t *testing.T) { + buf, err := initImage("test.jpg").Extract(100, 100, 300, 200) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 300, 200) if err != nil { t.Error(err) } @@ -34,6 +48,20 @@ func TestImageExtract(t *testing.T) { Write("fixtures/test_extract_out.jpg", buf) } +func TestImageExtractZero(t *testing.T) { + buf, err := initImage("test.jpg").Extract(0, 0, 300, 200) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 300, 200) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_extract_zero_out.jpg", buf) +} + func TestImageEnlarge(t *testing.T) { buf, err := initImage("test.png").Enlarge(500, 375) if err != nil { @@ -48,10 +76,24 @@ func TestImageEnlarge(t *testing.T) { Write("fixtures/test_enlarge_out.jpg", buf) } +func TestImageEnlargeAndCrop(t *testing.T) { + buf, err := initImage("test.png").EnlargeAndCrop(800, 480) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + err = assertSize(buf, 800, 480) + if err != nil { + t.Error(err) + } + + Write("fixtures/test_enlarge_crop_out.jpg", buf) +} + func TestImageCrop(t *testing.T) { buf, err := initImage("test.jpg").Crop(800, 600, NORTH) if err != nil { - t.Errorf("Cannot process the image: %#v", err) + t.Errorf("Cannot process the image: %s", err) } err = assertSize(buf, 800, 600) @@ -65,7 +107,7 @@ func TestImageCrop(t *testing.T) { func TestImageCropByWidth(t *testing.T) { buf, err := initImage("test.jpg").CropByWidth(600) if err != nil { - t.Errorf("Cannot process the image: %#v", err) + t.Errorf("Cannot process the image: %s", err) } err = assertSize(buf, 600, 375) @@ -79,7 +121,7 @@ func TestImageCropByWidth(t *testing.T) { func TestImageCropByHeight(t *testing.T) { buf, err := initImage("test.jpg").CropByHeight(300) if err != nil { - t.Errorf("Cannot process the image: %#v", err) + t.Errorf("Cannot process the image: %s", err) } err = assertSize(buf, 480, 300) @@ -93,7 +135,7 @@ func TestImageCropByHeight(t *testing.T) { func TestImageThumbnail(t *testing.T) { buf, err := initImage("test.jpg").Thumbnail(100) if err != nil { - t.Errorf("Cannot process the image: %#v", err) + t.Errorf("Cannot process the image: %s", err) } err = assertSize(buf, 100, 100) @@ -134,13 +176,51 @@ func TestImageWatermark(t *testing.T) { Write("fixtures/test_watermark_out.jpg", buf) } -func TestImageZoom(t *testing.T) { - buf, err := initImage("test.jpg").Zoom(1) +func TestImageWatermarkNoReplicate(t *testing.T) { + image := initImage("test.jpg") + _, err := image.Crop(800, 600, NORTH) if err != nil { - t.Errorf("Cannot process the image: %#v", err) + t.Errorf("Cannot process the image: %s", err) } - err = assertSize(buf, 3360, 2100) + buf, err := image.Watermark(Watermark{ + Text: "Copy me if you can", + Opacity: 0.5, + Width: 200, + DPI: 100, + NoReplicate: true, + Background: Color{255, 255, 255}, + }) + 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_replicate_out.jpg", buf) +} + +func TestImageZoom(t *testing.T) { + image := initImage("test.jpg") + + _, err := image.Extract(100, 100, 400, 300) + if err != nil { + t.Errorf("Cannot extract the image: %s", err) + } + + buf, err := image.Zoom(1) + if err != nil { + t.Errorf("Cannot process the image: %s", err) + } + + err = assertSize(buf, 800, 600) if err != nil { t.Error(err) } @@ -180,6 +260,19 @@ func TestImageConvert(t *testing.T) { Write("fixtures/test_image_convert_out.png", buf) } +func TestTransparentImageConvert(t *testing.T) { + image := initImage("transparent.png") + options := Options{ + Type: JPEG, + Background: Color{255, 255, 255}, + } + buf, err := image.Process(options) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("fixtures/test_transparent_image_convert_out.jpg", buf) +} + func TestImageMetadata(t *testing.T) { data, err := initImage("test.png").Metadata() if err != nil { @@ -196,6 +289,48 @@ func TestImageMetadata(t *testing.T) { } } +func TestInterpretation(t *testing.T) { + interpretation, err := initImage("test.jpg").Interpretation() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if interpretation != INTERPRETATION_sRGB { + t.Errorf("Invalid interpretation: %d", interpretation) + } +} + +func TestImageColourspace(t *testing.T) { + tests := []struct { + file string + interpretation Interpretation + }{ + {"test.jpg", INTERPRETATION_sRGB}, + {"test.jpg", INTERPRETATION_B_W}, + } + + for _, test := range tests { + buf, err := initImage(test.file).Colourspace(test.interpretation) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + interpretation, err := ImageInterpretation(buf) + if interpretation != test.interpretation { + t.Errorf("Invalid colourspace") + } + } +} + +func TestImageColourspaceIsSupported(t *testing.T) { + supported, err := initImage("test.jpg").ColourspaceIsSupported() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if supported != true { + t.Errorf("Non-supported colourspace") + } +} + func TestFluentInterface(t *testing.T) { image := initImage("test.jpg") _, err := image.CropByWidth(300) diff --git a/metadata.go b/metadata.go index a7c6d5a..c1e311a 100644 --- a/metadata.go +++ b/metadata.go @@ -18,6 +18,7 @@ type ImageMetadata struct { Profile bool Type string Space string + Colourspace string Size ImageSize } @@ -34,6 +35,17 @@ func Size(buf []byte) (ImageSize, error) { }, nil } +// Check in the image colourspace is supported by libvips +func ColourspaceIsSupported(buf []byte) (bool, error) { + return vipsColourspaceIsSupportedBuffer(buf) +} + +// Get the image interpretation type +// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation +func ImageInterpretation(buf []byte) (Interpretation, error) { + return vipsInterpretationBuffer(buf) +} + // Extract the image metadata (size, type, alpha channel, profile, EXIF orientation...) func Metadata(buf []byte) (ImageMetadata, error) { defer C.vips_thread_shutdown() @@ -56,7 +68,7 @@ func Metadata(buf []byte) (ImageMetadata, error) { Alpha: vipsHasAlpha(image), Profile: vipsHasProfile(image), Space: vipsSpace(image), - Type: getImageTypeName(imageType), + Type: ImageTypeName(imageType), } return metadata, nil diff --git a/metadata_test.go b/metadata_test.go index f5021ec..90ab09a 100644 --- a/metadata_test.go +++ b/metadata_test.go @@ -17,7 +17,6 @@ func TestSize(t *testing.T) { {"test.png", 400, 300}, {"test.webp", 550, 368}, } - for _, file := range files { size, err := Size(readFile(file.name)) if err != nil { @@ -39,15 +38,15 @@ func TestMetadata(t *testing.T) { profile bool space string }{ - {"test.jpg", "jpeg", 0, false, false, "bicubic"}, - {"test.png", "png", 0, true, false, "bicubic"}, - {"test.webp", "webp", 0, false, false, "bicubic"}, + {"test.jpg", "jpeg", 0, false, false, "srgb"}, + {"test.png", "png", 0, true, false, "srgb"}, + {"test.webp", "webp", 0, false, false, "srgb"}, } for _, file := range files { metadata, err := Metadata(readFile(file.name)) if err != nil { - t.Fatalf("Cannot read the image: %#v", err) + t.Fatalf("Cannot read the image: %s -> %s", file.name, err) } if metadata.Type != file.format { @@ -62,6 +61,58 @@ func TestMetadata(t *testing.T) { if metadata.Profile != file.profile { t.Fatalf("Unexpected image profile: %s != %s", metadata.Profile, file.profile) } + if metadata.Space != file.space { + t.Fatalf("Unexpected image profile: %s != %s", metadata.Profile, file.profile) + } + } +} + +func TestImageInterpretation(t *testing.T) { + files := []struct { + name string + interpretation Interpretation + }{ + {"test.jpg", INTERPRETATION_sRGB}, + {"test.png", INTERPRETATION_sRGB}, + {"test.webp", INTERPRETATION_sRGB}, + } + + for _, file := range files { + interpretation, err := ImageInterpretation(readFile(file.name)) + if err != nil { + t.Fatalf("Cannot read the image: %s -> %s", file.name, err) + } + if interpretation != file.interpretation { + t.Fatalf("Unexpected image interpretation") + } + } +} + +func TestColourspaceIsSupported(t *testing.T) { + files := []struct { + name string + }{ + {"test.jpg"}, + {"test.png"}, + {"test.webp"}, + } + + for _, file := range files { + supported, err := ColourspaceIsSupported(readFile(file.name)) + if err != nil { + t.Fatalf("Cannot read the image: %s -> %s", file.name, err) + } + if supported != true { + t.Fatalf("Unsupported image colourspace") + } + } + + supported, err := initImage("test.jpg").ColourspaceIsSupported() + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + if supported != true { + t.Errorf("Non-supported colourspace") } } diff --git a/options.go b/options.go index 598faf0..fae5fd7 100644 --- a/options.go +++ b/options.go @@ -42,10 +42,10 @@ func (i Interpolator) String() string { type Angle int const ( - D0 Angle = C.VIPS_ANGLE_D0 - D90 Angle = C.VIPS_ANGLE_D90 - D180 Angle = C.VIPS_ANGLE_D180 - D270 Angle = C.VIPS_ANGLE_D270 + D0 Angle = 0 + D90 Angle = 90 + D180 Angle = 180 + D270 Angle = 270 ) type Direction int @@ -55,11 +55,35 @@ const ( VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL ) +// Image interpretation type +// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation +type Interpretation int + +const ( + INTERPRETATION_ERROR Interpretation = C.VIPS_INTERPRETATION_ERROR + INTERPRETATION_MULTIBAND Interpretation = C.VIPS_INTERPRETATION_MULTIBAND + INTERPRETATION_B_W Interpretation = C.VIPS_INTERPRETATION_B_W + INTERPRETATION_CMYK Interpretation = C.VIPS_INTERPRETATION_CMYK + INTERPRETATION_RGB Interpretation = C.VIPS_INTERPRETATION_RGB + INTERPRETATION_sRGB Interpretation = C.VIPS_INTERPRETATION_sRGB + INTERPRETATION_RGB16 Interpretation = C.VIPS_INTERPRETATION_RGB16 + INTERPRETATION_GREY16 Interpretation = C.VIPS_INTERPRETATION_GREY16 + INTERPRETATION_scRGB Interpretation = C.VIPS_INTERPRETATION_scRGB + INTERPRETATION_LAB Interpretation = C.VIPS_INTERPRETATION_LAB + INTERPRETATION_XYZ Interpretation = C.VIPS_INTERPRETATION_XYZ +) + +const WATERMARK_FONT = "sans 10" + // Color represents a traditional RGB color scheme type Color struct { R, G, B uint8 } +// Shortcut to black RGB color representation +var ColorBlack = Color{0, 0, 0} + +// Text-based watermark configuration type Watermark struct { Width int DPI int @@ -71,27 +95,48 @@ type Watermark struct { Background Color } -type Options struct { - Height int - Width int - AreaHeight int - AreaWidth int - Top int - Left int - Extend int - Quality int - Compression int - Zoom int - Crop bool - Enlarge bool - Embed bool - Flip bool - Flop bool - NoAutoRotate bool - Colorspace bool - Rotate Angle - Gravity Gravity - Watermark Watermark - Type ImageType - Interpolator Interpolator +type GaussianBlur struct { + Sigma float64 + MinAmpl float64 +} + +type Sharpen struct { + Radius int + X1 float64 + Y2 float64 + Y3 float64 + M1 float64 + M2 float64 +} + +// Supported image transformation options +type Options struct { + Height int + Width int + AreaHeight int + AreaWidth int + Top int + Left int + Extend int + Quality int + Compression int + Zoom int + Crop bool + Enlarge bool + Embed bool + Flip bool + Flop bool + Force bool + NoAutoRotate bool + NoProfile bool + Interlace bool + Rotate Angle + Background Color + Gravity Gravity + Watermark Watermark + Type ImageType + Interpolator Interpolator + Interpretation Interpretation + GaussianBlur GaussianBlur + Sharpen Sharpen } diff --git a/resize.go b/resize.go index 1bfe48d..563a787 100644 --- a/resize.go +++ b/resize.go @@ -23,16 +23,8 @@ func Resize(buf []byte, o Options) ([]byte, error) { return nil, err } - // Defaults - if o.Quality == 0 { - o.Quality = QUALITY - } - if o.Compression == 0 { - o.Compression = 6 - } - if o.Type == 0 { - o.Type = imageType - } + // Clone and define default options + o = applyDefaults(o, imageType) if IsTypeSupported(o.Type) == false { return nil, errors.New("Unsupported image output type") @@ -43,13 +35,17 @@ func Resize(buf []byte, o Options) ([]byte, error) { inWidth := int(image.Xsize) inHeight := int(image.Ysize) + // Infer the required operation based on the in/out image sizes for a coherent transformation + normalizeOperation(&o, inWidth, inHeight) + // image calculations factor := imageCalculations(&o, inWidth, inHeight) - shrink := int(math.Max(math.Floor(factor), 1)) - residual := float64(shrink) / factor + shrink := calculateShrink(factor, o.Interpolator) + residual := calculateResidual(factor, shrink) - // Do not enlarge the output if the input width *or* height are already less than the required dimensions - if o.Enlarge == false { + // Do not enlarge the output if the input width or height + // are already less than the required dimensions + if !o.Enlarge && !o.Force { if inWidth < o.Width && inHeight < o.Height { factor = 1.0 shrink = 1 @@ -65,81 +61,161 @@ func Resize(buf []byte, o Options) ([]byte, error) { if err != nil { return nil, err } - if tmpImage != nil { - image = tmpImage - factor = math.Max(factor, 1.0) - shrink = int(math.Floor(factor)) - residual = float64(shrink) / factor - } + + image = tmpImage + factor = math.Max(factor, 1.0) + shrink = int(math.Floor(factor)) + residual = float64(shrink) / factor } - // Calculate integral box shrink - windowSize := vipsWindowSize(o.Interpolator.String()) - if factor >= 2 && windowSize > 3 { - // Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic - shrink = int(math.Max(float64(math.Floor(factor*3.0/windowSize)), 1)) - } - - // Transform image if necessary - shouldTransform := o.Width != inWidth || o.Height != inHeight || o.AreaWidth > 0 || o.AreaHeight > 0 - if shouldTransform { - // Use vips_shrink with the integral reduction - if shrink > 1 { - image, residual, err = shrinkImage(image, o, residual, shrink) - if err != nil { - return nil, err - } - } - // Use vips_affine with the remaining float part - if residual != 0 { - image, err = vipsAffine(image, residual, o.Interpolator) - if err != nil { - return nil, err - } - } - - debug("Transform image: factor=%v, shrink=%v, residual=%v", factor, shrink, residual) - // Extract area from image - image, err = extractImage(image, o) - if err != nil { - return nil, err - } - } - - // Zoom image if necessary + // Zoom image, if necessary image, err = zoomImage(image, o.Zoom) if err != nil { return nil, err } - // Rotate / flip image if necessary - image, err = rotateImage(image, o) + // Rotate / flip image, if necessary + image, err = rotateAndFlipImage(image, o) if err != nil { return nil, err } - saveOptions := vipsSaveOptions{ - Quality: o.Quality, - Type: o.Type, - Compression: o.Compression, + // Transform image, if necessary + if shouldTransformImage(o, inWidth, inHeight) { + image, err = transformImage(image, o, shrink, residual) + if err != nil { + return nil, err + } } - // watermark + // Apply effects, if necessary + if shouldApplyEffects(o) { + image, err = applyEffects(image, o) + if err != nil { + return nil, err + } + } + + // Add watermark, if necessary image, err = watermakImage(image, o.Watermark) if err != nil { return nil, err } - // Finally save as buffer - buf, err = vipsSave(image, saveOptions) + // Flatten image on a background, if necessary + image, err = imageFlatten(image, imageType, o) if err != nil { return nil, err } - return buf, nil + 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 extractImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, error) { +func applyDefaults(o Options, imageType ImageType) Options { + if o.Quality == 0 { + o.Quality = QUALITY + } + if o.Compression == 0 { + o.Compression = 6 + } + if o.Type == 0 { + o.Type = imageType + } + if o.Interpretation == 0 { + o.Interpretation = INTERPRETATION_sRGB + } + return o +} + +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 + } +} + +func shouldTransformImage(o Options, inWidth, inHeight int) bool { + return o.Force || (o.Width > 0 && o.Width != inWidth) || + (o.Height > 0 && o.Height != inHeight) || o.AreaWidth > 0 || o.AreaHeight > 0 +} + +func shouldApplyEffects(o Options) bool { + return o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 || o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 +} + +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) + if err != nil { + return nil, err + } + } + + residualx, residualy := residual, residual + if o.Force { + residualx = float64(o.Width) / float64(image.Xsize) + residualy = float64(o.Height) / float64(image.Ysize) + } + + if o.Force || residual != 0 { + image, err = vipsAffine(image, residualx, residualy, o.Interpolator) + if err != nil { + return nil, err + } + } + + if o.Force { + o.Crop = false + o.Embed = false + } + + image, err = extractOrEmbedImage(image, o) + if err != nil { + return nil, err + } + + debug("Transform: shrink=%v, residual=%v, interpolator=%v", + shrink, residual, o.Interpolator.String()) + + return image, nil +} + +func applyEffects(image *C.VipsImage, o Options) (*C.VipsImage, error) { + var err error + + if o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 { + image, err = vipsGaussianBlur(image, o.GaussianBlur) + if err != nil { + return nil, err + } + } + + if o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 { + image, err = vipsSharpen(image, o.Sharpen) + if err != nil { + return nil, err + } + } + + debug("Effects: gaussSigma=%v, gaussMinAmpl=%v, sharpenRadius=%v", + o.GaussianBlur.Sigma, o.GaussianBlur.MinAmpl, o.Sharpen.Radius) + + return image, nil +} + +func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { var err error = nil inWidth := int(image.Xsize) inHeight := int(image.Ysize) @@ -149,25 +225,31 @@ func extractImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, width := int(math.Min(float64(inWidth), float64(o.Width))) height := int(math.Min(float64(inHeight), float64(o.Height))) left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity) + left, top = int(math.Max(float64(left), 0)), int(math.Max(float64(top), 0)) image, err = vipsExtract(image, left, top, width, height) break case o.Embed: left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend) break - case o.Top > 0 || o.Left > 0: - if o.AreaWidth == 0 || o.AreaHeight == 0 { - err = errors.New("Area to extract cannot be 0") - } else { - image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight) + case o.Top != 0 || o.Left != 0: + if o.AreaWidth == 0 { + o.AreaHeight = o.Width } + if o.AreaHeight == 0 { + o.AreaHeight = o.Height + } + if o.AreaWidth == 0 || o.AreaHeight == 0 { + return nil, errors.New("Extract area width/height params are required") + } + image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight) break } return image, err } -func rotateImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, error) { +func rotateAndFlipImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { var err error var direction Direction = -1 @@ -198,17 +280,17 @@ func rotateImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, e return image, err } -func watermakImage(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImage, error) { - if len(w.Text) == 0 { +func watermakImage(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { + if w.Text == "" { return image, nil } // Defaults - if len(w.Font) == 0 { - w.Font = "sans 10" + if w.Font == "" { + w.Font = WATERMARK_FONT } if w.Width == 0 { - w.Width = int(math.Floor(float64(image.Xsize / 8))) + w.Width = int(math.Floor(float64(image.Xsize / 6))) } if w.DPI == 0 { w.DPI = 150 @@ -230,15 +312,23 @@ func watermakImage(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag return image, nil } -func zoomImage(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error) { - if zoom == 0 { +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 { return image, nil } + return vipsFlattenBackground(image, o.Background) +} + +func zoomImage(image *C.VipsImage, zoom int) (*C.VipsImage, error) { + if zoom == 0 { + return image, nil + } return vipsZoom(image, zoom+1) } -func shrinkImage(image *C.struct__VipsImage, o Options, residual float64, shrink int) (*C.struct__VipsImage, float64, error) { +func shrinkImage(image *C.VipsImage, o Options, residual float64, shrink int) (*C.VipsImage, float64, error) { // Use vips_shrink with the integral reduction image, err := vipsShrink(image, shrink) if err != nil { @@ -258,8 +348,8 @@ func shrinkImage(image *C.struct__VipsImage, o Options, residual float64, shrink return image, residual, nil } -func shrinkJpegImage(buf []byte, input *C.struct__VipsImage, factor float64, shrink int) (*C.struct__VipsImage, float64, error) { - var image *C.struct__VipsImage +func shrinkJpegImage(buf []byte, input *C.VipsImage, factor float64, shrink int) (*C.VipsImage, float64, error) { + var image *C.VipsImage var err error shrinkOnLoad := 1 @@ -305,8 +395,8 @@ func imageCalculations(o *Options, inWidth, inHeight int) float64 { case o.Height > 0: factor = yfactor o.Width = int(math.Floor(float64(inWidth) / factor)) + // Identity transform default: - // Identity transform o.Width = inWidth o.Height = inHeight break @@ -337,50 +427,63 @@ func calculateCrop(inWidth, inHeight, outWidth, outHeight int, gravity Gravity) return left, top } -func calculateRotationAndFlip(image *C.struct__VipsImage, angle Angle) (Angle, bool) { +func calculateRotationAndFlip(image *C.VipsImage, angle Angle) (Angle, bool) { rotate := D0 flip := false - if angle == -1 { - switch vipsExifOrientation(image) { - case 6: - rotate = D90 - break - case 3: - rotate = D180 - break - case 8: - rotate = D270 - break - case 2: - flip = true - break // flip 1 - case 7: - flip = true - rotate = D90 - break // flip 6 - case 4: - flip = true - rotate = D180 - break // flip 3 - case 5: - flip = true - rotate = D270 - break // flip 8 - } - } else { - if angle == 90 { - rotate = D90 - } else if angle == 180 { - rotate = D180 - } else if angle == 270 { - rotate = D270 - } + if angle > 0 { + return rotate, flip + } + + switch vipsExifOrientation(image) { + case 6: + rotate = D90 + break + case 3: + rotate = D180 + break + case 8: + rotate = D270 + break + case 2: + flip = true + break // flip 1 + case 7: + flip = true + rotate = D90 + break // flip 6 + case 4: + flip = true + rotate = D180 + break // flip 3 + case 5: + flip = true + rotate = D270 + break // flip 8 } return rotate, flip } +func calculateShrink(factor float64, i Interpolator) int { + var shrink float64 + + // Calculate integral box shrink + windowSize := vipsWindowSize(i.String()) + if factor >= 2 && windowSize > 3 { + // Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic + shrink = float64(math.Floor(factor * 3.0 / windowSize)) + } else { + shrink = math.Floor(factor) + } + + return int(math.Max(shrink, 1)) +} + +func calculateResidual(factor float64, shrink int) float64 { + return float64(shrink) / factor +} + func getAngle(angle Angle) Angle { divisor := angle % 90 if divisor != 0 { diff --git a/resize_test.go b/resize_test.go index 97b91bf..d57852c 100644 --- a/resize_test.go +++ b/resize_test.go @@ -20,9 +20,51 @@ func TestResize(t *testing.T) { t.Fatal("Image is not jpeg") } - err = Write("fixtures/test_out.jpg", newImg) - if err != nil { - t.Fatal("Cannot save the image") + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_out.jpg", newImg) +} + +func TestResizeCustomSizes(t *testing.T) { + tests := []struct { + file string + format ImageType + options Options + }{ + {"test.jpg", JPEG, Options{Width: 800, Height: 600}}, + {"test.jpg", JPEG, Options{Width: 1000, Height: 1000}}, + {"test.jpg", JPEG, Options{Width: 100, Height: 50}}, + {"test.jpg", JPEG, Options{Width: 2000, Height: 2000}}, + {"test.jpg", JPEG, Options{Width: 500, Height: 1000}}, + {"test.jpg", JPEG, Options{Width: 500}}, + {"test.jpg", JPEG, Options{Height: 500}}, + {"test.jpg", JPEG, Options{Crop: true, Width: 500, Height: 1000}}, + {"test.jpg", JPEG, Options{Crop: true, Enlarge: true, Width: 2000, Height: 1400}}, + {"test.jpg", JPEG, Options{Enlarge: true, Force: true, Width: 2000, Height: 2000}}, + {"test.jpg", JPEG, Options{Force: true, Width: 2000, Height: 2000}}, + } + + for _, test := range tests { + buf, _ := Read("fixtures/" + test.file) + image, err := Resize(buf, test.options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", test.options, err) + } + + if DetermineImageType(image) != test.format { + t.Fatal("Image format is invalid. Expected: %s", test.format) + } + + size, _ := Size(image) + if test.options.Height > 0 && size.Height != test.options.Height { + t.Fatalf("Invalid height: %d", size.Height) + } + if test.options.Width > 0 && size.Width != test.options.Width { + t.Fatalf("Invalid width: %d", size.Width) + } } } @@ -39,48 +81,12 @@ func TestRotate(t *testing.T) { t.Fatal("Image is not jpeg") } - err = Write("fixtures/test_rotate_out.jpg", newImg) - if err != nil { - t.Fatal("Cannot save the image") - } -} - -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) + size, _ := Size(newImg) + if size.Height != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } - 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") - } + Write("fixtures/test_rotate_out.jpg", newImg) } func TestInvalidRotate(t *testing.T) { @@ -96,44 +102,128 @@ func TestInvalidRotate(t *testing.T) { t.Fatal("Image is not jpeg") } - err = Write("fixtures/test_invalid_rotate_out.jpg", newImg) - if err != nil { - t.Fatal("Cannot save the image") + size, _ := Size(newImg) + if size.Height != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } + + Write("fixtures/test_invalid_rotate_out.jpg", newImg) } -func TestConvert(t *testing.T) { - width, height := 640, 480 - - options := Options{Width: width, Height: height, Crop: true, Type: PNG} - img, err := os.Open("fixtures/test.jpg") - if err != nil { - t.Fatal(err) - } - defer img.Close() - - buf, err := ioutil.ReadAll(img) - if err != nil { - t.Fatal(err) - } +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) != PNG { - t.Fatal("Image is not png") + if DetermineImageType(newImg) != JPEG { + t.Fatal("Image is not jpeg") } size, _ := Size(newImg) - if size.Height != height || size.Width != width { - t.Fatal("Invalid image size") + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) } - err = Write("fixtures/test_out.png", newImg) + Write("fixtures/test_corrupt_out.jpg", newImg) +} + +func TestNoColorProfile(t *testing.T) { + options := Options{Width: 800, Height: 600, NoProfile: true} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) if err != nil { - t.Fatal("Cannot save the image") + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + metadata, err := Metadata(newImg) + if metadata.Profile == true { + t.Fatal("Invalid profile data") + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } +} + +func TestGaussianBlur(t *testing.T) { + options := Options{Width: 800, Height: 600, GaussianBlur: GaussianBlur{Sigma: 5}} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_gaussian.jpg", newImg) +} + +func TestSharpen(t *testing.T) { + options := Options{Width: 800, Height: 600, Sharpen: Sharpen{Radius: 1, X1: 1.5, Y2: 20, Y3: 50, M1: 1, M2: 2}} + buf, _ := Read("fixtures/test.jpg") + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + size, _ := Size(newImg) + if size.Height != options.Height || size.Width != options.Width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + + Write("fixtures/test_sharpen.jpg", newImg) +} + +func TestConvert(t *testing.T) { + width, height := 300, 240 + formats := [3]ImageType{PNG, WEBP, JPEG} + + files := []string{ + "test.jpg", + "test.png", + "test.webp", + } + + for _, file := range files { + img, err := os.Open("fixtures/" + file) + if err != nil { + t.Fatal(err) + } + + buf, err := ioutil.ReadAll(img) + if err != nil { + t.Fatal(err) + } + img.Close() + + for _, format := range formats { + options := Options{Width: width, Height: height, Crop: true, Type: format} + + newImg, err := Resize(buf, options) + if err != nil { + t.Errorf("Resize(imgData, %#v) error: %#v", options, err) + } + + if DetermineImageType(newImg) != format { + t.Fatal("Image is not png") + } + + size, _ := Size(newImg) + if size.Height != height || size.Width != width { + t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height) + } + } } } @@ -166,10 +256,7 @@ func TestResizePngWithTransparency(t *testing.T) { t.Fatal("Invalid image size") } - err = Write("fixtures/transparent_out.png", newImg) - if err != nil { - t.Fatal("Cannot save the image") - } + Write("fixtures/transparent_out.png", newImg) } func runBenchmarkResize(file string, o Options, b *testing.B) { @@ -180,6 +267,11 @@ func runBenchmarkResize(file string, o Options, b *testing.B) { } } +func BenchmarkRotateJpeg(b *testing.B) { + options := Options{Rotate: 180} + runBenchmarkResize("test.jpg", options, b) +} + func BenchmarkResizeLargeJpeg(b *testing.B) { options := Options{ Width: 800, @@ -209,7 +301,17 @@ func BenchmarkConvertToJpeg(b *testing.B) { runBenchmarkResize("test.png", options, b) } -func BenchmarkCrop(b *testing.B) { +func BenchmarkConvertToPng(b *testing.B) { + options := Options{Type: PNG} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkConvertToWebp(b *testing.B) { + options := Options{Type: WEBP} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkCropJpeg(b *testing.B) { options := Options{ Width: 800, Height: 600, @@ -217,6 +319,22 @@ func BenchmarkCrop(b *testing.B) { runBenchmarkResize("test.jpg", options, b) } +func BenchmarkCropPng(b *testing.B) { + options := Options{ + Width: 800, + Height: 600, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkCropWebP(b *testing.B) { + options := Options{ + Width: 800, + Height: 600, + } + runBenchmarkResize("test.webp", options, b) +} + func BenchmarkExtractJpeg(b *testing.B) { options := Options{ Top: 100, @@ -226,3 +344,83 @@ func BenchmarkExtractJpeg(b *testing.B) { } runBenchmarkResize("test.jpg", options, b) } + +func BenchmarkExtractPng(b *testing.B) { + options := Options{ + Top: 100, + Left: 50, + AreaWidth: 600, + AreaHeight: 480, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkExtractWebp(b *testing.B) { + options := Options{ + Top: 100, + Left: 50, + AreaWidth: 600, + AreaHeight: 480, + } + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkZoomJpeg(b *testing.B) { + options := Options{Zoom: 1} + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkZoomPng(b *testing.B) { + options := Options{Zoom: 1} + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkZoomWebp(b *testing.B) { + options := Options{Zoom: 1} + runBenchmarkResize("test.webp", options, b) +} + +func BenchmarkWatermarkJpeg(b *testing.B) { + options := Options{ + Watermark: Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: Color{255, 255, 255}, + }, + } + runBenchmarkResize("test.jpg", options, b) +} + +func BenchmarkWatermarPng(b *testing.B) { + options := Options{ + Watermark: Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: Color{255, 255, 255}, + }, + } + runBenchmarkResize("test.png", options, b) +} + +func BenchmarkWatermarWebp(b *testing.B) { + options := Options{ + Watermark: Watermark{ + Text: "Chuck Norris (c) 2315", + Opacity: 0.25, + Width: 200, + DPI: 100, + Margin: 150, + Font: "sans bold 12", + Background: Color{255, 255, 255}, + }, + } + runBenchmarkResize("test.webp", options, b) +} diff --git a/type.go b/type.go index 295dc61..4077231 100644 --- a/type.go +++ b/type.go @@ -11,6 +11,15 @@ const ( MAGICK ) +// Pairs of image type and its name +var ImageTypes = map[ImageType]string{ + JPEG: "jpeg", + PNG: "png", + WEBP: "webp", + TIFF: "tiff", + MAGICK: "magick", +} + // Determines the image type format (jpeg, png, webp or tiff) func DetermineImageType(buf []byte) ImageType { return vipsImageType(buf) @@ -18,40 +27,28 @@ func DetermineImageType(buf []byte) ImageType { // Determines the image type format by name (jpeg, png, webp or tiff) func DetermineImageTypeName(buf []byte) string { - return getImageTypeName(vipsImageType(buf)) + return ImageTypeName(vipsImageType(buf)) } // Check if a given image type is supported func IsTypeSupported(t ImageType) bool { - return t == JPEG || t == PNG || t == WEBP + return ImageTypes[t] != "" } // Check if a given image type name is supported func IsTypeNameSupported(t string) bool { - return t == "jpeg" || t == "jpg" || - t == "png" || t == "webp" + for _, name := range ImageTypes { + if name == t { + return true + } + } + return false } -func getImageTypeName(code ImageType) string { - imageType := "unknown" - - switch { - case code == JPEG: - imageType = "jpeg" - break - case code == WEBP: - imageType = "webp" - break - case code == PNG: - imageType = "png" - break - case code == TIFF: - imageType = "tiff" - break - case code == MAGICK: - imageType = "magick" - break +func ImageTypeName(t ImageType) string { + imageType := ImageTypes[t] + if imageType == "" { + return "unknown" } - return imageType } diff --git a/type_test.go b/type_test.go index 534f738..68e6d21 100644 --- a/type_test.go +++ b/type_test.go @@ -68,7 +68,7 @@ func TestIsTypeNameSupported(t *testing.T) { name string expected bool }{ - {"jpg", true}, + {"jpeg", true}, {"png", true}, {"webp", true}, {"gif", false}, diff --git a/version.go b/version.go index 077f905..7372907 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package bimg -const Version = "0.1.5" +const Version = "0.1.21" diff --git a/vips.go b/vips.go index 27ff941..a2555d4 100644 --- a/vips.go +++ b/vips.go @@ -3,21 +3,32 @@ package bimg /* #cgo pkg-config: vips #include "vips.h" -#include "stdlib.h" */ import "C" import ( "errors" + "math" + "os" "runtime" "strings" "sync" "unsafe" ) +// Current libvips version +const VipsVersion = string(C.VIPS_VERSION) + +const HasMagickSupport = int(C.VIPS_MAGICK_SUPPORT) == 1 + +const ( + maxCacheMem = 100 * 1024 * 1024 + maxCacheSize = 500 +) + var ( m sync.Mutex - initialized bool = false + initialized bool ) type VipsMemoryInfo struct { @@ -27,9 +38,12 @@ type VipsMemoryInfo struct { } type vipsSaveOptions struct { - Quality int - Compression int - Type ImageType + Quality int + Compression int + Type ImageType + Interlace bool + NoProfile bool + Interpretation Interpretation } type vipsWatermarkOptions struct { @@ -47,16 +61,16 @@ type vipsWatermarkTextOptions struct { } func init() { - if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 { - panic("unsupported old vips version!") - } - Initialize() } // Explicit thread-safe start of libvips. // Only call this function if you've previously shutdown libvips func Initialize() { + if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 { + panic("unsupported libvips version!") + } + m.Lock() runtime.LockOSThread() defer m.Unlock() @@ -64,30 +78,42 @@ func Initialize() { err := C.vips_init(C.CString("bimg")) if err != 0 { - Shutdown() panic("unable to start vips!") } - C.vips_concurrency_set(0) // default - C.vips_cache_set_max_mem(100 * 1024 * 1024) // 100 MB - C.vips_cache_set_max(500) // 500 operations + // Set libvips cache params + C.vips_cache_set_max_mem(maxCacheMem) + C.vips_cache_set_max(maxCacheSize) + + // Define a custom thread concurrency limit in libvips (this may generate thread-unsafe issues) + // See: https://github.com/jcupitt/libvips/issues/261#issuecomment-92850414 + if os.Getenv("VIPS_CONCURRENCY") == "" { + C.vips_concurrency_set(1) + } + + // Enable libvips cache tracing + if os.Getenv("VIPS_TRACE") != "" { + C.vips_enable_cache_set_trace() + } + initialized = true } -// Explicit thread-safe libvips shutdown. Call this to drop caches. +// Thread-safe function to shutdown libvips. +// You can call this to drop caches as well. // If libvips was already initialized, the function is no-op func Shutdown() { m.Lock() defer m.Unlock() - if initialized == true { + if initialized { C.vips_shutdown() initialized = false } } // Output to stdout vips collected data. Useful for debugging -func VipsDebug() { +func VipsDebugInfo() { C.im__print_all() } @@ -100,28 +126,30 @@ func VipsMemory() VipsMemoryInfo { } } -func vipsExifOrientation(image *C.struct__VipsImage) int { +func vipsExifOrientation(image *C.VipsImage) int { return int(C.vips_exif_orientation(image)) } -func vipsHasAlpha(image *C.struct__VipsImage) bool { +func vipsHasAlpha(image *C.VipsImage) bool { return int(C.has_alpha_channel(image)) > 0 } -func vipsHasProfile(image *C.struct__VipsImage) bool { +func vipsHasProfile(image *C.VipsImage) bool { return int(C.has_profile_embed(image)) > 0 } func vipsWindowSize(name string) float64 { - return float64(C.interpolator_window_size(C.CString(name))) + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return float64(C.interpolator_window_size(cname)) } -func vipsSpace(image *C.struct__VipsImage) string { +func vipsSpace(image *C.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 +func vipsRotate(image *C.VipsImage, angle Angle) (*C.VipsImage, error) { + var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_rotate(image, &out, C.int(angle)) @@ -132,8 +160,8 @@ func vipsRotate(image *C.struct__VipsImage, angle Angle) (*C.struct__VipsImage, return out, nil } -func vipsFlip(image *C.struct__VipsImage, direction Direction) (*C.struct__VipsImage, error) { - var out *C.struct__VipsImage +func vipsFlip(image *C.VipsImage, direction Direction) (*C.VipsImage, error) { + var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_flip_bridge(image, &out, C.int(direction)) @@ -144,8 +172,8 @@ func vipsFlip(image *C.struct__VipsImage, direction Direction) (*C.struct__VipsI return out, nil } -func vipsZoom(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error) { - var out *C.struct__VipsImage +func vipsZoom(image *C.VipsImage, zoom int) (*C.VipsImage, error) { + var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_zoom_bridge(image, &out, C.int(zoom), C.int(zoom)) @@ -156,36 +184,8 @@ func vipsZoom(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error return out, nil } -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)) - - 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 +func vipsWatermark(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { + var out *C.VipsImage // Defaults noReplicate := 0 @@ -203,7 +203,7 @@ func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag 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))) + err := C.vips_watermark(image, &out, (*C.WatermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.WatermarkOptions)(unsafe.Pointer(&opts))) if err != 0 { return nil, catchVipsError() } @@ -211,8 +211,8 @@ func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag return out, nil } -func vipsRead(buf []byte) (*C.struct__VipsImage, ImageType, error) { - var image *C.struct__VipsImage +func vipsRead(buf []byte) (*C.VipsImage, ImageType, error) { + var image *C.VipsImage imageType := vipsImageType(buf) if imageType == UNKNOWN { @@ -230,46 +230,130 @@ func vipsRead(buf []byte) (*C.struct__VipsImage, ImageType, error) { return image, imageType, nil } -func vipsSave(image *C.struct__VipsImage, o vipsSaveOptions) ([]byte, error) { - var ptr unsafe.Pointer - length := C.size_t(0) - err := C.int(0) +func vipsColourspaceIsSupportedBuffer(buf []byte) (bool, error) { + image, _, err := vipsRead(buf) + if err != nil { + return false, err + } + C.g_object_unref(C.gpointer(image)) + return vipsColourspaceIsSupported(image), nil +} +func vipsColourspaceIsSupported(image *C.VipsImage) bool { + return int(C.vips_colourspace_issupported_bridge(image)) == 1 +} + +func vipsInterpretationBuffer(buf []byte) (Interpretation, error) { + image, _, err := vipsRead(buf) + if err != nil { + return INTERPRETATION_ERROR, err + } + C.g_object_unref(C.gpointer(image)) + return vipsInterpretation(image), nil +} + +func vipsInterpretation(image *C.VipsImage) Interpretation { + return Interpretation(C.vips_image_guess_interpretation_bridge(image)) +} + +func vipsFlattenBackground(image *C.VipsImage, background Color) (*C.VipsImage, error) { + var outImage *C.VipsImage + + backgroundC := [3]C.double{ + C.double(background.R), + C.double(background.G), + C.double(background.B), + } + + err := C.vips_flatten_background_brigde(image, &outImage, (*C.double)(&backgroundC[0])) + if int(err) != 0 { + return nil, catchVipsError() + } + + C.g_object_unref(C.gpointer(image)) + image = outImage + + return image, nil +} + +func vipsPreSave(image *C.VipsImage, o *vipsSaveOptions) (*C.VipsImage, error) { + // Remove ICC profile metadata + if o.NoProfile { + C.remove_profile(image) + } + + // Use a default interpretation and cast it to C type + if o.Interpretation == 0 { + o.Interpretation = INTERPRETATION_sRGB + } + interpretation := C.VipsInterpretation(o.Interpretation) + + // Apply the proper colour space + var outImage *C.VipsImage + if vipsColourspaceIsSupported(image) { + err := C.vips_colourspace_bridge(image, &outImage, interpretation) + if int(err) != 0 { + return nil, catchVipsError() + } + image = outImage + } + + return image, nil +} + +func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) { defer C.g_object_unref(C.gpointer(image)) - switch { - case o.Type == PNG: - err = C.vips_pngsave_bridge(image, &ptr, &length, 1, C.int(o.Compression), C.int(o.Quality), 0) + tmpImage, err := vipsPreSave(image, &o) + if err != nil { + return nil, err + } + defer C.g_object_unref(C.gpointer(tmpImage)) + + length := C.size_t(0) + saveErr := C.int(0) + interlace := C.int(boolToInt(o.Interlace)) + quality := C.int(o.Quality) + + var ptr unsafe.Pointer + switch o.Type { + case WEBP: + saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, 1, quality) break - case o.Type == WEBP: - err = C.vips_webpsave_bridge(image, &ptr, &length, 1, C.int(o.Quality), 0) + case PNG: + saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, 1, C.int(o.Compression), quality, interlace) break default: - err = C.vips_jpegsave_bridge(image, &ptr, &length, 1, C.int(o.Quality), 0) + saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, 1, quality, interlace) break } - if int(err) != 0 { + if int(saveErr) != 0 { return nil, catchVipsError() } buf := C.GoBytes(ptr, C.int(length)) - // Cleanup + // Clean up C.g_free(C.gpointer(ptr)) C.vips_error_clear() return buf, nil } -func vipsExtract(image *C.struct__VipsImage, left, top, width, height int) (*C.struct__VipsImage, error) { - var buf *C.struct__VipsImage +func max(x int) int { + return int(math.Max(float64(x), 0)) +} + +func vipsExtract(image *C.VipsImage, left, top, width, height int) (*C.VipsImage, error) { + var buf *C.VipsImage defer C.g_object_unref(C.gpointer(image)) if width > MAX_SIZE || height > MAX_SIZE { return nil, errors.New("Maximum image size exceeded") } + top, left = max(top), max(left) err := C.vips_extract_area_bridge(image, &buf, C.int(left), C.int(top), C.int(width), C.int(height)) if err != 0 { return nil, catchVipsError() @@ -278,11 +362,12 @@ func vipsExtract(image *C.struct__VipsImage, left, top, width, height int) (*C.s return buf, nil } -func vipsShrinkJpeg(buf []byte, input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, error) { - var image *C.struct__VipsImage +func vipsShrinkJpeg(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, error) { + var image *C.VipsImage + var ptr = unsafe.Pointer(&buf[0]) defer C.g_object_unref(C.gpointer(input)) - err := C.vips_jpegload_buffer_shrink(unsafe.Pointer(&buf[0]), C.size_t(len(buf)), &image, C.int(shrink)) + err := C.vips_jpegload_buffer_shrink(ptr, C.size_t(len(buf)), &image, C.int(shrink)) if err != 0 { return nil, catchVipsError() } @@ -290,8 +375,8 @@ func vipsShrinkJpeg(buf []byte, input *C.struct__VipsImage, shrink int) (*C.stru return image, nil } -func vipsShrink(input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, error) { - var image *C.struct__VipsImage +func vipsShrink(input *C.VipsImage, shrink int) (*C.VipsImage, error) { + var image *C.VipsImage defer C.g_object_unref(C.gpointer(input)) err := C.vips_shrink_bridge(input, &image, C.double(float64(shrink)), C.double(float64(shrink))) @@ -302,8 +387,8 @@ func vipsShrink(input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, e return image, nil } -func vipsEmbed(input *C.struct__VipsImage, left, top, width, height, extend int) (*C.struct__VipsImage, error) { - var image *C.struct__VipsImage +func vipsEmbed(input *C.VipsImage, left, top, width, height, extend int) (*C.VipsImage, error) { + var image *C.VipsImage defer C.g_object_unref(C.gpointer(input)) err := C.vips_embed_bridge(input, &image, C.int(left), C.int(top), C.int(width), C.int(height), C.int(extend)) @@ -314,16 +399,16 @@ func vipsEmbed(input *C.struct__VipsImage, left, top, width, height, extend int) return image, nil } -func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (*C.struct__VipsImage, error) { - var image *C.struct__VipsImage - istring := C.CString(i.String()) - interpolator := C.vips_interpolate_new(istring) +func vipsAffine(input *C.VipsImage, residualx, residualy float64, i Interpolator) (*C.VipsImage, error) { + var image *C.VipsImage + cstring := C.CString(i.String()) + interpolator := C.vips_interpolate_new(cstring) - defer C.free(unsafe.Pointer(istring)) + defer C.free(unsafe.Pointer(cstring)) defer C.g_object_unref(C.gpointer(input)) defer C.g_object_unref(C.gpointer(interpolator)) - err := C.vips_affine_interpolator(input, &image, C.double(residual), 0, 0, C.double(residual), interpolator) + err := C.vips_affine_interpolator(input, &image, C.double(residualx), 0, 0, C.double(residualy), interpolator) if err != 0 { return nil, catchVipsError() } @@ -331,36 +416,37 @@ func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (* return image, nil } -func vipsImageType(buf []byte) ImageType { - imageType := UNKNOWN - - if len(buf) == 0 { - return imageType +func vipsImageType(bytes []byte) ImageType { + if len(bytes) == 0 { + return UNKNOWN } + if bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47 { + return PNG + } + if bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF { + return JPEG + } + if bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50 { + return WEBP + } + if (bytes[0] == 0x49 && bytes[1] == 0x49 && bytes[2] == 0x2A && bytes[3] == 0x0) || + (bytes[0] == 0x4D && bytes[1] == 0x4D && bytes[2] == 0x0 && bytes[3] == 0x2A) { + return TIFF + } + if HasMagickSupport && strings.HasSuffix(readImageType(bytes), "MagickBuffer") { + return MAGICK + } + + return UNKNOWN +} + +func readImageType(buf []byte) string { length := C.size_t(len(buf)) imageBuf := unsafe.Pointer(&buf[0]) - bufferType := C.GoString(C.vips_foreign_find_load_buffer(imageBuf, length)) - - switch { - case strings.HasSuffix(bufferType, "JpegBuffer"): - imageType = JPEG - break - case strings.HasSuffix(bufferType, "PngBuffer"): - imageType = PNG - break - case strings.HasSuffix(bufferType, "TiffBuffer"): - imageType = TIFF - break - case strings.HasSuffix(bufferType, "WebpBuffer"): - imageType = WEBP - break - case strings.HasSuffix(bufferType, "MagickBuffer"): - imageType = MAGICK - break - } - - return imageType + load := C.vips_foreign_find_load_buffer(imageBuf, length) + defer C.free(imageBuf) + return C.GoString(load) } func catchVipsError() error { @@ -369,3 +455,32 @@ func catchVipsError() error { C.vips_thread_shutdown() return errors.New(s) } + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func vipsGaussianBlur(image *C.VipsImage, o GaussianBlur) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + err := C.vips_gaussblur_bridge(image, &out, C.double(o.Sigma), C.double(o.MinAmpl)) + if err != 0 { + return nil, catchVipsError() + } + return out, nil +} + +func vipsSharpen(image *C.VipsImage, o Sharpen) (*C.VipsImage, error) { + var out *C.VipsImage + defer C.g_object_unref(C.gpointer(image)) + + err := C.vips_sharpen_bridge(image, &out, C.int(o.Radius), C.double(o.X1), C.double(o.Y2), C.double(o.Y3), C.double(o.M1), C.double(o.M2)) + if err != 0 { + return nil, catchVipsError() + } + return out, nil +} diff --git a/vips.h b/vips.h index 0fa7cf9..1eecdc3 100644 --- a/vips.h +++ b/vips.h @@ -2,6 +2,28 @@ #include #include +#ifdef VIPS_MAGICK_H +#define VIPS_MAGICK_SUPPORT 1 +#else +#define VIPS_MAGICK_SUPPORT 0 +#endif + +/** + * Starting libvips 7.41, VIPS_ANGLE_x has been renamed to VIPS_ANGLE_Dx + * "to help python". So we provide the macro to correctly build for versions + * before 7.41.x. + * https://github.com/jcupitt/libvips/blob/master/ChangeLog#L128 + */ + +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) +#define VIPS_ANGLE_D0 VIPS_ANGLE_0 +#define VIPS_ANGLE_D90 VIPS_ANGLE_90 +#define VIPS_ANGLE_D180 VIPS_ANGLE_180 +#define VIPS_ANGLE_D270 VIPS_ANGLE_270 +#endif + +#define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation" + enum types { UNKNOWN = 0, JPEG, @@ -12,46 +34,91 @@ enum types { }; typedef struct { - char *Text; - char *Font; -} watermarkTextOptions; + const char *Text; + const char *Font; +} WatermarkTextOptions; typedef struct { - int Width; - int DPI; - int Margin; - int NoReplicate; - float Opacity; + int Width; + int DPI; + int Margin; + int NoReplicate; + float Opacity; double Background[3]; -} watermarkOptions; +} WatermarkOptions; + +static int +has_profile_embed(VipsImage *image) { + return vips_image_get_typeof(image, VIPS_META_ICC_NAME); +} + +static void +remove_profile(VipsImage *image) { + vips_image_remove(image, VIPS_META_ICC_NAME); +} + +static gboolean +with_interlace(int interlace) { + return interlace > 0 ? TRUE : FALSE; +} + +static int +has_alpha_channel(VipsImage *image) { + return ( + (image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) || + (image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) || + (image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK) + ) ? 1 : 0; +} + +/** + * This method is here to handle the weird initialization of the vips lib. + * libvips use a macro VIPS_INIT() that call vips__init() in version < 7.41, + * or calls vips_init() in version >= 7.41. + * + * Anyway, it's not possible to build bimg on Debian Jessie with libvips 7.40.x, + * as vips_init() is a macro to VIPS_INIT(), which is also a macro, hence, cgo + * is unable to determine the return type of vips_init(), making the build impossible. + * In order to correctly build bimg, for version < 7.41, we should undef vips_init and + * creates a vips_init() method that calls VIPS_INIT(). + */ + +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) +#undef vips_init +int +vips_init(const char *argv0) +{ + return VIPS_INIT(argv0); +} +#endif + +void +vips_enable_cache_set_trace() { + vips_cache_set_trace(TRUE); +} int -vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) -{ +vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) { return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL); -}; +} int -vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) -{ +vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL); -}; +} int -vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) -{ +vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) { return vips_flip(in, out, direction, NULL); -}; +} int -vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) -{ +vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { return vips_shrink(in, out, xshrink, yshrink, NULL); -}; +} int -vips_rotate(VipsImage *in, VipsImage **buf, int angle) -{ +vips_rotate(VipsImage *in, VipsImage **out, int angle) { int rotate = VIPS_ANGLE_D0; if (angle == 90) { @@ -62,35 +129,21 @@ vips_rotate(VipsImage *in, VipsImage **buf, int angle) rotate = VIPS_ANGLE_D270; } - return vips_rot(in, buf, rotate, NULL); -}; + return vips_rot(in, out, rotate, NULL); +} int vips_exif_orientation(VipsImage *image) { int orientation = 0; - const char **exif; + const char *exif; if ( - vips_image_get_typeof(image, "exif-ifd0-Orientation") != 0 && - !vips_image_get_string(image, "exif-ifd0-Orientation", exif) + vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 && + !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif) ) { - orientation = atoi(exif[0]); + orientation = atoi(&exif[0]); } return orientation; -}; - -int -has_profile_embed(VipsImage *image) { - return (vips_image_get_typeof(image, VIPS_META_ICC_NAME) > 0) ? 1 : 0; -}; - -int -has_alpha_channel(VipsImage *image) { - return ( - (image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) || - (image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) || - (image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK) - ) ? 1 : 0; -}; +} int interpolator_window_size(char const *name) { @@ -98,155 +151,202 @@ interpolator_window_size(char const *name) { int window_size = vips_interpolate_get_window_size(interpolator); g_object_unref(interpolator); return window_size; -}; +} const char * vips_enum_nick_bridge(VipsImage *image) { return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); -}; +} int -vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) -{ +vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) { return vips_zoom(in, out, xfac, yfac, NULL); -}; +} int -vips_colorspace_bridge(VipsImage *in, VipsImage **out) -{ - return vips_colourspace(in, out, VIPS_INTERPRETATION_LAB, NULL); -}; - -int -vips_hist_find_ndim_bridge(VipsImage *in, VipsImage **out) -{ - 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_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend) -{ +vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend) { return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); -}; +} int -vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) -{ +vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) { return vips_extract_area(in, out, left, top, width, height, NULL); -}; +} int -vips_jpegsave_bridge(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); -}; +vips_colourspace_issupported_bridge(VipsImage *in) { + return vips_colourspace_issupported(in) ? 1 : 0; +} + +VipsInterpretation +vips_image_guess_interpretation_bridge(VipsImage *in) { + return vips_image_guess_interpretation(in); +} int -vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) -{ +vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) { + return vips_colourspace(in, out, space, NULL); +} + +int +vips_jpegsave_bridge(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", with_interlace(interlace), + NULL + ); +} + +int +vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) { #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)) - return vips_pngsave_buffer(in, buf, len, "strip", FALSE, "compression", compression, - "interlace", interlace, "filter", VIPS_FOREIGN_PNG_FILTER_NONE, NULL); + return vips_pngsave_buffer(in, buf, len, + "strip", FALSE, + "compression", compression, + "interlace", with_interlace(interlace), + "filter", VIPS_FOREIGN_PNG_FILTER_NONE, + NULL + ); #else - return vips_pngsave_buffer(in, buf, len, "strip", FALSE, "compression", compression, - "interlace", interlace, NULL); + return vips_pngsave_buffer(in, buf, len, + "strip", FALSE, + "compression", compression, + "interlace", with_interlace(interlace), + NULL + ); #endif -}; +} int -vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) -{ - return vips_webpsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL); -}; +vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality) { + return vips_webpsave_buffer(in, buf, len, + "strip", strip, + "Q", quality, + NULL + ); +} int -vips_init_image(void *buf, size_t len, int imageType, VipsImage **out) { +vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double background[3]) { + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); + return vips_flatten(in, out, + "background", vipsBackground, + NULL + ); +} + +int +vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) { int code = 1; if (imageType == JPEG) { - code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); + code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); } else if (imageType == PNG) { - code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); + code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); } else if (imageType == WEBP) { - code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); + code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); } else if (imageType == TIFF) { - code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); + code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); #if (VIPS_MAJOR_VERSION >= 8) } else if (imageType == MAGICK) { - code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); + code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); #endif } return code; -}; +} int -vips_watermark(VipsImage *in, VipsImage **out, watermarkTextOptions *to, watermarkOptions *o) -{ - double ones[3] = { 1, 1, 1 }; +vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out) { + VipsImage *cache = vips_image_new(); - VipsImage *base = vips_image_new(); - VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 12); - t[0] = in; + if ( + vips_replicate(in, &cache, + 1 + orig->Xsize / in->Xsize, + 1 + orig->Ysize / in->Ysize, NULL) || + vips_crop(cache, out, 0, 0, orig->Xsize, orig->Ysize, NULL) + ) { + g_object_unref(cache); + return 1; + } - // 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); - } + g_object_unref(cache); + return 0; +} - // 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); - } +int +vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o) { + double ones[3] = { 1, 1, 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); - } + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); + t[0] = in; - // 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); - } + // 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; + } - g_object_unref(base); - return (0); -}; + // Replicate if necessary + if (o->NoReplicate != 1) { + VipsImage *cache = vips_image_new(); + if (vips_watermark_replicate(t[0], t[4], &cache)) { + g_object_unref(cache); + g_object_unref(base); + return 1; + } + g_object_unref(t[4]); + t[4] = cache; + } + + // Make the constant image to paint the text with. + if ( + vips_black(&t[5], 1, 1, NULL) || + vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) || + vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) || + vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) || + vips_embed(t[8], &t[9], 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[4], t[9], t[0], out, "blend", TRUE, NULL)) { + g_object_unref(base); + return 1; + } + + g_object_unref(base); + return 0; +} + +int +vips_gaussblur_bridge(VipsImage *in, VipsImage **out, double sigma, double min_ampl) { +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) + return vips_gaussblur(in, out, (int) sigma, NULL); +#else + return vips_gaussblur(in, out, sigma, NULL, "min_ampl", min_ampl, NULL); +#endif +} + +int +vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, double y2, double y3, double m1, double m2) { +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) + return vips_sharpen(in, out, radius, x1, y2, y3, m1, m2, NULL); +#else + return vips_sharpen(in, out, "radius", radius, "x1", x1, "y2", y2, "y3", y3, "m1", m1, "m2", m2, NULL); +#endif +} diff --git a/vips_test.go b/vips_test.go index 533e810..da5cea5 100644 --- a/vips_test.go +++ b/vips_test.go @@ -18,31 +18,19 @@ func TestVipsRead(t *testing.T) { } for _, file := range files { - img, _ := os.Open(path.Join("fixtures", file.name)) - buf, _ := ioutil.ReadAll(img) - defer img.Close() - - image, imageType, _ := vipsRead(buf) + image, imageType, _ := vipsRead(readImage(file.name)) if image == nil { t.Fatal("Empty image") } if imageType != file.expected { - t.Fatal("Empty image") + t.Fatal("Invalid image type") } } } func TestVipsSave(t *testing.T) { - img, _ := os.Open(path.Join("fixtures", "test.jpg")) - buf, _ := ioutil.ReadAll(img) - defer img.Close() - - image, _, _ := vipsRead(buf) - if image == nil { - t.Fatal("Empty image") - } - - options := vipsSaveOptions{Quality: 95, Type: JPEG} + image, _, _ := vipsRead(readImage("test.jpg")) + options := vipsSaveOptions{Quality: 95, Type: JPEG, Interlace: true} buf, err := vipsSave(image, options) if err != nil { @@ -52,3 +40,80 @@ func TestVipsSave(t *testing.T) { t.Fatal("Empty image") } } + +func TestVipsRotate(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + newImg, err := vipsRotate(image, D90) + if err != nil { + t.Fatal("Cannot save the image") + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsZoom(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + newImg, err := vipsZoom(image, 1) + if err != nil { + t.Fatal("Cannot save the image") + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsWatermark(t *testing.T) { + image, _, _ := vipsRead(readImage("test.jpg")) + + watermark := Watermark{ + Text: "Copy me if you can", + Font: "sans bold 12", + Opacity: 0.5, + Width: 200, + DPI: 100, + Margin: 100, + Background: Color{255, 255, 255}, + } + + newImg, err := vipsWatermark(image, watermark) + if err != nil { + t.Errorf("Cannot add watermark: %s", err) + } + + buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95}) + if len(buf) == 0 { + t.Fatal("Empty image") + } +} + +func TestVipsImageType(t *testing.T) { + imgType := vipsImageType(readImage("test.jpg")) + if imgType != JPEG { + t.Fatal("Invalid image type") + } +} + +func TestVipsMemory(t *testing.T) { + mem := VipsMemory() + + if mem.Memory < 1024 { + t.Fatal("Invalid memory") + } + if mem.Allocations == 0 { + t.Fatal("Invalid memory allocations") + } +} + +func readImage(file string) []byte { + img, _ := os.Open(path.Join("fixtures", file)) + buf, _ := ioutil.ReadAll(img) + defer img.Close() + return buf +}