Merge pull request #67 from h2non/master

merge(master): update develop branch
master
Tomás Aparicio 10 years ago
commit 099bc57753

@ -5,9 +5,4 @@ go:
- release - release
- tip - tip
before_install: 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 - - curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

@ -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. 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 uses internally libvips, which requires a [low memory footprint](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use)
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. 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, zoom, watermark... and conversion between multiple formats. 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](http://nodejs.org).
For getting started, take a look to the [examples](#examples) and [programmatic API](https://godoc.org/github.com/h2non/bimg) documentation. ## Contents
bimg was heavily inspired in [sharp](https://github.com/lovell/sharp), - [Supported image operations](#supported-image-operations)
its homologous package built for node.js by [Lovell Fuller](https://github.com/lovell). - [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Performance](#performance)
- [Benchmark](#benchmark)
- [Examples](#examples)
- [Debugging](#debugging)
- [API](#api)
- [Credits](#credits)
## Supported image operations
**Note**: bimg is still beta. Pull request and issues are highly appreciated - 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 ## 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 - curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
``` ```
The [install script](https://github.com/lovell/sharp/blob/master/preinstall.sh) requires `curl` and `pkg-config` 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
```
## Supported image operations The [install script](https://github.com/lovell/sharp/blob/master/preinstall.sh) requires `curl` and `pkg-config`
- Resize 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
- 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...)
## Performance ## 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) - [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) - [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 BenchmarkRotateJpeg-8 20 64686945 ns/op
BenchmarkResizeLargeJpeg 30 46652408 ns/op BenchmarkResizeLargeJpeg-8 20 63390416 ns/op
BenchmarkResizePng 20 57387902 ns/op BenchmarkResizePng-8 100 18147294 ns/op
BenchmarkResizeWebP 500 2453220 ns/op BenchmarkResizeWebP-8 100 20836741 ns/op
BenchmarkConvertToJpeg 30 35556414 ns/op BenchmarkConvertToJpeg-8 100 12831812 ns/op
BenchmarkCrop 30 51768475 ns/op BenchmarkConvertToPng-8 10 128901422 ns/op
BenchmarkExtract 30 50866406 ns/op BenchmarkConvertToWebp-8 10 204027990 ns/op
ok 9.424s 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 ```go
import ( 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 #### Custom options
See [Options](https://godoc.org/github.com/h2non/bimg#Options) struct to discover all the available fields 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, Crop: true,
Quality: 95, Quality: 95,
Rotate: 180, Rotate: 180,
Interlace: true,
} }
buffer, err := bimg.Read("image.jpg") buffer, err := bimg.Read("image.jpg")
@ -174,9 +241,8 @@ if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
} }
options := bimg.Watermark{ watermark := bimg.Watermark{
Watermark{ Text: "Chuck Norris (c) 2315",
Text: "Chuck Norris - Copyright (c) 2315",
Opacity: 0.25, Opacity: 0.25,
Width: 200, Width: 200,
DPI: 100, DPI: 100,
@ -184,9 +250,8 @@ options := bimg.Watermark{
Font: "sans bold 12", Font: "sans bold 12",
Background: bimg.Color{255, 255, 255}, Background: bimg.Color{255, 255, 255},
} }
}
newImage, err := bimg.NewImage(buffer).Watermark() newImage, err := bimg.NewImage(buffer).Watermark(watermark)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
} }
@ -220,349 +285,36 @@ if err != nil {
bimg.Write("new.jpg", newImage) bimg.Write("new.jpg", newImage)
``` ```
#### func DetermineImageTypeName ## Debugging
```go
func DetermineImageTypeName(buf []byte) string
```
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)
```
#### func Resize
```go
func Resize(buf []byte, o Options) ([]byte, error)
```
#### func Shutdown
```go
func Shutdown()
```
Explicit thread-safe libvips shutdown. Call this to drop caches. If libvips was
already initialized, the function is no-op
#### func VipsDebug
```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 Run the process passing the `DEBUG` environment variable
type Direction int
``` ```
DEBUG=bimg ./app
```go
const (
HORIZONTAL Direction = C.VIPS_DIRECTION_HORIZONTAL
VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL
)
``` ```
#### type Gravity Enable libvips traces (note that a lot of data will be written in stdout):
```go
type Gravity int
``` ```
VIPS_TRACE=1 ./app
```go
const (
CENTRE Gravity = iota
NORTH
EAST
SOUTH
WEST
)
``` ```
#### type Image ## API
```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 See [godoc reference](https://godoc.org/github.com/h2non/bimg) for detailed API documentation.
```go ## Credits
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) - [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 ## License
MIT - Tomas Aparicio MIT - Tomas Aparicio
[![views](https://sourcegraph.com/api/repos/github.com/h2non/bimg/.counters/views.svg)](https://sourcegraph.com/github.com/h2non/bimg)

@ -1,62 +1,5 @@
package bimg package bimg
import ( import . "github.com/tj/go-debug"
"github.com/dustin/go-humanize"
. "github.com/tj/go-debug"
"runtime"
"strconv"
"time"
)
var debug = Debug("bimg") 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
}

@ -1,23 +1,9 @@
package bimg package bimg
import ( import "io/ioutil"
"io/ioutil"
"os"
)
func Read(path string) ([]byte, error) { func Read(path string) ([]byte, error) {
file, err := os.Open(path) return ioutil.ReadFile(path)
if err != nil {
return nil, err
}
defer file.Close()
buf, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return buf, nil
} }
func Write(path string, buf []byte) error { func Write(path string, buf []byte) error {

@ -4,6 +4,11 @@ type Image struct {
buffer []byte buffer []byte
} }
// Creates a new image
func NewImage(buf []byte) *Image {
return &Image{buf}
}
// Resize the image to fixed width and height // Resize the image to fixed width and height
func (i *Image) Resize(width, height int) ([]byte, error) { func (i *Image) Resize(width, height int) ([]byte, error) {
options := Options{ options := Options{
@ -14,6 +19,27 @@ func (i *Image) Resize(width, height int) ([]byte, error) {
return i.Process(options) 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 // Extract area from the by X/Y axis
func (i *Image) Extract(top, left, width, height int) ([]byte, error) { func (i *Image) Extract(top, left, width, height int) ([]byte, error) {
options := Options{ options := Options{
@ -22,10 +48,15 @@ func (i *Image) Extract(top, left, width, height int) ([]byte, error) {
AreaWidth: width, AreaWidth: width,
AreaHeight: height, AreaHeight: height,
} }
if top == 0 && left == 0 {
options.Top = -1
}
return i.Process(options) 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) { func (i *Image) Enlarge(width, height int) ([]byte, error) {
options := Options{ options := Options{
Width: width, Width: width,
@ -35,6 +66,17 @@ func (i *Image) Enlarge(width, height int) ([]byte, error) {
return i.Process(options) 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 // Crop the image to the exact size specified
func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) { func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) {
options := Options{ options := Options{
@ -75,15 +117,16 @@ func (i *Image) Thumbnail(pixels int) ([]byte, error) {
return i.Process(options) 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) { func (i *Image) Watermark(w Watermark) ([]byte, error) {
options := Options{Watermark: w} options := Options{Watermark: w}
return i.Process(options) return i.Process(options)
} }
// Zoom the image by the given factor // Zoom the image by the given factor.
func (i *Image) Zoom(level int) ([]byte, error) { // You should probably call Extract() before
options := Options{Zoom: level} func (i *Image) Zoom(factor int) ([]byte, error) {
options := Options{Zoom: factor}
return i.Process(options) return i.Process(options)
} }
@ -111,6 +154,12 @@ func (i *Image) Convert(t ImageType) ([]byte, error) {
return i.Process(options) 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 // Transform the image by custom options
func (i *Image) Process(o Options) ([]byte, error) { func (i *Image) Process(o Options) ([]byte, error) {
image, err := Resize(i.buffer, o) image, err := Resize(i.buffer, o)
@ -126,6 +175,17 @@ func (i *Image) Metadata() (ImageMetadata, error) {
return Metadata(i.buffer) 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) // Get image type format (jpeg, png, webp, tiff)
func (i *Image) Type() string { func (i *Image) Type() string {
return DetermineImageTypeName(i.buffer) return DetermineImageTypeName(i.buffer)
@ -140,8 +200,3 @@ func (i *Image) Size() (ImageSize, error) {
func (i *Image) Image() []byte { func (i *Image) Image() []byte {
return i.buffer return i.buffer
} }
// Creates a new image
func NewImage(buf []byte) *Image {
return &Image{buf}
}

@ -20,13 +20,27 @@ func TestImageResize(t *testing.T) {
Write("fixtures/test_resize_out.jpg", buf) Write("fixtures/test_resize_out.jpg", buf)
} }
func TestImageExtract(t *testing.T) { func TestImageResizeAndCrop(t *testing.T) {
buf, err := initImage("test.jpg").Extract(100, 100, 300, 300) buf, err := initImage("test.jpg").ResizeAndCrop(300, 200)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) 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 { if err != nil {
t.Error(err) t.Error(err)
} }
@ -34,6 +48,20 @@ func TestImageExtract(t *testing.T) {
Write("fixtures/test_extract_out.jpg", buf) 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) { func TestImageEnlarge(t *testing.T) {
buf, err := initImage("test.png").Enlarge(500, 375) buf, err := initImage("test.png").Enlarge(500, 375)
if err != nil { if err != nil {
@ -48,10 +76,24 @@ func TestImageEnlarge(t *testing.T) {
Write("fixtures/test_enlarge_out.jpg", buf) 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) { func TestImageCrop(t *testing.T) {
buf, err := initImage("test.jpg").Crop(800, 600, NORTH) buf, err := initImage("test.jpg").Crop(800, 600, NORTH)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %s", err)
} }
err = assertSize(buf, 800, 600) err = assertSize(buf, 800, 600)
@ -65,7 +107,7 @@ func TestImageCrop(t *testing.T) {
func TestImageCropByWidth(t *testing.T) { func TestImageCropByWidth(t *testing.T) {
buf, err := initImage("test.jpg").CropByWidth(600) buf, err := initImage("test.jpg").CropByWidth(600)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %s", err)
} }
err = assertSize(buf, 600, 375) err = assertSize(buf, 600, 375)
@ -79,7 +121,7 @@ func TestImageCropByWidth(t *testing.T) {
func TestImageCropByHeight(t *testing.T) { func TestImageCropByHeight(t *testing.T) {
buf, err := initImage("test.jpg").CropByHeight(300) buf, err := initImage("test.jpg").CropByHeight(300)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %s", err)
} }
err = assertSize(buf, 480, 300) err = assertSize(buf, 480, 300)
@ -93,7 +135,7 @@ func TestImageCropByHeight(t *testing.T) {
func TestImageThumbnail(t *testing.T) { func TestImageThumbnail(t *testing.T) {
buf, err := initImage("test.jpg").Thumbnail(100) buf, err := initImage("test.jpg").Thumbnail(100)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %s", err)
} }
err = assertSize(buf, 100, 100) err = assertSize(buf, 100, 100)
@ -134,13 +176,51 @@ func TestImageWatermark(t *testing.T) {
Write("fixtures/test_watermark_out.jpg", buf) Write("fixtures/test_watermark_out.jpg", buf)
} }
func TestImageWatermarkNoReplicate(t *testing.T) {
image := initImage("test.jpg")
_, err := image.Crop(800, 600, NORTH)
if err != nil {
t.Errorf("Cannot process the image: %s", err)
}
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) { func TestImageZoom(t *testing.T) {
buf, err := initImage("test.jpg").Zoom(1) image := initImage("test.jpg")
_, err := image.Extract(100, 100, 400, 300)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) 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, 3360, 2100) err = assertSize(buf, 800, 600)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -180,6 +260,19 @@ func TestImageConvert(t *testing.T) {
Write("fixtures/test_image_convert_out.png", buf) 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) { func TestImageMetadata(t *testing.T) {
data, err := initImage("test.png").Metadata() data, err := initImage("test.png").Metadata()
if err != nil { 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) { func TestFluentInterface(t *testing.T) {
image := initImage("test.jpg") image := initImage("test.jpg")
_, err := image.CropByWidth(300) _, err := image.CropByWidth(300)

@ -18,6 +18,7 @@ type ImageMetadata struct {
Profile bool Profile bool
Type string Type string
Space string Space string
Colourspace string
Size ImageSize Size ImageSize
} }
@ -34,6 +35,17 @@ func Size(buf []byte) (ImageSize, error) {
}, nil }, 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...) // Extract the image metadata (size, type, alpha channel, profile, EXIF orientation...)
func Metadata(buf []byte) (ImageMetadata, error) { func Metadata(buf []byte) (ImageMetadata, error) {
defer C.vips_thread_shutdown() defer C.vips_thread_shutdown()
@ -56,7 +68,7 @@ func Metadata(buf []byte) (ImageMetadata, error) {
Alpha: vipsHasAlpha(image), Alpha: vipsHasAlpha(image),
Profile: vipsHasProfile(image), Profile: vipsHasProfile(image),
Space: vipsSpace(image), Space: vipsSpace(image),
Type: getImageTypeName(imageType), Type: ImageTypeName(imageType),
} }
return metadata, nil return metadata, nil

@ -17,7 +17,6 @@ func TestSize(t *testing.T) {
{"test.png", 400, 300}, {"test.png", 400, 300},
{"test.webp", 550, 368}, {"test.webp", 550, 368},
} }
for _, file := range files { for _, file := range files {
size, err := Size(readFile(file.name)) size, err := Size(readFile(file.name))
if err != nil { if err != nil {
@ -39,15 +38,15 @@ func TestMetadata(t *testing.T) {
profile bool profile bool
space string space string
}{ }{
{"test.jpg", "jpeg", 0, false, false, "bicubic"}, {"test.jpg", "jpeg", 0, false, false, "srgb"},
{"test.png", "png", 0, true, false, "bicubic"}, {"test.png", "png", 0, true, false, "srgb"},
{"test.webp", "webp", 0, false, false, "bicubic"}, {"test.webp", "webp", 0, false, false, "srgb"},
} }
for _, file := range files { for _, file := range files {
metadata, err := Metadata(readFile(file.name)) metadata, err := Metadata(readFile(file.name))
if err != nil { 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 { if metadata.Type != file.format {
@ -62,6 +61,58 @@ func TestMetadata(t *testing.T) {
if metadata.Profile != file.profile { if metadata.Profile != file.profile {
t.Fatalf("Unexpected image profile: %s != %s", 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")
} }
} }

@ -42,10 +42,10 @@ func (i Interpolator) String() string {
type Angle int type Angle int
const ( const (
D0 Angle = C.VIPS_ANGLE_D0 D0 Angle = 0
D90 Angle = C.VIPS_ANGLE_D90 D90 Angle = 90
D180 Angle = C.VIPS_ANGLE_D180 D180 Angle = 180
D270 Angle = C.VIPS_ANGLE_D270 D270 Angle = 270
) )
type Direction int type Direction int
@ -55,11 +55,35 @@ const (
VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL 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 // Color represents a traditional RGB color scheme
type Color struct { type Color struct {
R, G, B uint8 R, G, B uint8
} }
// Shortcut to black RGB color representation
var ColorBlack = Color{0, 0, 0}
// Text-based watermark configuration
type Watermark struct { type Watermark struct {
Width int Width int
DPI int DPI int
@ -71,6 +95,21 @@ type Watermark struct {
Background Color Background Color
} }
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 { type Options struct {
Height int Height int
Width int Width int
@ -87,11 +126,17 @@ type Options struct {
Embed bool Embed bool
Flip bool Flip bool
Flop bool Flop bool
Force bool
NoAutoRotate bool NoAutoRotate bool
Colorspace bool NoProfile bool
Interlace bool
Rotate Angle Rotate Angle
Background Color
Gravity Gravity Gravity Gravity
Watermark Watermark Watermark Watermark
Type ImageType Type ImageType
Interpolator Interpolator Interpolator Interpolator
Interpretation Interpretation
GaussianBlur GaussianBlur
Sharpen Sharpen
} }

@ -23,16 +23,8 @@ func Resize(buf []byte, o Options) ([]byte, error) {
return nil, err return nil, err
} }
// Defaults // Clone and define default options
if o.Quality == 0 { o = applyDefaults(o, imageType)
o.Quality = QUALITY
}
if o.Compression == 0 {
o.Compression = 6
}
if o.Type == 0 {
o.Type = imageType
}
if IsTypeSupported(o.Type) == false { if IsTypeSupported(o.Type) == false {
return nil, errors.New("Unsupported image output type") return nil, errors.New("Unsupported image output type")
@ -43,13 +35,17 @@ func Resize(buf []byte, o Options) ([]byte, error) {
inWidth := int(image.Xsize) inWidth := int(image.Xsize)
inHeight := int(image.Ysize) 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 // image calculations
factor := imageCalculations(&o, inWidth, inHeight) factor := imageCalculations(&o, inWidth, inHeight)
shrink := int(math.Max(math.Floor(factor), 1)) shrink := calculateShrink(factor, o.Interpolator)
residual := float64(shrink) / factor residual := calculateResidual(factor, shrink)
// Do not enlarge the output if the input width *or* height are already less than the required dimensions // Do not enlarge the output if the input width or height
if o.Enlarge == false { // are already less than the required dimensions
if !o.Enlarge && !o.Force {
if inWidth < o.Width && inHeight < o.Height { if inWidth < o.Width && inHeight < o.Height {
factor = 1.0 factor = 1.0
shrink = 1 shrink = 1
@ -65,55 +61,49 @@ func Resize(buf []byte, o Options) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if tmpImage != nil {
image = tmpImage image = tmpImage
factor = math.Max(factor, 1.0) factor = math.Max(factor, 1.0)
shrink = int(math.Floor(factor)) shrink = int(math.Floor(factor))
residual = float64(shrink) / factor residual = float64(shrink) / factor
} }
}
// Calculate integral box shrink // Zoom image, if necessary
windowSize := vipsWindowSize(o.Interpolator.String()) image, err = zoomImage(image, o.Zoom)
if factor >= 2 && windowSize > 3 { if err != nil {
// Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic return nil, err
shrink = int(math.Max(float64(math.Floor(factor*3.0/windowSize)), 1))
} }
// Transform image if necessary // Rotate / flip image, if necessary
shouldTransform := o.Width != inWidth || o.Height != inHeight || o.AreaWidth > 0 || o.AreaHeight > 0 image, err = rotateAndFlipImage(image, o)
if shouldTransform {
// Use vips_shrink with the integral reduction
if shrink > 1 {
image, residual, err = shrinkImage(image, o, residual, shrink)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
// Use vips_affine with the remaining float part // Transform image, if necessary
if residual != 0 { if shouldTransformImage(o, inWidth, inHeight) {
image, err = vipsAffine(image, residual, o.Interpolator) image, err = transformImage(image, o, shrink, residual)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
debug("Transform image: factor=%v, shrink=%v, residual=%v", factor, shrink, residual) // Apply effects, if necessary
// Extract area from image if shouldApplyEffects(o) {
image, err = extractImage(image, o) image, err = applyEffects(image, o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
// Zoom image if necessary // Add watermark, if necessary
image, err = zoomImage(image, o.Zoom) image, err = watermakImage(image, o.Watermark)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Rotate / flip image if necessary // Flatten image on a background, if necessary
image, err = rotateImage(image, o) image, err = imageFlatten(image, imageType, o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -122,24 +112,110 @@ func Resize(buf []byte, o Options) ([]byte, error) {
Quality: o.Quality, Quality: o.Quality,
Type: o.Type, Type: o.Type,
Compression: o.Compression, Compression: o.Compression,
Interlace: o.Interlace,
NoProfile: o.NoProfile,
Interpretation: o.Interpretation,
} }
// watermark // Finally get the resultant buffer
image, err = watermakImage(image, o.Watermark) return vipsSave(image, saveOptions)
}
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 { if err != nil {
return nil, err return nil, err
} }
}
// Finally save as buffer if o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 {
buf, err = vipsSave(image, saveOptions) image, err = vipsSharpen(image, o.Sharpen)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
debug("Effects: gaussSigma=%v, gaussMinAmpl=%v, sharpenRadius=%v",
o.GaussianBlur.Sigma, o.GaussianBlur.MinAmpl, o.Sharpen.Radius)
return buf, nil return image, nil
} }
func extractImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, error) { func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) {
var err error = nil var err error = nil
inWidth := int(image.Xsize) inWidth := int(image.Xsize)
inHeight := int(image.Ysize) 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))) width := int(math.Min(float64(inWidth), float64(o.Width)))
height := int(math.Min(float64(inHeight), float64(o.Height))) height := int(math.Min(float64(inHeight), float64(o.Height)))
left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity) 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) image, err = vipsExtract(image, left, top, width, height)
break break
case o.Embed: case o.Embed:
left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2
image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend) image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend)
break break
case o.Top > 0 || o.Left > 0: 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 { if o.AreaWidth == 0 || o.AreaHeight == 0 {
err = errors.New("Area to extract cannot be 0") return nil, errors.New("Extract area width/height params are required")
} else {
image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight)
} }
image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight)
break break
} }
return image, err 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 err error
var direction Direction = -1 var direction Direction = -1
@ -198,17 +280,17 @@ func rotateImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, e
return image, err return image, err
} }
func watermakImage(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImage, error) { func watermakImage(image *C.VipsImage, w Watermark) (*C.VipsImage, error) {
if len(w.Text) == 0 { if w.Text == "" {
return image, nil return image, nil
} }
// Defaults // Defaults
if len(w.Font) == 0 { if w.Font == "" {
w.Font = "sans 10" w.Font = WATERMARK_FONT
} }
if w.Width == 0 { 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 { if w.DPI == 0 {
w.DPI = 150 w.DPI = 150
@ -230,15 +312,23 @@ func watermakImage(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag
return image, nil return image, nil
} }
func zoomImage(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error) { func imageFlatten(image *C.VipsImage, imageType ImageType, o Options) (*C.VipsImage, error) {
if zoom == 0 { // Only PNG images are supported for now
if imageType != PNG || o.Background == ColorBlack {
return image, nil 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) 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 // Use vips_shrink with the integral reduction
image, err := vipsShrink(image, shrink) image, err := vipsShrink(image, shrink)
if err != nil { if err != nil {
@ -258,8 +348,8 @@ func shrinkImage(image *C.struct__VipsImage, o Options, residual float64, shrink
return image, residual, nil return image, residual, nil
} }
func shrinkJpegImage(buf []byte, input *C.struct__VipsImage, factor float64, shrink int) (*C.struct__VipsImage, float64, error) { func shrinkJpegImage(buf []byte, input *C.VipsImage, factor float64, shrink int) (*C.VipsImage, float64, error) {
var image *C.struct__VipsImage var image *C.VipsImage
var err error var err error
shrinkOnLoad := 1 shrinkOnLoad := 1
@ -305,8 +395,8 @@ func imageCalculations(o *Options, inWidth, inHeight int) float64 {
case o.Height > 0: case o.Height > 0:
factor = yfactor factor = yfactor
o.Width = int(math.Floor(float64(inWidth) / factor)) o.Width = int(math.Floor(float64(inWidth) / factor))
default:
// Identity transform // Identity transform
default:
o.Width = inWidth o.Width = inWidth
o.Height = inHeight o.Height = inHeight
break break
@ -337,11 +427,14 @@ func calculateCrop(inWidth, inHeight, outWidth, outHeight int, gravity Gravity)
return left, top return left, top
} }
func calculateRotationAndFlip(image *C.struct__VipsImage, angle Angle) (Angle, bool) { func calculateRotationAndFlip(image *C.VipsImage, angle Angle) (Angle, bool) {
rotate := D0 rotate := D0
flip := false flip := false
if angle == -1 { if angle > 0 {
return rotate, flip
}
switch vipsExifOrientation(image) { switch vipsExifOrientation(image) {
case 6: case 6:
rotate = D90 rotate = D90
@ -368,17 +461,27 @@ func calculateRotationAndFlip(image *C.struct__VipsImage, angle Angle) (Angle, b
rotate = D270 rotate = D270
break // flip 8 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 { } else {
if angle == 90 { shrink = math.Floor(factor)
rotate = D90
} else if angle == 180 {
rotate = D180
} else if angle == 270 {
rotate = D270
} }
return int(math.Max(shrink, 1))
} }
return rotate, flip func calculateResidual(factor float64, shrink int) float64 {
return float64(shrink) / factor
} }
func getAngle(angle Angle) Angle { func getAngle(angle Angle) Angle {

@ -20,9 +20,51 @@ func TestResize(t *testing.T) {
t.Fatal("Image is not jpeg") t.Fatal("Image is not jpeg")
} }
err = Write("fixtures/test_out.jpg", newImg) 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 { if err != nil {
t.Fatal("Cannot save the image") 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,15 +81,17 @@ func TestRotate(t *testing.T) {
t.Fatal("Image is not jpeg") t.Fatal("Image is not jpeg")
} }
err = Write("fixtures/test_rotate_out.jpg", newImg) size, _ := Size(newImg)
if err != nil { if size.Height != options.Width {
t.Fatal("Cannot save the image") t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
} }
Write("fixtures/test_rotate_out.jpg", newImg)
} }
func testColorspace(t *testing.T) { func TestInvalidRotate(t *testing.T) {
options := Options{Colorspace: true} options := Options{Width: 800, Height: 600, Rotate: 111}
buf, _ := Read("fixtures/sky.jpg") buf, _ := Read("fixtures/test.jpg")
newImg, err := Resize(buf, options) newImg, err := Resize(buf, options)
if err != nil { if err != nil {
@ -58,10 +102,12 @@ func testColorspace(t *testing.T) {
t.Fatal("Image is not jpeg") t.Fatal("Image is not jpeg")
} }
err = Write("fixtures/test_color_out.jpg", newImg) size, _ := Size(newImg)
if err != nil { if size.Height != options.Width {
t.Fatal("Cannot save the image") t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
} }
Write("fixtures/test_invalid_rotate_out.jpg", newImg)
} }
func TestCorruptedImage(t *testing.T) { func TestCorruptedImage(t *testing.T) {
@ -77,14 +123,36 @@ func TestCorruptedImage(t *testing.T) {
t.Fatal("Image is not jpeg") t.Fatal("Image is not jpeg")
} }
err = Write("fixtures/test_corrupt_out.jpg", newImg) 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_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 { 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")
} }
func TestInvalidRotate(t *testing.T) { size, _ := Size(newImg)
options := Options{Width: 800, Height: 600, Rotate: 111} 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") buf, _ := Read("fixtures/test.jpg")
newImg, err := Resize(buf, options) newImg, err := Resize(buf, options)
@ -92,48 +160,70 @@ func TestInvalidRotate(t *testing.T) {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err) t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
} }
if DetermineImageType(newImg) != JPEG { size, _ := Size(newImg)
t.Fatal("Image is not jpeg") 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)
} }
err = Write("fixtures/test_invalid_rotate_out.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 { if err != nil {
t.Fatal("Cannot save the image") 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) { func TestConvert(t *testing.T) {
width, height := 640, 480 width, height := 300, 240
formats := [3]ImageType{PNG, WEBP, JPEG}
files := []string{
"test.jpg",
"test.png",
"test.webp",
}
options := Options{Width: width, Height: height, Crop: true, Type: PNG} for _, file := range files {
img, err := os.Open("fixtures/test.jpg") img, err := os.Open("fixtures/" + file)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer img.Close()
buf, err := ioutil.ReadAll(img) buf, err := ioutil.ReadAll(img)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
img.Close()
for _, format := range formats {
options := Options{Width: width, Height: height, Crop: true, Type: format}
newImg, err := Resize(buf, options) newImg, err := Resize(buf, options)
if err != nil { if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err) t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
} }
if DetermineImageType(newImg) != PNG { if DetermineImageType(newImg) != format {
t.Fatal("Image is not png") t.Fatal("Image is not png")
} }
size, _ := Size(newImg) size, _ := Size(newImg)
if size.Height != height || size.Width != width { if size.Height != height || size.Width != width {
t.Fatal("Invalid image size") t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
}
} }
err = Write("fixtures/test_out.png", newImg)
if err != nil {
t.Fatal("Cannot save the image")
} }
} }
@ -166,10 +256,7 @@ func TestResizePngWithTransparency(t *testing.T) {
t.Fatal("Invalid image size") t.Fatal("Invalid image size")
} }
err = Write("fixtures/transparent_out.png", newImg) Write("fixtures/transparent_out.png", newImg)
if err != nil {
t.Fatal("Cannot save the image")
}
} }
func runBenchmarkResize(file string, o Options, b *testing.B) { 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) { func BenchmarkResizeLargeJpeg(b *testing.B) {
options := Options{ options := Options{
Width: 800, Width: 800,
@ -209,7 +301,17 @@ func BenchmarkConvertToJpeg(b *testing.B) {
runBenchmarkResize("test.png", options, 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{ options := Options{
Width: 800, Width: 800,
Height: 600, Height: 600,
@ -217,6 +319,22 @@ func BenchmarkCrop(b *testing.B) {
runBenchmarkResize("test.jpg", options, 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) { func BenchmarkExtractJpeg(b *testing.B) {
options := Options{ options := Options{
Top: 100, Top: 100,
@ -226,3 +344,83 @@ func BenchmarkExtractJpeg(b *testing.B) {
} }
runBenchmarkResize("test.jpg", options, 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)
}

@ -11,6 +11,15 @@ const (
MAGICK 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) // Determines the image type format (jpeg, png, webp or tiff)
func DetermineImageType(buf []byte) ImageType { func DetermineImageType(buf []byte) ImageType {
return vipsImageType(buf) return vipsImageType(buf)
@ -18,40 +27,28 @@ func DetermineImageType(buf []byte) ImageType {
// Determines the image type format by name (jpeg, png, webp or tiff) // Determines the image type format by name (jpeg, png, webp or tiff)
func DetermineImageTypeName(buf []byte) string { func DetermineImageTypeName(buf []byte) string {
return getImageTypeName(vipsImageType(buf)) return ImageTypeName(vipsImageType(buf))
} }
// Check if a given image type is supported // Check if a given image type is supported
func IsTypeSupported(t ImageType) bool { func IsTypeSupported(t ImageType) bool {
return t == JPEG || t == PNG || t == WEBP return ImageTypes[t] != ""
} }
// Check if a given image type name is supported // Check if a given image type name is supported
func IsTypeNameSupported(t string) bool { func IsTypeNameSupported(t string) bool {
return t == "jpeg" || t == "jpg" || for _, name := range ImageTypes {
t == "png" || t == "webp" if name == t {
} return true
}
func getImageTypeName(code ImageType) string { }
imageType := "unknown" return false
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 return imageType
} }

@ -68,7 +68,7 @@ func TestIsTypeNameSupported(t *testing.T) {
name string name string
expected bool expected bool
}{ }{
{"jpg", true}, {"jpeg", true},
{"png", true}, {"png", true},
{"webp", true}, {"webp", true},
{"gif", false}, {"gif", false},

@ -1,3 +1,3 @@
package bimg package bimg
const Version = "0.1.5" const Version = "0.1.21"

@ -3,21 +3,32 @@ package bimg
/* /*
#cgo pkg-config: vips #cgo pkg-config: vips
#include "vips.h" #include "vips.h"
#include "stdlib.h"
*/ */
import "C" import "C"
import ( import (
"errors" "errors"
"math"
"os"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"unsafe" "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 ( var (
m sync.Mutex m sync.Mutex
initialized bool = false initialized bool
) )
type VipsMemoryInfo struct { type VipsMemoryInfo struct {
@ -30,6 +41,9 @@ type vipsSaveOptions struct {
Quality int Quality int
Compression int Compression int
Type ImageType Type ImageType
Interlace bool
NoProfile bool
Interpretation Interpretation
} }
type vipsWatermarkOptions struct { type vipsWatermarkOptions struct {
@ -47,16 +61,16 @@ type vipsWatermarkTextOptions struct {
} }
func init() { func init() {
if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 {
panic("unsupported old vips version!")
}
Initialize() Initialize()
} }
// Explicit thread-safe start of libvips. // Explicit thread-safe start of libvips.
// Only call this function if you've previously shutdown libvips // Only call this function if you've previously shutdown libvips
func Initialize() { func Initialize() {
if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 {
panic("unsupported libvips version!")
}
m.Lock() m.Lock()
runtime.LockOSThread() runtime.LockOSThread()
defer m.Unlock() defer m.Unlock()
@ -64,30 +78,42 @@ func Initialize() {
err := C.vips_init(C.CString("bimg")) err := C.vips_init(C.CString("bimg"))
if err != 0 { if err != 0 {
Shutdown()
panic("unable to start vips!") panic("unable to start vips!")
} }
C.vips_concurrency_set(0) // default // Set libvips cache params
C.vips_cache_set_max_mem(100 * 1024 * 1024) // 100 MB C.vips_cache_set_max_mem(maxCacheMem)
C.vips_cache_set_max(500) // 500 operations 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 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 // If libvips was already initialized, the function is no-op
func Shutdown() { func Shutdown() {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
if initialized == true { if initialized {
C.vips_shutdown() C.vips_shutdown()
initialized = false initialized = false
} }
} }
// Output to stdout vips collected data. Useful for debugging // Output to stdout vips collected data. Useful for debugging
func VipsDebug() { func VipsDebugInfo() {
C.im__print_all() 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)) 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 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 return int(C.has_profile_embed(image)) > 0
} }
func vipsWindowSize(name string) float64 { 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)) return C.GoString(C.vips_enum_nick_bridge(image))
} }
func vipsRotate(image *C.struct__VipsImage, angle Angle) (*C.struct__VipsImage, error) { func vipsRotate(image *C.VipsImage, angle Angle) (*C.VipsImage, error) {
var out *C.struct__VipsImage var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image)) defer C.g_object_unref(C.gpointer(image))
err := C.vips_rotate(image, &out, C.int(angle)) 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 return out, nil
} }
func vipsFlip(image *C.struct__VipsImage, direction Direction) (*C.struct__VipsImage, error) { func vipsFlip(image *C.VipsImage, direction Direction) (*C.VipsImage, error) {
var out *C.struct__VipsImage var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image)) defer C.g_object_unref(C.gpointer(image))
err := C.vips_flip_bridge(image, &out, C.int(direction)) 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 return out, nil
} }
func vipsZoom(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error) { func vipsZoom(image *C.VipsImage, zoom int) (*C.VipsImage, error) {
var out *C.struct__VipsImage var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image)) defer C.g_object_unref(C.gpointer(image))
err := C.vips_zoom_bridge(image, &out, C.int(zoom), C.int(zoom)) 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 return out, nil
} }
func vipsColorSpace(image *C.struct__VipsImage) (*C.struct__VipsImage, error) { func vipsWatermark(image *C.VipsImage, w Watermark) (*C.VipsImage, error) {
var out *C.struct__VipsImage var out *C.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
// Defaults // Defaults
noReplicate := 0 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(text))
defer C.free(unsafe.Pointer(font)) 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 { if err != 0 {
return nil, catchVipsError() return nil, catchVipsError()
} }
@ -211,8 +211,8 @@ func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag
return out, nil return out, nil
} }
func vipsRead(buf []byte) (*C.struct__VipsImage, ImageType, error) { func vipsRead(buf []byte) (*C.VipsImage, ImageType, error) {
var image *C.struct__VipsImage var image *C.VipsImage
imageType := vipsImageType(buf) imageType := vipsImageType(buf)
if imageType == UNKNOWN { if imageType == UNKNOWN {
@ -230,26 +230,105 @@ func vipsRead(buf []byte) (*C.struct__VipsImage, ImageType, error) {
return image, imageType, nil return image, imageType, nil
} }
func vipsSave(image *C.struct__VipsImage, o vipsSaveOptions) ([]byte, error) { func vipsColourspaceIsSupportedBuffer(buf []byte) (bool, error) {
var ptr unsafe.Pointer image, _, err := vipsRead(buf)
length := C.size_t(0) if err != nil {
err := C.int(0) 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)) defer C.g_object_unref(C.gpointer(image))
switch { tmpImage, err := vipsPreSave(image, &o)
case o.Type == PNG: if err != nil {
err = C.vips_pngsave_bridge(image, &ptr, &length, 1, C.int(o.Compression), C.int(o.Quality), 0) 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 break
case o.Type == WEBP: case PNG:
err = C.vips_webpsave_bridge(image, &ptr, &length, 1, C.int(o.Quality), 0) saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, 1, C.int(o.Compression), quality, interlace)
break break
default: 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 break
} }
if int(err) != 0 { if int(saveErr) != 0 {
return nil, catchVipsError() return nil, catchVipsError()
} }
@ -262,14 +341,19 @@ func vipsSave(image *C.struct__VipsImage, o vipsSaveOptions) ([]byte, error) {
return buf, nil return buf, nil
} }
func vipsExtract(image *C.struct__VipsImage, left, top, width, height int) (*C.struct__VipsImage, error) { func max(x int) int {
var buf *C.struct__VipsImage 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)) defer C.g_object_unref(C.gpointer(image))
if width > MAX_SIZE || height > MAX_SIZE { if width > MAX_SIZE || height > MAX_SIZE {
return nil, errors.New("Maximum image size exceeded") 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)) err := C.vips_extract_area_bridge(image, &buf, C.int(left), C.int(top), C.int(width), C.int(height))
if err != 0 { if err != 0 {
return nil, catchVipsError() return nil, catchVipsError()
@ -278,11 +362,12 @@ func vipsExtract(image *C.struct__VipsImage, left, top, width, height int) (*C.s
return buf, nil return buf, nil
} }
func vipsShrinkJpeg(buf []byte, input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, error) { func vipsShrinkJpeg(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, error) {
var image *C.struct__VipsImage var image *C.VipsImage
var ptr = unsafe.Pointer(&buf[0])
defer C.g_object_unref(C.gpointer(input)) 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 { if err != 0 {
return nil, catchVipsError() return nil, catchVipsError()
} }
@ -290,8 +375,8 @@ func vipsShrinkJpeg(buf []byte, input *C.struct__VipsImage, shrink int) (*C.stru
return image, nil return image, nil
} }
func vipsShrink(input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, error) { func vipsShrink(input *C.VipsImage, shrink int) (*C.VipsImage, error) {
var image *C.struct__VipsImage var image *C.VipsImage
defer C.g_object_unref(C.gpointer(input)) defer C.g_object_unref(C.gpointer(input))
err := C.vips_shrink_bridge(input, &image, C.double(float64(shrink)), C.double(float64(shrink))) 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 return image, nil
} }
func vipsEmbed(input *C.struct__VipsImage, left, top, width, height, extend int) (*C.struct__VipsImage, error) { func vipsEmbed(input *C.VipsImage, left, top, width, height, extend int) (*C.VipsImage, error) {
var image *C.struct__VipsImage var image *C.VipsImage
defer C.g_object_unref(C.gpointer(input)) 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)) 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 return image, nil
} }
func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (*C.struct__VipsImage, error) { func vipsAffine(input *C.VipsImage, residualx, residualy float64, i Interpolator) (*C.VipsImage, error) {
var image *C.struct__VipsImage var image *C.VipsImage
istring := C.CString(i.String()) cstring := C.CString(i.String())
interpolator := C.vips_interpolate_new(istring) 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(input))
defer C.g_object_unref(C.gpointer(interpolator)) 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 { if err != 0 {
return nil, catchVipsError() return nil, catchVipsError()
} }
@ -331,36 +416,37 @@ func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (*
return image, nil return image, nil
} }
func vipsImageType(buf []byte) ImageType { func vipsImageType(bytes []byte) ImageType {
imageType := UNKNOWN if len(bytes) == 0 {
return UNKNOWN
if len(buf) == 0 {
return imageType
} }
length := C.size_t(len(buf)) if bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47 {
imageBuf := unsafe.Pointer(&buf[0]) return PNG
bufferType := C.GoString(C.vips_foreign_find_load_buffer(imageBuf, length)) }
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
}
switch { return UNKNOWN
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 func readImageType(buf []byte) string {
length := C.size_t(len(buf))
imageBuf := unsafe.Pointer(&buf[0])
load := C.vips_foreign_find_load_buffer(imageBuf, length)
defer C.free(imageBuf)
return C.GoString(load)
} }
func catchVipsError() error { func catchVipsError() error {
@ -369,3 +455,32 @@ func catchVipsError() error {
C.vips_thread_shutdown() C.vips_thread_shutdown()
return errors.New(s) 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
}

330
vips.h

@ -2,6 +2,28 @@
#include <vips/vips.h> #include <vips/vips.h>
#include <vips/vips7compat.h> #include <vips/vips7compat.h>
#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 { enum types {
UNKNOWN = 0, UNKNOWN = 0,
JPEG, JPEG,
@ -12,9 +34,9 @@ enum types {
}; };
typedef struct { typedef struct {
char *Text; const char *Text;
char *Font; const char *Font;
} watermarkTextOptions; } WatermarkTextOptions;
typedef struct { typedef struct {
int Width; int Width;
@ -23,35 +45,80 @@ typedef struct {
int NoReplicate; int NoReplicate;
float Opacity; float Opacity;
double Background[3]; 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 int
vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) 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) {
return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL); return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL);
}; }
int 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); return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL);
}; }
int 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); return vips_flip(in, out, direction, NULL);
}; }
int 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); return vips_shrink(in, out, xshrink, yshrink, NULL);
}; }
int int
vips_rotate(VipsImage *in, VipsImage **buf, int angle) vips_rotate(VipsImage *in, VipsImage **out, int angle) {
{
int rotate = VIPS_ANGLE_D0; int rotate = VIPS_ANGLE_D0;
if (angle == 90) { if (angle == 90) {
@ -62,35 +129,21 @@ vips_rotate(VipsImage *in, VipsImage **buf, int angle)
rotate = VIPS_ANGLE_D270; rotate = VIPS_ANGLE_D270;
} }
return vips_rot(in, buf, rotate, NULL); return vips_rot(in, out, rotate, NULL);
}; }
int int
vips_exif_orientation(VipsImage *image) { vips_exif_orientation(VipsImage *image) {
int orientation = 0; int orientation = 0;
const char **exif; const char *exif;
if ( if (
vips_image_get_typeof(image, "exif-ifd0-Orientation") != 0 && vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 &&
!vips_image_get_string(image, "exif-ifd0-Orientation", exif) !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif)
) { ) {
orientation = atoi(exif[0]); orientation = atoi(&exif[0]);
} }
return orientation; 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 int
interpolator_window_size(char const *name) { interpolator_window_size(char const *name) {
@ -98,102 +151,137 @@ interpolator_window_size(char const *name) {
int window_size = vips_interpolate_get_window_size(interpolator); int window_size = vips_interpolate_get_window_size(interpolator);
g_object_unref(interpolator); g_object_unref(interpolator);
return window_size; return window_size;
}; }
const char * const char *
vips_enum_nick_bridge(VipsImage *image) { vips_enum_nick_bridge(VipsImage *image) {
return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
}; }
int 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); return vips_zoom(in, out, xfac, yfac, NULL);
}; }
int int
vips_colorspace_bridge(VipsImage *in, VipsImage **out) 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);
return vips_colourspace(in, out, VIPS_INTERPRETATION_LAB, NULL); }
};
int int
vips_hist_find_ndim_bridge(VipsImage *in, VipsImage **out) 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);
return vips_hist_find_ndim(in, out, "bins", 5, NULL); }
};
int int
vips_max_bridge(VipsImage *in, double *out, int **x, int **y) vips_colourspace_issupported_bridge(VipsImage *in) {
{ return vips_colourspace_issupported(in) ? 1 : 0;
double ones[3] = { 1, 1, 1 }; }
return vips_max(in, ones, "x", x, "y", y, NULL);
};
int VipsInterpretation
vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend) vips_image_guess_interpretation_bridge(VipsImage *in) {
{ return vips_image_guess_interpretation(in);
return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); }
};
int int
vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) {
{ return vips_colourspace(in, out, space, NULL);
return vips_extract_area(in, out, left, top, width, height, NULL); }
};
int int
vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) {
{ return vips_jpegsave_buffer(in, buf, len,
return vips_jpegsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL); "strip", strip,
}; "Q", quality,
"optimize_coding", TRUE,
"interlace", with_interlace(interlace),
NULL
);
}
int int
vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) 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)) #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
return vips_pngsave_buffer(in, buf, len, "strip", FALSE, "compression", compression, return vips_pngsave_buffer(in, buf, len,
"interlace", interlace, "filter", VIPS_FOREIGN_PNG_FILTER_NONE, NULL); "strip", FALSE,
"compression", compression,
"interlace", with_interlace(interlace),
"filter", VIPS_FOREIGN_PNG_FILTER_NONE,
NULL
);
#else #else
return vips_pngsave_buffer(in, buf, len, "strip", FALSE, "compression", compression, return vips_pngsave_buffer(in, buf, len,
"interlace", interlace, NULL); "strip", FALSE,
"compression", compression,
"interlace", with_interlace(interlace),
NULL
);
#endif #endif
}; }
int int
vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality) {
{ return vips_webpsave_buffer(in, buf, len,
return vips_webpsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL); "strip", strip,
}; "Q", quality,
NULL
);
}
int
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 int
vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) { vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) {
int code = 1; int code = 1;
if (imageType == JPEG) { 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) { } 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) { } 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) { } 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) #if (VIPS_MAJOR_VERSION >= 8)
} else if (imageType == MAGICK) { } 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 #endif
} }
return code; return code;
}; }
int int
vips_watermark(VipsImage *in, VipsImage **out, watermarkTextOptions *to, watermarkOptions *o) vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out) {
{ VipsImage *cache = vips_image_new();
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;
}
g_object_unref(cache);
return 0;
}
int
vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o) {
double ones[3] = { 1, 1, 1 }; double ones[3] = { 1, 1, 1 };
VipsImage *base = vips_image_new(); VipsImage *base = vips_image_new();
VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 12); VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10);
t[0] = in; t[0] = in;
// Make the mask. // Make the mask.
@ -205,48 +293,60 @@ vips_watermark(VipsImage *in, VipsImage **out, watermarkTextOptions *to, waterma
NULL) || NULL) ||
vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) || vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) ||
vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) || vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) ||
vips_embed(t[3], &t[4], 100, 100, vips_embed(t[3], &t[4], 100, 100, t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL)
t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL)
) { ) {
g_object_unref(base); g_object_unref(base);
return (1); return 1;
} }
// Replicate if necessary // Replicate if necessary
if (o->NoReplicate != 1 && ( if (o->NoReplicate != 1) {
vips_replicate(t[4], &t[5], VipsImage *cache = vips_image_new();
1 + t[0]->Xsize / t[4]->Xsize, if (vips_watermark_replicate(t[0], t[4], &cache)) {
1 + t[0]->Ysize / t[4]->Ysize, NULL) || g_object_unref(cache);
vips_crop(t[5], &t[6], 0, 0,
t[0]->Xsize, t[0]->Ysize, NULL)
)) {
g_object_unref(base); g_object_unref(base);
return (1); return 1;
}
g_object_unref(t[4]);
t[4] = cache;
} }
// Make the constant image to paint the text with. // Make the constant image to paint the text with.
if ( if (
vips_black(&t[7], 1, 1, NULL) || vips_black(&t[5], 1, 1, NULL) ||
vips_linear( t[7], &t[8], ones, o->Background, 3, NULL) || vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) ||
vips_cast(t[8], &t[9], VIPS_FORMAT_UCHAR, NULL) || vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) ||
vips_copy(t[9], &t[10], vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) ||
"interpretation", t[0]->Type, vips_embed(t[8], &t[9], 0, 0, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_COPY, NULL)
NULL) ||
vips_embed(t[10], &t[11], 0, 0,
t[0]->Xsize, t[0]->Ysize,
"extend", VIPS_EXTEND_COPY,
NULL)
) { ) {
g_object_unref(base); g_object_unref(base);
return (1); return 1;
} }
// Blend the mask and text and write to output. // Blend the mask and text and write to output.
if (vips_ifthenelse(t[6], t[11], t[0], out, "blend", TRUE, NULL)) { if (vips_ifthenelse(t[4], t[9], t[0], out, "blend", TRUE, NULL)) {
g_object_unref(base); g_object_unref(base);
return (1); return 1;
} }
g_object_unref(base); g_object_unref(base);
return (0); 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
}

@ -18,37 +18,102 @@ func TestVipsRead(t *testing.T) {
} }
for _, file := range files { for _, file := range files {
img, _ := os.Open(path.Join("fixtures", file.name)) image, imageType, _ := vipsRead(readImage(file.name))
buf, _ := ioutil.ReadAll(img)
defer img.Close()
image, imageType, _ := vipsRead(buf)
if image == nil { if image == nil {
t.Fatal("Empty image") t.Fatal("Empty image")
} }
if imageType != file.expected { if imageType != file.expected {
t.Fatal("Empty image") t.Fatal("Invalid image type")
} }
} }
} }
func TestVipsSave(t *testing.T) { func TestVipsSave(t *testing.T) {
img, _ := os.Open(path.Join("fixtures", "test.jpg")) image, _, _ := vipsRead(readImage("test.jpg"))
buf, _ := ioutil.ReadAll(img) options := vipsSaveOptions{Quality: 95, Type: JPEG, Interlace: true}
defer img.Close()
image, _, _ := vipsRead(buf) buf, err := vipsSave(image, options)
if image == nil { if err != nil {
t.Fatal("Cannot save the image")
}
if len(buf) == 0 {
t.Fatal("Empty image") t.Fatal("Empty image")
} }
}
options := vipsSaveOptions{Quality: 95, Type: JPEG} func TestVipsRotate(t *testing.T) {
image, _, _ := vipsRead(readImage("test.jpg"))
buf, err := vipsSave(image, options) 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 { if err != nil {
t.Fatal("Cannot save the image") 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 { if len(buf) == 0 {
t.Fatal("Empty image") 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
}

Loading…
Cancel
Save