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
- tip
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
- curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

@ -1,19 +1,44 @@
# bimg [![Build Status](https://travis-ci.org/h2non/bimg.png)](https://travis-ci.org/h2non/bimg) [![GitHub release](https://img.shields.io/github/tag/h2non/bimg.svg)](https://github.com/h2non/bimg/releases) [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.png)](https://godoc.org/github.com/h2non/bimg) [![Coverage Status](https://coveralls.io/repos/h2non/bimg/badge.svg?branch=master)](https://coveralls.io/r/h2non/bimg?branch=master)
# bimg [![Build Status](https://travis-ci.org/h2non/bimg.png)](https://travis-ci.org/h2non/bimg) [![GitHub release](http://img.shields.io/github/tag/h2non/bimg.svg?style=flat-square)](https://github.com/h2non/bimg/releases) [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.svg)](https://godoc.org/github.com/h2non/bimg)
Small [Go](http://golang.org) library for blazing fast and efficient image processing based on [libvips](https://github.com/jcupitt/libvips) using C bindings. It provides a clean, simple and fluent [API](https://godoc.org/github.com/h2non/bimg) in pure Go.
Small [Go](http://golang.org) package for fast high-level image processing using [libvips](https://github.com/jcupitt/libvips) via C bindings, providing a simple, elegant and fluent [programmatic API](#examples).
bimg is designed to be a small and efficient library with a generic and useful set of features.
It uses internally libvips, which requires a [low memory footprint](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use)
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.
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.
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),
its homologous package built for node.js by [Lovell Fuller](https://github.com/lovell).
- [Supported image operations](#supported-image-operations)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Performance](#performance)
- [Benchmark](#benchmark)
- [Examples](#examples)
- [Debugging](#debugging)
- [API](#api)
- [Credits](#credits)
## Supported image operations
**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
@ -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 -
```
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
- Enlarge
- Crop
- Rotate (with auto-rotate based on EXIF orientation)
- Flip (with auto-flip based on EXIF metadata)
- Flop
- Zoom
- Thumbnail
- Extract area
- Watermark (fully customizable text-based)
- Format conversion (with additional quality/compression settings)
- EXIF metadata (size, alpha channel, profile, orientation...)
For platform specific installations, see [Mac OS](https://github.com/lovell/sharp/blob/master/README.md#mac-os-tips) tips or [Windows](https://github.com/lovell/sharp/blob/master/README.md#windows) tips
## Performance
@ -59,23 +76,32 @@ Here you can see some performance test comparisons for multiple scenarios:
- [libvips speed and memory usage](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use)
- [sharp performance tests](https://github.com/lovell/sharp#the-task)
#### Benchmarks
## Benchmark
Tested using Go 1.4 and libvips-7.42.3 in OSX i7 2.7Ghz
Tested using Go 1.5.1 and libvips-7.42.3 in OSX i7 2.7Ghz
```
PASS
BenchmarkResizeLargeJpeg 30 46652408 ns/op
BenchmarkResizePng 20 57387902 ns/op
BenchmarkResizeWebP 500 2453220 ns/op
BenchmarkConvertToJpeg 30 35556414 ns/op
BenchmarkCrop 30 51768475 ns/op
BenchmarkExtract 30 50866406 ns/op
ok 9.424s
BenchmarkRotateJpeg-8 20 64686945 ns/op
BenchmarkResizeLargeJpeg-8 20 63390416 ns/op
BenchmarkResizePng-8 100 18147294 ns/op
BenchmarkResizeWebP-8 100 20836741 ns/op
BenchmarkConvertToJpeg-8 100 12831812 ns/op
BenchmarkConvertToPng-8 10 128901422 ns/op
BenchmarkConvertToWebp-8 10 204027990 ns/op
BenchmarkCropJpeg-8 30 59068572 ns/op
BenchmarkCropPng-8 10 117303259 ns/op
BenchmarkCropWebP-8 10 107060659 ns/op
BenchmarkExtractJpeg-8 50 30708919 ns/op
BenchmarkExtractPng-8 3000 595546 ns/op
BenchmarkExtractWebp-8 3000 386379 ns/op
BenchmarkZoomJpeg-8 10 160005424 ns/op
BenchmarkZoomPng-8 30 44561047 ns/op
BenchmarkZoomWebp-8 10 126732678 ns/op
BenchmarkWatermarkJpeg-8 20 79006133 ns/op
BenchmarkWatermarPng-8 200 8197291 ns/op
BenchmarkWatermarWebp-8 30 49360369 ns/op
```
## API
### Examples
## Examples
```go
import (
@ -140,6 +166,46 @@ if bimg.NewImage(newImage).Type() == "png" {
}
```
#### Force resize
Force resize operation without perserving the aspect ratio:
```go
buffer, err := bimg.Read("image.jpg")
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
newImage, err := bimg.NewImage(buffer).ForceResize(1000, 500)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
size := bimg.Size(newImage)
if size.Width != 1000 || size.Height != 500 {
fmt.Fprintln(os.Stderr, "Incorrect image size")
}
```
#### Custom colour space (black & white)
```go
buffer, err := bimg.Read("image.jpg")
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
newImage, err := bimg.NewImage(buffer).Colourspace(bimg.INTERPRETATION_B_W)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
colourSpace, _ := bimg.ImageInterpretation(newImage)
if colourSpace != bimg.INTERPRETATION_B_W {
fmt.Fprintln(os.Stderr, "Invalid colour space")
}
```
#### Custom options
See [Options](https://godoc.org/github.com/h2non/bimg#Options) struct to discover all the available fields
@ -151,6 +217,7 @@ options := bimg.Options{
Crop: true,
Quality: 95,
Rotate: 180,
Interlace: true,
}
buffer, err := bimg.Read("image.jpg")
@ -174,9 +241,8 @@ if err != nil {
fmt.Fprintln(os.Stderr, err)
}
options := bimg.Watermark{
Watermark{
Text: "Chuck Norris - Copyright (c) 2315",
watermark := bimg.Watermark{
Text: "Chuck Norris (c) 2315",
Opacity: 0.25,
Width: 200,
DPI: 100,
@ -184,9 +250,8 @@ options := bimg.Watermark{
Font: "sans bold 12",
Background: bimg.Color{255, 255, 255},
}
}
newImage, err := bimg.NewImage(buffer).Watermark()
newImage, err := bimg.NewImage(buffer).Watermark(watermark)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
@ -220,349 +285,36 @@ if err != nil {
bimg.Write("new.jpg", newImage)
```
#### func DetermineImageTypeName
```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
## Debugging
```go
type Direction int
Run the process passing the `DEBUG` environment variable
```
```go
const (
HORIZONTAL Direction = C.VIPS_DIRECTION_HORIZONTAL
VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL
)
DEBUG=bimg ./app
```
#### type Gravity
```go
type Gravity int
Enable libvips traces (note that a lot of data will be written in stdout):
```
```go
const (
CENTRE Gravity = iota
NORTH
EAST
SOUTH
WEST
)
VIPS_TRACE=1 ./app
```
#### type Image
```go
type Image struct {
}
```
#### func NewImage
```go
func NewImage(buf []byte) *Image
```
Creates a new image
#### func (*Image) Convert
```go
func (i *Image) Convert(t ImageType) ([]byte, error)
```
Convert image to another format
#### func (*Image) Crop
```go
func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error)
```
Crop the image to the exact size specified
#### func (*Image) CropByHeight
```go
func (i *Image) CropByHeight(height int) ([]byte, error)
```
Crop an image by height (auto width)
#### func (*Image) CropByWidth
```go
func (i *Image) CropByWidth(width int) ([]byte, error)
```
Crop an image by width (auto height)
#### func (*Image) Enlarge
```go
func (i *Image) Enlarge(width, height int) ([]byte, error)
```
Enlarge the image from the by X/Y axis
#### func (*Image) Extract
```go
func (i *Image) Extract(top, left, width, height int) ([]byte, error)
```
Extract area from the by X/Y axis
#### func (*Image) Flip
```go
func (i *Image) Flip() ([]byte, error)
```
Flip the image about the vertical Y axis
#### func (*Image) Flop
```go
func (i *Image) Flop() ([]byte, error)
```
Flop the image about the horizontal X axis
#### func (*Image) Metadata
```go
func (i *Image) Metadata() (ImageMetadata, error)
```
Get image metadata (size, alpha channel, profile, EXIF rotation)
#### func (*Image) Process
```go
func (i *Image) Process(o Options) ([]byte, error)
```
Transform the image by custom options
#### func (*Image) Resize
```go
func (i *Image) Resize(width, height int) ([]byte, error)
```
Resize the image to fixed width and height
#### func (*Image) Rotate
```go
func (i *Image) Rotate(a Angle) ([]byte, error)
```
Rotate the image by given angle degrees (0, 90, 180 or 270)
#### func (*Image) Size
```go
func (i *Image) Size() (ImageSize, error)
```
Get image size
#### func (*Image) Thumbnail
```go
func (i *Image) Thumbnail(pixels int) ([]byte, error)
```
Thumbnail the image by the a given width by aspect ratio 4:4
#### func (*Image) Type
```go
func (i *Image) Type() string
```
Get image type format (jpeg, png, webp, tiff)
#### type ImageMetadata
```go
type ImageMetadata struct {
Orientation int
Channels int
Alpha bool
Profile bool
Type string
Space string
Size ImageSize
}
```
#### func Metadata
```go
func Metadata(buf []byte) (ImageMetadata, error)
```
Extract the image metadata (size, type, alpha channel, profile, EXIF
orientation...)
#### type ImageSize
```go
type ImageSize struct {
Width int
Height int
}
```
#### func Size
```go
func Size(buf []byte) (ImageSize, error)
```
Get the image size by width and height pixels
#### type ImageType
```go
type ImageType int
```
```go
const (
UNKNOWN ImageType = iota
JPEG
WEBP
PNG
TIFF
MAGICK
)
```
#### func DetermineImageType
```go
func DetermineImageType(buf []byte) ImageType
```
Determines the image type format (jpeg, png, webp or tiff)
#### type Interpolator
```go
type Interpolator int
```
```go
const (
BICUBIC Interpolator = iota
BILINEAR
NOHALO
)
```
#### func (Interpolator) String
```go
func (i Interpolator) String() string
```
## API
#### type Options
See [godoc reference](https://godoc.org/github.com/h2non/bimg) for detailed API documentation.
```go
type Options struct {
Height int
Width int
AreaHeight int
AreaWidth int
Top int
Left int
Extend int
Quality int
Compression int
Crop bool
Enlarge bool
Embed bool
Flip bool
Flop bool
Rotate Angle
Gravity Gravity
Type ImageType
Interpolator Interpolator
}
```
## Credits
## Special Thanks
People who recurrently contributed to improve `bimg` in some way.
- [John Cupitt](https://github.com/jcupitt)
- [Yoan Blanc](https://github.com/greut)
- [Christophe Eblé](https://github.com/chreble)
- [Brant Fitzsimmons](https://github.com/bfitzsimmons)
- [Thomas Meson](https://github.com/zllak)
Thank you!
## License
MIT - Tomas Aparicio
[![views](https://sourcegraph.com/api/repos/github.com/h2non/bimg/.counters/views.svg)](https://sourcegraph.com/github.com/h2non/bimg)

@ -1,62 +1,5 @@
package bimg
import (
"github.com/dustin/go-humanize"
. "github.com/tj/go-debug"
"runtime"
"strconv"
"time"
)
import . "github.com/tj/go-debug"
var debug = Debug("bimg")
// Print Go memory and garbage collector stats. Useful for debugging
func PrintMemoryStats() {
log := Debug("memory")
mem := memoryStats()
log("\u001b[33m---- Memory Dump Stats ----\u001b[39m")
log("Allocated: %s", humanize.Bytes(mem.Alloc))
log("Total Allocated: %s", humanize.Bytes(mem.TotalAlloc))
log("Memory Allocations: %d", mem.Mallocs)
log("Memory Frees: %d", mem.Frees)
log("Heap Allocated: %s", humanize.Bytes(mem.HeapAlloc))
log("Heap System: %s", humanize.Bytes(mem.HeapSys))
log("Heap In Use: %s", humanize.Bytes(mem.HeapInuse))
log("Heap Idle: %s", humanize.Bytes(mem.HeapIdle))
log("Heap OS Related: %s", humanize.Bytes(mem.HeapReleased))
log("Heap Objects: %s", humanize.Bytes(mem.HeapObjects))
log("Stack In Use: %s", humanize.Bytes(mem.StackInuse))
log("Stack System: %s", humanize.Bytes(mem.StackSys))
log("Stack Span In Use: %s", humanize.Bytes(mem.MSpanInuse))
log("Stack Cache In Use: %s", humanize.Bytes(mem.MCacheInuse))
log("Next GC cycle: %s", humanizeNano(mem.NextGC))
log("Last GC cycle: %s", humanize.Time(time.Unix(0, int64(mem.LastGC))))
log("\u001b[33m---- End Memory Dump ----\u001b[39m")
}
func memoryStats() runtime.MemStats {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
return mem
}
func humanizeNano(n uint64) string {
var suffix string
switch {
case n > 1e9:
n /= 1e9
suffix = "s"
case n > 1e6:
n /= 1e6
suffix = "ms"
case n > 1e3:
n /= 1e3
suffix = "us"
default:
suffix = "ns"
}
return strconv.Itoa(int(n)) + suffix
}

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

@ -4,6 +4,11 @@ type Image struct {
buffer []byte
}
// Creates a new image
func NewImage(buf []byte) *Image {
return &Image{buf}
}
// Resize the image to fixed width and height
func (i *Image) Resize(width, height int) ([]byte, error) {
options := Options{
@ -14,6 +19,27 @@ func (i *Image) Resize(width, height int) ([]byte, error) {
return i.Process(options)
}
// Force resize with custom size (aspect ratio won't be maintained)
func (i *Image) ForceResize(width, height int) ([]byte, error) {
options := Options{
Width: width,
Height: height,
Force: true,
}
return i.Process(options)
}
// Resize the image to fixed width and height with additional crop transformation
func (i *Image) ResizeAndCrop(width, height int) ([]byte, error) {
options := Options{
Width: width,
Height: height,
Embed: true,
Crop: true,
}
return i.Process(options)
}
// Extract area from the by X/Y axis
func (i *Image) Extract(top, left, width, height int) ([]byte, error) {
options := Options{
@ -22,10 +48,15 @@ func (i *Image) Extract(top, left, width, height int) ([]byte, error) {
AreaWidth: width,
AreaHeight: height,
}
if top == 0 && left == 0 {
options.Top = -1
}
return i.Process(options)
}
// Enlarge the image from the by X/Y axis
// Enlarge the image by width and height. Aspect ratio is maintained
func (i *Image) Enlarge(width, height int) ([]byte, error) {
options := Options{
Width: width,
@ -35,6 +66,17 @@ func (i *Image) Enlarge(width, height int) ([]byte, error) {
return i.Process(options)
}
// Enlarge the image by width and height with additional crop transformation
func (i *Image) EnlargeAndCrop(width, height int) ([]byte, error) {
options := Options{
Width: width,
Height: height,
Enlarge: true,
Crop: true,
}
return i.Process(options)
}
// Crop the image to the exact size specified
func (i *Image) Crop(width, height int, gravity Gravity) ([]byte, error) {
options := Options{
@ -75,15 +117,16 @@ func (i *Image) Thumbnail(pixels int) ([]byte, error) {
return i.Process(options)
}
// Insert an image to the existent one as watermark
// Add text as watermark on the given image
func (i *Image) Watermark(w Watermark) ([]byte, error) {
options := Options{Watermark: w}
return i.Process(options)
}
// Zoom the image by the given factor
func (i *Image) Zoom(level int) ([]byte, error) {
options := Options{Zoom: level}
// Zoom the image by the given factor.
// You should probably call Extract() before
func (i *Image) Zoom(factor int) ([]byte, error) {
options := Options{Zoom: factor}
return i.Process(options)
}
@ -111,6 +154,12 @@ func (i *Image) Convert(t ImageType) ([]byte, error) {
return i.Process(options)
}
// Colour space conversion
func (i *Image) Colourspace(c Interpretation) ([]byte, error) {
options := Options{Interpretation: c}
return i.Process(options)
}
// Transform the image by custom options
func (i *Image) Process(o Options) ([]byte, error) {
image, err := Resize(i.buffer, o)
@ -126,6 +175,17 @@ func (i *Image) Metadata() (ImageMetadata, error) {
return Metadata(i.buffer)
}
// Get the image interpretation type
// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation
func (i *Image) Interpretation() (Interpretation, error) {
return ImageInterpretation(i.buffer)
}
// Check if the current image has a valid colourspace
func (i *Image) ColourspaceIsSupported() (bool, error) {
return ColourspaceIsSupported(i.buffer)
}
// Get image type format (jpeg, png, webp, tiff)
func (i *Image) Type() string {
return DetermineImageTypeName(i.buffer)
@ -140,8 +200,3 @@ func (i *Image) Size() (ImageSize, error) {
func (i *Image) Image() []byte {
return i.buffer
}
// Creates a new image
func NewImage(buf []byte) *Image {
return &Image{buf}
}

@ -20,13 +20,27 @@ func TestImageResize(t *testing.T) {
Write("fixtures/test_resize_out.jpg", buf)
}
func TestImageExtract(t *testing.T) {
buf, err := initImage("test.jpg").Extract(100, 100, 300, 300)
func TestImageResizeAndCrop(t *testing.T) {
buf, err := initImage("test.jpg").ResizeAndCrop(300, 200)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
err = assertSize(buf, 300, 300)
err = assertSize(buf, 300, 200)
if err != nil {
t.Error(err)
}
Write("fixtures/test_resize_crop_out.jpg", buf)
}
func TestImageExtract(t *testing.T) {
buf, err := initImage("test.jpg").Extract(100, 100, 300, 200)
if err != nil {
t.Errorf("Cannot process the image: %s", err)
}
err = assertSize(buf, 300, 200)
if err != nil {
t.Error(err)
}
@ -34,6 +48,20 @@ func TestImageExtract(t *testing.T) {
Write("fixtures/test_extract_out.jpg", buf)
}
func TestImageExtractZero(t *testing.T) {
buf, err := initImage("test.jpg").Extract(0, 0, 300, 200)
if err != nil {
t.Errorf("Cannot process the image: %s", err)
}
err = assertSize(buf, 300, 200)
if err != nil {
t.Error(err)
}
Write("fixtures/test_extract_zero_out.jpg", buf)
}
func TestImageEnlarge(t *testing.T) {
buf, err := initImage("test.png").Enlarge(500, 375)
if err != nil {
@ -48,10 +76,24 @@ func TestImageEnlarge(t *testing.T) {
Write("fixtures/test_enlarge_out.jpg", buf)
}
func TestImageEnlargeAndCrop(t *testing.T) {
buf, err := initImage("test.png").EnlargeAndCrop(800, 480)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
err = assertSize(buf, 800, 480)
if err != nil {
t.Error(err)
}
Write("fixtures/test_enlarge_crop_out.jpg", buf)
}
func TestImageCrop(t *testing.T) {
buf, err := initImage("test.jpg").Crop(800, 600, NORTH)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
t.Errorf("Cannot process the image: %s", err)
}
err = assertSize(buf, 800, 600)
@ -65,7 +107,7 @@ func TestImageCrop(t *testing.T) {
func TestImageCropByWidth(t *testing.T) {
buf, err := initImage("test.jpg").CropByWidth(600)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
t.Errorf("Cannot process the image: %s", err)
}
err = assertSize(buf, 600, 375)
@ -79,7 +121,7 @@ func TestImageCropByWidth(t *testing.T) {
func TestImageCropByHeight(t *testing.T) {
buf, err := initImage("test.jpg").CropByHeight(300)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
t.Errorf("Cannot process the image: %s", err)
}
err = assertSize(buf, 480, 300)
@ -93,7 +135,7 @@ func TestImageCropByHeight(t *testing.T) {
func TestImageThumbnail(t *testing.T) {
buf, err := initImage("test.jpg").Thumbnail(100)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
t.Errorf("Cannot process the image: %s", err)
}
err = assertSize(buf, 100, 100)
@ -134,13 +176,51 @@ func TestImageWatermark(t *testing.T) {
Write("fixtures/test_watermark_out.jpg", buf)
}
func 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) {
buf, err := initImage("test.jpg").Zoom(1)
image := initImage("test.jpg")
_, err := image.Extract(100, 100, 400, 300)
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 {
t.Error(err)
}
@ -180,6 +260,19 @@ func TestImageConvert(t *testing.T) {
Write("fixtures/test_image_convert_out.png", buf)
}
func TestTransparentImageConvert(t *testing.T) {
image := initImage("transparent.png")
options := Options{
Type: JPEG,
Background: Color{255, 255, 255},
}
buf, err := image.Process(options)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
Write("fixtures/test_transparent_image_convert_out.jpg", buf)
}
func TestImageMetadata(t *testing.T) {
data, err := initImage("test.png").Metadata()
if err != nil {
@ -196,6 +289,48 @@ func TestImageMetadata(t *testing.T) {
}
}
func TestInterpretation(t *testing.T) {
interpretation, err := initImage("test.jpg").Interpretation()
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
if interpretation != INTERPRETATION_sRGB {
t.Errorf("Invalid interpretation: %d", interpretation)
}
}
func TestImageColourspace(t *testing.T) {
tests := []struct {
file string
interpretation Interpretation
}{
{"test.jpg", INTERPRETATION_sRGB},
{"test.jpg", INTERPRETATION_B_W},
}
for _, test := range tests {
buf, err := initImage(test.file).Colourspace(test.interpretation)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
interpretation, err := ImageInterpretation(buf)
if interpretation != test.interpretation {
t.Errorf("Invalid colourspace")
}
}
}
func TestImageColourspaceIsSupported(t *testing.T) {
supported, err := initImage("test.jpg").ColourspaceIsSupported()
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
if supported != true {
t.Errorf("Non-supported colourspace")
}
}
func TestFluentInterface(t *testing.T) {
image := initImage("test.jpg")
_, err := image.CropByWidth(300)

@ -18,6 +18,7 @@ type ImageMetadata struct {
Profile bool
Type string
Space string
Colourspace string
Size ImageSize
}
@ -34,6 +35,17 @@ func Size(buf []byte) (ImageSize, error) {
}, nil
}
// Check in the image colourspace is supported by libvips
func ColourspaceIsSupported(buf []byte) (bool, error) {
return vipsColourspaceIsSupportedBuffer(buf)
}
// Get the image interpretation type
// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation
func ImageInterpretation(buf []byte) (Interpretation, error) {
return vipsInterpretationBuffer(buf)
}
// Extract the image metadata (size, type, alpha channel, profile, EXIF orientation...)
func Metadata(buf []byte) (ImageMetadata, error) {
defer C.vips_thread_shutdown()
@ -56,7 +68,7 @@ func Metadata(buf []byte) (ImageMetadata, error) {
Alpha: vipsHasAlpha(image),
Profile: vipsHasProfile(image),
Space: vipsSpace(image),
Type: getImageTypeName(imageType),
Type: ImageTypeName(imageType),
}
return metadata, nil

@ -17,7 +17,6 @@ func TestSize(t *testing.T) {
{"test.png", 400, 300},
{"test.webp", 550, 368},
}
for _, file := range files {
size, err := Size(readFile(file.name))
if err != nil {
@ -39,15 +38,15 @@ func TestMetadata(t *testing.T) {
profile bool
space string
}{
{"test.jpg", "jpeg", 0, false, false, "bicubic"},
{"test.png", "png", 0, true, false, "bicubic"},
{"test.webp", "webp", 0, false, false, "bicubic"},
{"test.jpg", "jpeg", 0, false, false, "srgb"},
{"test.png", "png", 0, true, false, "srgb"},
{"test.webp", "webp", 0, false, false, "srgb"},
}
for _, file := range files {
metadata, err := Metadata(readFile(file.name))
if err != nil {
t.Fatalf("Cannot read the image: %#v", err)
t.Fatalf("Cannot read the image: %s -> %s", file.name, err)
}
if metadata.Type != file.format {
@ -62,6 +61,58 @@ func TestMetadata(t *testing.T) {
if metadata.Profile != file.profile {
t.Fatalf("Unexpected image profile: %s != %s", metadata.Profile, file.profile)
}
if metadata.Space != file.space {
t.Fatalf("Unexpected image profile: %s != %s", metadata.Profile, file.profile)
}
}
}
func TestImageInterpretation(t *testing.T) {
files := []struct {
name string
interpretation Interpretation
}{
{"test.jpg", INTERPRETATION_sRGB},
{"test.png", INTERPRETATION_sRGB},
{"test.webp", INTERPRETATION_sRGB},
}
for _, file := range files {
interpretation, err := ImageInterpretation(readFile(file.name))
if err != nil {
t.Fatalf("Cannot read the image: %s -> %s", file.name, err)
}
if interpretation != file.interpretation {
t.Fatalf("Unexpected image interpretation")
}
}
}
func TestColourspaceIsSupported(t *testing.T) {
files := []struct {
name string
}{
{"test.jpg"},
{"test.png"},
{"test.webp"},
}
for _, file := range files {
supported, err := ColourspaceIsSupported(readFile(file.name))
if err != nil {
t.Fatalf("Cannot read the image: %s -> %s", file.name, err)
}
if supported != true {
t.Fatalf("Unsupported image colourspace")
}
}
supported, err := initImage("test.jpg").ColourspaceIsSupported()
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
if supported != true {
t.Errorf("Non-supported colourspace")
}
}

@ -42,10 +42,10 @@ func (i Interpolator) String() string {
type Angle int
const (
D0 Angle = C.VIPS_ANGLE_D0
D90 Angle = C.VIPS_ANGLE_D90
D180 Angle = C.VIPS_ANGLE_D180
D270 Angle = C.VIPS_ANGLE_D270
D0 Angle = 0
D90 Angle = 90
D180 Angle = 180
D270 Angle = 270
)
type Direction int
@ -55,11 +55,35 @@ const (
VERTICAL Direction = C.VIPS_DIRECTION_VERTICAL
)
// Image interpretation type
// See: http://www.vips.ecs.soton.ac.uk/supported/current/doc/html/libvips/VipsImage.html#VipsInterpretation
type Interpretation int
const (
INTERPRETATION_ERROR Interpretation = C.VIPS_INTERPRETATION_ERROR
INTERPRETATION_MULTIBAND Interpretation = C.VIPS_INTERPRETATION_MULTIBAND
INTERPRETATION_B_W Interpretation = C.VIPS_INTERPRETATION_B_W
INTERPRETATION_CMYK Interpretation = C.VIPS_INTERPRETATION_CMYK
INTERPRETATION_RGB Interpretation = C.VIPS_INTERPRETATION_RGB
INTERPRETATION_sRGB Interpretation = C.VIPS_INTERPRETATION_sRGB
INTERPRETATION_RGB16 Interpretation = C.VIPS_INTERPRETATION_RGB16
INTERPRETATION_GREY16 Interpretation = C.VIPS_INTERPRETATION_GREY16
INTERPRETATION_scRGB Interpretation = C.VIPS_INTERPRETATION_scRGB
INTERPRETATION_LAB Interpretation = C.VIPS_INTERPRETATION_LAB
INTERPRETATION_XYZ Interpretation = C.VIPS_INTERPRETATION_XYZ
)
const WATERMARK_FONT = "sans 10"
// Color represents a traditional RGB color scheme
type Color struct {
R, G, B uint8
}
// Shortcut to black RGB color representation
var ColorBlack = Color{0, 0, 0}
// Text-based watermark configuration
type Watermark struct {
Width int
DPI int
@ -71,6 +95,21 @@ type Watermark struct {
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 {
Height int
Width int
@ -87,11 +126,17 @@ type Options struct {
Embed bool
Flip bool
Flop bool
Force bool
NoAutoRotate bool
Colorspace bool
NoProfile bool
Interlace bool
Rotate Angle
Background Color
Gravity Gravity
Watermark Watermark
Type ImageType
Interpolator Interpolator
Interpretation Interpretation
GaussianBlur GaussianBlur
Sharpen Sharpen
}

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

@ -20,9 +20,51 @@ func TestResize(t *testing.T) {
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 {
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")
}
err = Write("fixtures/test_rotate_out.jpg", newImg)
if err != nil {
t.Fatal("Cannot save the image")
size, _ := Size(newImg)
if size.Height != options.Width {
t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
}
Write("fixtures/test_rotate_out.jpg", newImg)
}
func testColorspace(t *testing.T) {
options := Options{Colorspace: true}
buf, _ := Read("fixtures/sky.jpg")
func TestInvalidRotate(t *testing.T) {
options := Options{Width: 800, Height: 600, Rotate: 111}
buf, _ := Read("fixtures/test.jpg")
newImg, err := Resize(buf, options)
if err != nil {
@ -58,10 +102,12 @@ func testColorspace(t *testing.T) {
t.Fatal("Image is not jpeg")
}
err = Write("fixtures/test_color_out.jpg", newImg)
if err != nil {
t.Fatal("Cannot save the image")
size, _ := Size(newImg)
if size.Height != options.Width {
t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
}
Write("fixtures/test_invalid_rotate_out.jpg", newImg)
}
func TestCorruptedImage(t *testing.T) {
@ -77,14 +123,36 @@ func TestCorruptedImage(t *testing.T) {
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 {
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) {
options := Options{Width: 800, Height: 600, Rotate: 111}
size, _ := Size(newImg)
if size.Height != options.Height || size.Width != options.Width {
t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
}
}
func TestGaussianBlur(t *testing.T) {
options := Options{Width: 800, Height: 600, GaussianBlur: GaussianBlur{Sigma: 5}}
buf, _ := Read("fixtures/test.jpg")
newImg, err := Resize(buf, options)
@ -92,48 +160,70 @@ func TestInvalidRotate(t *testing.T) {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
}
if DetermineImageType(newImg) != JPEG {
t.Fatal("Image is not jpeg")
size, _ := Size(newImg)
if size.Height != options.Height || size.Width != options.Width {
t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
}
Write("fixtures/test_gaussian.jpg", newImg)
}
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 {
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) {
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}
img, err := os.Open("fixtures/test.jpg")
for _, file := range files {
img, err := os.Open("fixtures/" + file)
if err != nil {
t.Fatal(err)
}
defer img.Close()
buf, err := ioutil.ReadAll(img)
if err != nil {
t.Fatal(err)
}
img.Close()
for _, format := range formats {
options := Options{Width: width, Height: height, Crop: true, Type: format}
newImg, err := Resize(buf, options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
}
if DetermineImageType(newImg) != PNG {
if DetermineImageType(newImg) != format {
t.Fatal("Image is not png")
}
size, _ := Size(newImg)
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")
}
err = Write("fixtures/transparent_out.png", newImg)
if err != nil {
t.Fatal("Cannot save the image")
}
Write("fixtures/transparent_out.png", newImg)
}
func runBenchmarkResize(file string, o Options, b *testing.B) {
@ -180,6 +267,11 @@ func runBenchmarkResize(file string, o Options, b *testing.B) {
}
}
func BenchmarkRotateJpeg(b *testing.B) {
options := Options{Rotate: 180}
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkResizeLargeJpeg(b *testing.B) {
options := Options{
Width: 800,
@ -209,7 +301,17 @@ func BenchmarkConvertToJpeg(b *testing.B) {
runBenchmarkResize("test.png", options, b)
}
func BenchmarkCrop(b *testing.B) {
func BenchmarkConvertToPng(b *testing.B) {
options := Options{Type: PNG}
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkConvertToWebp(b *testing.B) {
options := Options{Type: WEBP}
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkCropJpeg(b *testing.B) {
options := Options{
Width: 800,
Height: 600,
@ -217,6 +319,22 @@ func BenchmarkCrop(b *testing.B) {
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkCropPng(b *testing.B) {
options := Options{
Width: 800,
Height: 600,
}
runBenchmarkResize("test.png", options, b)
}
func BenchmarkCropWebP(b *testing.B) {
options := Options{
Width: 800,
Height: 600,
}
runBenchmarkResize("test.webp", options, b)
}
func BenchmarkExtractJpeg(b *testing.B) {
options := Options{
Top: 100,
@ -226,3 +344,83 @@ func BenchmarkExtractJpeg(b *testing.B) {
}
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkExtractPng(b *testing.B) {
options := Options{
Top: 100,
Left: 50,
AreaWidth: 600,
AreaHeight: 480,
}
runBenchmarkResize("test.png", options, b)
}
func BenchmarkExtractWebp(b *testing.B) {
options := Options{
Top: 100,
Left: 50,
AreaWidth: 600,
AreaHeight: 480,
}
runBenchmarkResize("test.webp", options, b)
}
func BenchmarkZoomJpeg(b *testing.B) {
options := Options{Zoom: 1}
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkZoomPng(b *testing.B) {
options := Options{Zoom: 1}
runBenchmarkResize("test.png", options, b)
}
func BenchmarkZoomWebp(b *testing.B) {
options := Options{Zoom: 1}
runBenchmarkResize("test.webp", options, b)
}
func BenchmarkWatermarkJpeg(b *testing.B) {
options := Options{
Watermark: Watermark{
Text: "Chuck Norris (c) 2315",
Opacity: 0.25,
Width: 200,
DPI: 100,
Margin: 150,
Font: "sans bold 12",
Background: Color{255, 255, 255},
},
}
runBenchmarkResize("test.jpg", options, b)
}
func BenchmarkWatermarPng(b *testing.B) {
options := Options{
Watermark: Watermark{
Text: "Chuck Norris (c) 2315",
Opacity: 0.25,
Width: 200,
DPI: 100,
Margin: 150,
Font: "sans bold 12",
Background: Color{255, 255, 255},
},
}
runBenchmarkResize("test.png", options, b)
}
func BenchmarkWatermarWebp(b *testing.B) {
options := Options{
Watermark: Watermark{
Text: "Chuck Norris (c) 2315",
Opacity: 0.25,
Width: 200,
DPI: 100,
Margin: 150,
Font: "sans bold 12",
Background: Color{255, 255, 255},
},
}
runBenchmarkResize("test.webp", options, b)
}

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

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

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

@ -3,21 +3,32 @@ package bimg
/*
#cgo pkg-config: vips
#include "vips.h"
#include "stdlib.h"
*/
import "C"
import (
"errors"
"math"
"os"
"runtime"
"strings"
"sync"
"unsafe"
)
// Current libvips version
const VipsVersion = string(C.VIPS_VERSION)
const HasMagickSupport = int(C.VIPS_MAGICK_SUPPORT) == 1
const (
maxCacheMem = 100 * 1024 * 1024
maxCacheSize = 500
)
var (
m sync.Mutex
initialized bool = false
initialized bool
)
type VipsMemoryInfo struct {
@ -30,6 +41,9 @@ type vipsSaveOptions struct {
Quality int
Compression int
Type ImageType
Interlace bool
NoProfile bool
Interpretation Interpretation
}
type vipsWatermarkOptions struct {
@ -47,16 +61,16 @@ type vipsWatermarkTextOptions struct {
}
func init() {
if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 {
panic("unsupported old vips version!")
}
Initialize()
}
// Explicit thread-safe start of libvips.
// Only call this function if you've previously shutdown libvips
func Initialize() {
if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 {
panic("unsupported libvips version!")
}
m.Lock()
runtime.LockOSThread()
defer m.Unlock()
@ -64,30 +78,42 @@ func Initialize() {
err := C.vips_init(C.CString("bimg"))
if err != 0 {
Shutdown()
panic("unable to start vips!")
}
C.vips_concurrency_set(0) // default
C.vips_cache_set_max_mem(100 * 1024 * 1024) // 100 MB
C.vips_cache_set_max(500) // 500 operations
// Set libvips cache params
C.vips_cache_set_max_mem(maxCacheMem)
C.vips_cache_set_max(maxCacheSize)
// Define a custom thread concurrency limit in libvips (this may generate thread-unsafe issues)
// See: https://github.com/jcupitt/libvips/issues/261#issuecomment-92850414
if os.Getenv("VIPS_CONCURRENCY") == "" {
C.vips_concurrency_set(1)
}
// Enable libvips cache tracing
if os.Getenv("VIPS_TRACE") != "" {
C.vips_enable_cache_set_trace()
}
initialized = true
}
// Explicit thread-safe libvips shutdown. Call this to drop caches.
// Thread-safe function to shutdown libvips.
// You can call this to drop caches as well.
// If libvips was already initialized, the function is no-op
func Shutdown() {
m.Lock()
defer m.Unlock()
if initialized == true {
if initialized {
C.vips_shutdown()
initialized = false
}
}
// Output to stdout vips collected data. Useful for debugging
func VipsDebug() {
func VipsDebugInfo() {
C.im__print_all()
}
@ -100,28 +126,30 @@ func VipsMemory() VipsMemoryInfo {
}
}
func vipsExifOrientation(image *C.struct__VipsImage) int {
func vipsExifOrientation(image *C.VipsImage) int {
return int(C.vips_exif_orientation(image))
}
func vipsHasAlpha(image *C.struct__VipsImage) bool {
func vipsHasAlpha(image *C.VipsImage) bool {
return int(C.has_alpha_channel(image)) > 0
}
func vipsHasProfile(image *C.struct__VipsImage) bool {
func vipsHasProfile(image *C.VipsImage) bool {
return int(C.has_profile_embed(image)) > 0
}
func vipsWindowSize(name string) float64 {
return float64(C.interpolator_window_size(C.CString(name)))
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
return float64(C.interpolator_window_size(cname))
}
func vipsSpace(image *C.struct__VipsImage) string {
func vipsSpace(image *C.VipsImage) string {
return C.GoString(C.vips_enum_nick_bridge(image))
}
func vipsRotate(image *C.struct__VipsImage, angle Angle) (*C.struct__VipsImage, error) {
var out *C.struct__VipsImage
func vipsRotate(image *C.VipsImage, angle Angle) (*C.VipsImage, error) {
var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image))
err := C.vips_rotate(image, &out, C.int(angle))
@ -132,8 +160,8 @@ func vipsRotate(image *C.struct__VipsImage, angle Angle) (*C.struct__VipsImage,
return out, nil
}
func vipsFlip(image *C.struct__VipsImage, direction Direction) (*C.struct__VipsImage, error) {
var out *C.struct__VipsImage
func vipsFlip(image *C.VipsImage, direction Direction) (*C.VipsImage, error) {
var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image))
err := C.vips_flip_bridge(image, &out, C.int(direction))
@ -144,8 +172,8 @@ func vipsFlip(image *C.struct__VipsImage, direction Direction) (*C.struct__VipsI
return out, nil
}
func vipsZoom(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error) {
var out *C.struct__VipsImage
func vipsZoom(image *C.VipsImage, zoom int) (*C.VipsImage, error) {
var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image))
err := C.vips_zoom_bridge(image, &out, C.int(zoom), C.int(zoom))
@ -156,36 +184,8 @@ func vipsZoom(image *C.struct__VipsImage, zoom int) (*C.struct__VipsImage, error
return out, nil
}
func vipsColorSpace(image *C.struct__VipsImage) (*C.struct__VipsImage, error) {
var out *C.struct__VipsImage
var temp *C.struct__VipsImage
var max *C.double
var x *C.int
var y *C.int
defer C.g_object_unref(C.gpointer(image))
err := C.vips_colorspace_bridge(image, &out)
if err != 0 {
return nil, catchVipsError()
}
err = C.vips_hist_find_ndim_bridge(out, &temp)
if err != 0 {
return nil, catchVipsError()
}
err = C.vips_max_bridge(temp, max, &x, &y)
if err != 0 {
return nil, catchVipsError()
}
debug("MAX VALUE %dx%d", x, y)
return temp, nil
}
func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImage, error) {
var out *C.struct__VipsImage
func vipsWatermark(image *C.VipsImage, w Watermark) (*C.VipsImage, error) {
var out *C.VipsImage
// Defaults
noReplicate := 0
@ -203,7 +203,7 @@ func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag
defer C.free(unsafe.Pointer(text))
defer C.free(unsafe.Pointer(font))
err := C.vips_watermark(image, &out, (*C.watermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.watermarkOptions)(unsafe.Pointer(&opts)))
err := C.vips_watermark(image, &out, (*C.WatermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.WatermarkOptions)(unsafe.Pointer(&opts)))
if err != 0 {
return nil, catchVipsError()
}
@ -211,8 +211,8 @@ func vipsWatermark(image *C.struct__VipsImage, w Watermark) (*C.struct__VipsImag
return out, nil
}
func vipsRead(buf []byte) (*C.struct__VipsImage, ImageType, error) {
var image *C.struct__VipsImage
func vipsRead(buf []byte) (*C.VipsImage, ImageType, error) {
var image *C.VipsImage
imageType := vipsImageType(buf)
if imageType == UNKNOWN {
@ -230,26 +230,105 @@ func vipsRead(buf []byte) (*C.struct__VipsImage, ImageType, error) {
return image, imageType, nil
}
func vipsSave(image *C.struct__VipsImage, o vipsSaveOptions) ([]byte, error) {
var ptr unsafe.Pointer
length := C.size_t(0)
err := C.int(0)
func vipsColourspaceIsSupportedBuffer(buf []byte) (bool, error) {
image, _, err := vipsRead(buf)
if err != nil {
return false, err
}
C.g_object_unref(C.gpointer(image))
return vipsColourspaceIsSupported(image), nil
}
func vipsColourspaceIsSupported(image *C.VipsImage) bool {
return int(C.vips_colourspace_issupported_bridge(image)) == 1
}
func vipsInterpretationBuffer(buf []byte) (Interpretation, error) {
image, _, err := vipsRead(buf)
if err != nil {
return INTERPRETATION_ERROR, err
}
C.g_object_unref(C.gpointer(image))
return vipsInterpretation(image), nil
}
func vipsInterpretation(image *C.VipsImage) Interpretation {
return Interpretation(C.vips_image_guess_interpretation_bridge(image))
}
func vipsFlattenBackground(image *C.VipsImage, background Color) (*C.VipsImage, error) {
var outImage *C.VipsImage
backgroundC := [3]C.double{
C.double(background.R),
C.double(background.G),
C.double(background.B),
}
err := C.vips_flatten_background_brigde(image, &outImage, (*C.double)(&backgroundC[0]))
if int(err) != 0 {
return nil, catchVipsError()
}
C.g_object_unref(C.gpointer(image))
image = outImage
return image, nil
}
func vipsPreSave(image *C.VipsImage, o *vipsSaveOptions) (*C.VipsImage, error) {
// Remove ICC profile metadata
if o.NoProfile {
C.remove_profile(image)
}
// Use a default interpretation and cast it to C type
if o.Interpretation == 0 {
o.Interpretation = INTERPRETATION_sRGB
}
interpretation := C.VipsInterpretation(o.Interpretation)
// Apply the proper colour space
var outImage *C.VipsImage
if vipsColourspaceIsSupported(image) {
err := C.vips_colourspace_bridge(image, &outImage, interpretation)
if int(err) != 0 {
return nil, catchVipsError()
}
image = outImage
}
return image, nil
}
func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) {
defer C.g_object_unref(C.gpointer(image))
switch {
case o.Type == PNG:
err = C.vips_pngsave_bridge(image, &ptr, &length, 1, C.int(o.Compression), C.int(o.Quality), 0)
tmpImage, err := vipsPreSave(image, &o)
if err != nil {
return nil, err
}
defer C.g_object_unref(C.gpointer(tmpImage))
length := C.size_t(0)
saveErr := C.int(0)
interlace := C.int(boolToInt(o.Interlace))
quality := C.int(o.Quality)
var ptr unsafe.Pointer
switch o.Type {
case WEBP:
saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, 1, quality)
break
case o.Type == WEBP:
err = C.vips_webpsave_bridge(image, &ptr, &length, 1, C.int(o.Quality), 0)
case PNG:
saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, 1, C.int(o.Compression), quality, interlace)
break
default:
err = C.vips_jpegsave_bridge(image, &ptr, &length, 1, C.int(o.Quality), 0)
saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, 1, quality, interlace)
break
}
if int(err) != 0 {
if int(saveErr) != 0 {
return nil, catchVipsError()
}
@ -262,14 +341,19 @@ func vipsSave(image *C.struct__VipsImage, o vipsSaveOptions) ([]byte, error) {
return buf, nil
}
func vipsExtract(image *C.struct__VipsImage, left, top, width, height int) (*C.struct__VipsImage, error) {
var buf *C.struct__VipsImage
func max(x int) int {
return int(math.Max(float64(x), 0))
}
func vipsExtract(image *C.VipsImage, left, top, width, height int) (*C.VipsImage, error) {
var buf *C.VipsImage
defer C.g_object_unref(C.gpointer(image))
if width > MAX_SIZE || height > MAX_SIZE {
return nil, errors.New("Maximum image size exceeded")
}
top, left = max(top), max(left)
err := C.vips_extract_area_bridge(image, &buf, C.int(left), C.int(top), C.int(width), C.int(height))
if err != 0 {
return nil, catchVipsError()
@ -278,11 +362,12 @@ func vipsExtract(image *C.struct__VipsImage, left, top, width, height int) (*C.s
return buf, nil
}
func vipsShrinkJpeg(buf []byte, input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, error) {
var image *C.struct__VipsImage
func vipsShrinkJpeg(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, error) {
var image *C.VipsImage
var ptr = unsafe.Pointer(&buf[0])
defer C.g_object_unref(C.gpointer(input))
err := C.vips_jpegload_buffer_shrink(unsafe.Pointer(&buf[0]), C.size_t(len(buf)), &image, C.int(shrink))
err := C.vips_jpegload_buffer_shrink(ptr, C.size_t(len(buf)), &image, C.int(shrink))
if err != 0 {
return nil, catchVipsError()
}
@ -290,8 +375,8 @@ func vipsShrinkJpeg(buf []byte, input *C.struct__VipsImage, shrink int) (*C.stru
return image, nil
}
func vipsShrink(input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, error) {
var image *C.struct__VipsImage
func vipsShrink(input *C.VipsImage, shrink int) (*C.VipsImage, error) {
var image *C.VipsImage
defer C.g_object_unref(C.gpointer(input))
err := C.vips_shrink_bridge(input, &image, C.double(float64(shrink)), C.double(float64(shrink)))
@ -302,8 +387,8 @@ func vipsShrink(input *C.struct__VipsImage, shrink int) (*C.struct__VipsImage, e
return image, nil
}
func vipsEmbed(input *C.struct__VipsImage, left, top, width, height, extend int) (*C.struct__VipsImage, error) {
var image *C.struct__VipsImage
func vipsEmbed(input *C.VipsImage, left, top, width, height, extend int) (*C.VipsImage, error) {
var image *C.VipsImage
defer C.g_object_unref(C.gpointer(input))
err := C.vips_embed_bridge(input, &image, C.int(left), C.int(top), C.int(width), C.int(height), C.int(extend))
@ -314,16 +399,16 @@ func vipsEmbed(input *C.struct__VipsImage, left, top, width, height, extend int)
return image, nil
}
func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (*C.struct__VipsImage, error) {
var image *C.struct__VipsImage
istring := C.CString(i.String())
interpolator := C.vips_interpolate_new(istring)
func vipsAffine(input *C.VipsImage, residualx, residualy float64, i Interpolator) (*C.VipsImage, error) {
var image *C.VipsImage
cstring := C.CString(i.String())
interpolator := C.vips_interpolate_new(cstring)
defer C.free(unsafe.Pointer(istring))
defer C.free(unsafe.Pointer(cstring))
defer C.g_object_unref(C.gpointer(input))
defer C.g_object_unref(C.gpointer(interpolator))
err := C.vips_affine_interpolator(input, &image, C.double(residual), 0, 0, C.double(residual), interpolator)
err := C.vips_affine_interpolator(input, &image, C.double(residualx), 0, 0, C.double(residualy), interpolator)
if err != 0 {
return nil, catchVipsError()
}
@ -331,36 +416,37 @@ func vipsAffine(input *C.struct__VipsImage, residual float64, i Interpolator) (*
return image, nil
}
func vipsImageType(buf []byte) ImageType {
imageType := UNKNOWN
if len(buf) == 0 {
return imageType
func vipsImageType(bytes []byte) ImageType {
if len(bytes) == 0 {
return UNKNOWN
}
length := C.size_t(len(buf))
imageBuf := unsafe.Pointer(&buf[0])
bufferType := C.GoString(C.vips_foreign_find_load_buffer(imageBuf, length))
if bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47 {
return PNG
}
if bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF {
return JPEG
}
if bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50 {
return WEBP
}
if (bytes[0] == 0x49 && bytes[1] == 0x49 && bytes[2] == 0x2A && bytes[3] == 0x0) ||
(bytes[0] == 0x4D && bytes[1] == 0x4D && bytes[2] == 0x0 && bytes[3] == 0x2A) {
return TIFF
}
if HasMagickSupport && strings.HasSuffix(readImageType(bytes), "MagickBuffer") {
return MAGICK
}
switch {
case strings.HasSuffix(bufferType, "JpegBuffer"):
imageType = JPEG
break
case strings.HasSuffix(bufferType, "PngBuffer"):
imageType = PNG
break
case strings.HasSuffix(bufferType, "TiffBuffer"):
imageType = TIFF
break
case strings.HasSuffix(bufferType, "WebpBuffer"):
imageType = WEBP
break
case strings.HasSuffix(bufferType, "MagickBuffer"):
imageType = MAGICK
break
return UNKNOWN
}
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 {
@ -369,3 +455,32 @@ func catchVipsError() error {
C.vips_thread_shutdown()
return errors.New(s)
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
func vipsGaussianBlur(image *C.VipsImage, o GaussianBlur) (*C.VipsImage, error) {
var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image))
err := C.vips_gaussblur_bridge(image, &out, C.double(o.Sigma), C.double(o.MinAmpl))
if err != 0 {
return nil, catchVipsError()
}
return out, nil
}
func vipsSharpen(image *C.VipsImage, o Sharpen) (*C.VipsImage, error) {
var out *C.VipsImage
defer C.g_object_unref(C.gpointer(image))
err := C.vips_sharpen_bridge(image, &out, C.int(o.Radius), C.double(o.X1), C.double(o.Y2), C.double(o.Y3), C.double(o.M1), C.double(o.M2))
if err != 0 {
return nil, catchVipsError()
}
return out, nil
}

330
vips.h

@ -2,6 +2,28 @@
#include <vips/vips.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 {
UNKNOWN = 0,
JPEG,
@ -12,9 +34,9 @@ enum types {
};
typedef struct {
char *Text;
char *Font;
} watermarkTextOptions;
const char *Text;
const char *Font;
} WatermarkTextOptions;
typedef struct {
int Width;
@ -23,35 +45,80 @@ typedef struct {
int NoReplicate;
float Opacity;
double Background[3];
} watermarkOptions;
} WatermarkOptions;
static int
has_profile_embed(VipsImage *image) {
return vips_image_get_typeof(image, VIPS_META_ICC_NAME);
}
static void
remove_profile(VipsImage *image) {
vips_image_remove(image, VIPS_META_ICC_NAME);
}
static gboolean
with_interlace(int interlace) {
return interlace > 0 ? TRUE : FALSE;
}
static int
has_alpha_channel(VipsImage *image) {
return (
(image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) ||
(image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) ||
(image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK)
) ? 1 : 0;
}
/**
* This method is here to handle the weird initialization of the vips lib.
* libvips use a macro VIPS_INIT() that call vips__init() in version < 7.41,
* or calls vips_init() in version >= 7.41.
*
* Anyway, it's not possible to build bimg on Debian Jessie with libvips 7.40.x,
* as vips_init() is a macro to VIPS_INIT(), which is also a macro, hence, cgo
* is unable to determine the return type of vips_init(), making the build impossible.
* In order to correctly build bimg, for version < 7.41, we should undef vips_init and
* creates a vips_init() method that calls VIPS_INIT().
*/
#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41)
#undef vips_init
int
vips_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);
};
}
int
vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink)
{
vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) {
return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL);
};
}
int
vips_flip_bridge(VipsImage *in, VipsImage **out, int direction)
{
vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) {
return vips_flip(in, out, direction, NULL);
};
}
int
vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink)
{
vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) {
return vips_shrink(in, out, xshrink, yshrink, NULL);
};
}
int
vips_rotate(VipsImage *in, VipsImage **buf, int angle)
{
vips_rotate(VipsImage *in, VipsImage **out, int angle) {
int rotate = VIPS_ANGLE_D0;
if (angle == 90) {
@ -62,35 +129,21 @@ vips_rotate(VipsImage *in, VipsImage **buf, int angle)
rotate = VIPS_ANGLE_D270;
}
return vips_rot(in, buf, rotate, NULL);
};
return vips_rot(in, out, rotate, NULL);
}
int
vips_exif_orientation(VipsImage *image) {
int orientation = 0;
const char **exif;
const char *exif;
if (
vips_image_get_typeof(image, "exif-ifd0-Orientation") != 0 &&
!vips_image_get_string(image, "exif-ifd0-Orientation", exif)
vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 &&
!vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif)
) {
orientation = atoi(exif[0]);
orientation = atoi(&exif[0]);
}
return orientation;
};
int
has_profile_embed(VipsImage *image) {
return (vips_image_get_typeof(image, VIPS_META_ICC_NAME) > 0) ? 1 : 0;
};
int
has_alpha_channel(VipsImage *image) {
return (
(image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) ||
(image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) ||
(image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK)
) ? 1 : 0;
};
}
int
interpolator_window_size(char const *name) {
@ -98,102 +151,137 @@ interpolator_window_size(char const *name) {
int window_size = vips_interpolate_get_window_size(interpolator);
g_object_unref(interpolator);
return window_size;
};
}
const char *
vips_enum_nick_bridge(VipsImage *image) {
return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
};
}
int
vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac)
{
vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) {
return vips_zoom(in, out, xfac, yfac, NULL);
};
}
int
vips_colorspace_bridge(VipsImage *in, VipsImage **out)
{
return vips_colourspace(in, out, VIPS_INTERPRETATION_LAB, NULL);
};
vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend) {
return vips_embed(in, out, left, top, width, height, "extend", extend, NULL);
}
int
vips_hist_find_ndim_bridge(VipsImage *in, VipsImage **out)
{
return vips_hist_find_ndim(in, out, "bins", 5, NULL);
};
vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) {
return vips_extract_area(in, out, left, top, width, height, NULL);
}
int
vips_max_bridge(VipsImage *in, double *out, int **x, int **y)
{
double ones[3] = { 1, 1, 1 };
return vips_max(in, ones, "x", x, "y", y, NULL);
};
vips_colourspace_issupported_bridge(VipsImage *in) {
return vips_colourspace_issupported(in) ? 1 : 0;
}
int
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);
};
VipsInterpretation
vips_image_guess_interpretation_bridge(VipsImage *in) {
return vips_image_guess_interpretation(in);
}
int
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);
};
vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) {
return vips_colourspace(in, out, space, NULL);
}
int
vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace)
{
return vips_jpegsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL);
};
vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) {
return vips_jpegsave_buffer(in, buf, len,
"strip", strip,
"Q", quality,
"optimize_coding", TRUE,
"interlace", with_interlace(interlace),
NULL
);
}
int
vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace)
{
vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) {
#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
return vips_pngsave_buffer(in, buf, len, "strip", FALSE, "compression", compression,
"interlace", interlace, "filter", VIPS_FOREIGN_PNG_FILTER_NONE, NULL);
return vips_pngsave_buffer(in, buf, len,
"strip", FALSE,
"compression", compression,
"interlace", with_interlace(interlace),
"filter", VIPS_FOREIGN_PNG_FILTER_NONE,
NULL
);
#else
return vips_pngsave_buffer(in, buf, len, "strip", FALSE, "compression", compression,
"interlace", interlace, NULL);
return vips_pngsave_buffer(in, buf, len,
"strip", FALSE,
"compression", compression,
"interlace", with_interlace(interlace),
NULL
);
#endif
};
}
int
vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace)
{
return vips_webpsave_buffer(in, buf, len, "strip", strip, "Q", quality, "optimize_coding", TRUE, "interlace", interlace, NULL);
};
vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality) {
return vips_webpsave_buffer(in, buf, len,
"strip", strip,
"Q", quality,
NULL
);
}
int
vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double background[3]) {
VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3);
return vips_flatten(in, out,
"background", vipsBackground,
NULL
);
}
int
vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) {
int code = 1;
if (imageType == JPEG) {
code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL);
} else if (imageType == PNG) {
code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL);
} else if (imageType == WEBP) {
code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL);
} else if (imageType == TIFF) {
code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL);
#if (VIPS_MAJOR_VERSION >= 8)
} else if (imageType == MAGICK) {
code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL);
#endif
}
return code;
};
}
int
vips_watermark(VipsImage *in, VipsImage **out, watermarkTextOptions *to, watermarkOptions *o)
{
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 };
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;
// Make the mask.
@ -205,48 +293,60 @@ vips_watermark(VipsImage *in, VipsImage **out, watermarkTextOptions *to, waterma
NULL) ||
vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) ||
vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) ||
vips_embed(t[3], &t[4], 100, 100,
t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL)
vips_embed(t[3], &t[4], 100, 100, t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL)
) {
g_object_unref(base);
return (1);
return 1;
}
// Replicate if necessary
if (o->NoReplicate != 1 && (
vips_replicate(t[4], &t[5],
1 + t[0]->Xsize / t[4]->Xsize,
1 + t[0]->Ysize / t[4]->Ysize, NULL) ||
vips_crop(t[5], &t[6], 0, 0,
t[0]->Xsize, t[0]->Ysize, NULL)
)) {
if (o->NoReplicate != 1) {
VipsImage *cache = vips_image_new();
if (vips_watermark_replicate(t[0], t[4], &cache)) {
g_object_unref(cache);
g_object_unref(base);
return (1);
return 1;
}
g_object_unref(t[4]);
t[4] = cache;
}
// Make the constant image to paint the text with.
if (
vips_black(&t[7], 1, 1, NULL) ||
vips_linear( t[7], &t[8], ones, o->Background, 3, NULL) ||
vips_cast(t[8], &t[9], VIPS_FORMAT_UCHAR, NULL) ||
vips_copy(t[9], &t[10],
"interpretation", t[0]->Type,
NULL) ||
vips_embed(t[10], &t[11], 0, 0,
t[0]->Xsize, t[0]->Ysize,
"extend", VIPS_EXTEND_COPY,
NULL)
vips_black(&t[5], 1, 1, NULL) ||
vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) ||
vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) ||
vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) ||
vips_embed(t[8], &t[9], 0, 0, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_COPY, NULL)
) {
g_object_unref(base);
return (1);
return 1;
}
// 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);
return (1);
return 1;
}
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 {
img, _ := os.Open(path.Join("fixtures", file.name))
buf, _ := ioutil.ReadAll(img)
defer img.Close()
image, imageType, _ := vipsRead(buf)
image, imageType, _ := vipsRead(readImage(file.name))
if image == nil {
t.Fatal("Empty image")
}
if imageType != file.expected {
t.Fatal("Empty image")
t.Fatal("Invalid image type")
}
}
}
func TestVipsSave(t *testing.T) {
img, _ := os.Open(path.Join("fixtures", "test.jpg"))
buf, _ := ioutil.ReadAll(img)
defer img.Close()
image, _, _ := vipsRead(readImage("test.jpg"))
options := vipsSaveOptions{Quality: 95, Type: JPEG, Interlace: true}
image, _, _ := vipsRead(buf)
if image == nil {
buf, err := vipsSave(image, options)
if err != nil {
t.Fatal("Cannot save the image")
}
if len(buf) == 0 {
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 {
t.Fatal("Cannot save the image")
}
buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95})
if len(buf) == 0 {
t.Fatal("Empty image")
}
}
func TestVipsWatermark(t *testing.T) {
image, _, _ := vipsRead(readImage("test.jpg"))
watermark := Watermark{
Text: "Copy me if you can",
Font: "sans bold 12",
Opacity: 0.5,
Width: 200,
DPI: 100,
Margin: 100,
Background: Color{255, 255, 255},
}
newImg, err := vipsWatermark(image, watermark)
if err != nil {
t.Errorf("Cannot add watermark: %s", err)
}
buf, _ := vipsSave(newImg, vipsSaveOptions{Quality: 95})
if len(buf) == 0 {
t.Fatal("Empty image")
}
}
func TestVipsImageType(t *testing.T) {
imgType := vipsImageType(readImage("test.jpg"))
if imgType != JPEG {
t.Fatal("Invalid image type")
}
}
func TestVipsMemory(t *testing.T) {
mem := VipsMemory()
if mem.Memory < 1024 {
t.Fatal("Invalid memory")
}
if mem.Allocations == 0 {
t.Fatal("Invalid memory allocations")
}
}
func readImage(file string) []byte {
img, _ := os.Open(path.Join("fixtures", file))
buf, _ := ioutil.ReadAll(img)
defer img.Close()
return buf
}

Loading…
Cancel
Save