refactor(resize): extract

This commit is contained in:
Tomas Aparicio 2015-04-06 23:43:30 +02:00
parent d9eac32a9a
commit 3904953399
9 changed files with 246 additions and 173 deletions

View file

@ -1,15 +1,18 @@
# 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)]() [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.png)](https://godoc.org/github.com/h2non/bimg) # 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)]() [![GoDoc](https://godoc.org/github.com/h2non/bimg?status.png)](https://godoc.org/github.com/h2non/bimg)
Go library for blazing fast image processing based on [libvips](https://github.com/jcupitt/libvips) using C bindings. Go small but featured library for blazing fast image processing based on [libvips](https://github.com/jcupitt/libvips) using C bindings.
bimg is designed to be a small and efficient library with a limited by generic specific set of features. bimg is designed to be a small and efficient library with a specific but useful and generic set of features.
Thanks to libvips it's typically 4x faster than using the quickest ImageMagick and GraphicsMagick settings It uses internally libvips, which is 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.
or Go native image processing package, and in some cases it's even 8x faster processing JPEG images. bimg can read JPEG, PNG, WEBP, TIFF and Magick formats and it can output to JPEG, PNG and WEBP.
It supports JPEG, PNG, WEBP, TIFF and Magick image formats.
It support common [image transformation](#supported-image-operations) operations such as crop, resize, rotate... and image conversion into multiple formats.
To getting started see the [examples](#examples) and [programmatic API](https://godoc.org/github.com/h2non/bimg) documentation.
bimg was heavily inspired in [sharp](https://github.com/lovell/sharp), a great node.js package for image processing build by [Lovell Fuller](https://github.com/lovell). bimg was heavily inspired in [sharp](https://github.com/lovell/sharp), a great node.js package for image processing build by [Lovell Fuller](https://github.com/lovell).
`Work in progress` **Note**: bimg is still a beta package. PR and issues are highly appreciated
## Prerequisites ## Prerequisites
@ -38,10 +41,10 @@ The [install script](https://github.com/lovell/sharp/blob/master/preinstall.sh)
- Enlarge - Enlarge
- Crop - Crop
- Rotate - Rotate
- Flip/Flop - Flip
- Extract area - Extract area
- Extract image metadata (size, format, profile, orientation...) - EXIF metadata (size, alpha channel, profile, orientation...)
- Image conversion to multiple formats - Format conversion
## Performance ## Performance

View file

@ -6,15 +6,20 @@ import (
) )
func Read(path string) ([]byte, error) { func Read(path string) ([]byte, error) {
data, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
buf, err := ioutil.ReadAll(data) buf, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return buf, nil return buf, nil
} }
func Write(path string, buf []byte) error {
return ioutil.WriteFile(path, buf, 0644)
}

View file

@ -19,3 +19,20 @@ func TestRead(t *testing.T) {
t.Fatal("Image is not jpeg") t.Fatal("Image is not jpeg")
} }
} }
func TestWrite(t *testing.T) {
buf, err := Read("fixtures/test.jpg")
if err != nil {
t.Errorf("Cannot read the image: %#v", err)
}
if len(buf) == 0 {
t.Fatal("Empty buffer")
}
err = Write("fixtures/test_write_out.jpg", buf)
if err != nil {
t.Fatal("Cannot write the file: %#v", err)
}
}

View file

@ -4,7 +4,7 @@ type Image struct {
buffer []byte buffer []byte
} }
func (i *Image) Resize(width int, height int) ([]byte, error) { func (i *Image) Resize(width, height int) ([]byte, error) {
options := Options{ options := Options{
Width: width, Width: width,
Height: height, Height: height,
@ -12,17 +12,17 @@ func (i *Image) Resize(width int, height int) ([]byte, error) {
return i.Process(options) return i.Process(options)
} }
func (i *Image) Extract(top int, left int, width int, height int) ([]byte, error) { func (i *Image) Extract(top, left, width, height int) ([]byte, error) {
options := Options{ options := Options{
Width: width, Top: top,
Height: height, Left: left,
Top: top, AreaWidth: width,
Left: left, AreaHeight: height,
} }
return i.Process(options) return i.Process(options)
} }
func (i *Image) Crop(width int, height int) ([]byte, error) { func (i *Image) Crop(width, height int) ([]byte, error) {
options := Options{ options := Options{
Width: width, Width: width,
Height: height, Height: height,
@ -31,14 +31,6 @@ func (i *Image) Crop(width int, height int) ([]byte, error) {
return i.Process(options) return i.Process(options)
} }
func (i *Image) Thumbnail(width int, height int) ([]byte, error) {
options := Options{
Width: width,
Height: height,
}
return i.Process(options)
}
func (i *Image) Rotate(a Angle) ([]byte, error) { func (i *Image) Rotate(a Angle) ([]byte, error) {
options := Options{Rotate: a} options := Options{Rotate: a}
return i.Process(options) return i.Process(options)
@ -49,11 +41,6 @@ func (i *Image) Flip() ([]byte, error) {
return i.Process(options) return i.Process(options)
} }
func (i *Image) Flop() ([]byte, error) {
options := Options{Flip: HORIZONTAL}
return i.Process(options)
}
func (i *Image) Convert(t ImageType) ([]byte, error) { func (i *Image) Convert(t ImageType) ([]byte, error) {
options := Options{Type: t} options := Options{Type: t}
return i.Process(options) return i.Process(options)

View file

@ -1,37 +1,59 @@
package bimg package bimg
import ( import (
"io/ioutil" "path"
"os"
"testing" "testing"
) )
func TestImageResize(t *testing.T) { func TestImageResize(t *testing.T) {
image := readImage() buf, err := initImage("test.jpg").Resize(300, 240)
_, err := image.Resize(300, 240)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %#v", err)
} }
Write("fixtures/test_resize_out.jpg", buf)
}
func TestImageExtract(t *testing.T) {
buf, err := initImage("test.jpg").Extract(100, 100, 300, 300)
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
Write("fixtures/test_extract_out.jpg", buf)
} }
func TestImageCrop(t *testing.T) { func TestImageCrop(t *testing.T) {
image := readImage() buf, err := initImage("test.jpg").Crop(800, 600)
_, err := image.Crop(800, 600)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %#v", err)
} }
Write("fixtures/test_crop_out.jpg", buf)
}
func TestImageFlip(t *testing.T) {
buf, err := initImage("test.jpg").Flip()
if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
Write("fixtures/test_flip_out.jpg", buf)
} }
func TestImageRotate(t *testing.T) { func TestImageRotate(t *testing.T) {
image := readImage() buf, err := initImage("test_flip_out.jpg").Rotate(90)
_, err := image.Rotate(D90)
if err != nil { if err != nil {
t.Errorf("Cannot process the image: %#v", err) t.Errorf("Cannot process the image: %#v", err)
} }
Write("fixtures/test_image_rotate_out.jpg", buf)
} }
func readImage() *Image { func TestImageConvert(t *testing.T) {
data, _ := os.Open("fixtures/test.jpg") buf, err := initImage("test_rotate_out.jpg").Convert(PNG)
buf, _ := ioutil.ReadAll(data) if err != nil {
t.Errorf("Cannot process the image: %#v", err)
}
Write("fixtures/test_image_convert_out.png", buf)
}
func initImage(file string) *Image {
buf, _ := Read(path.Join("fixtures", file))
return NewImage(buf) return NewImage(buf)
} }

View file

@ -55,6 +55,8 @@ const (
type Options struct { type Options struct {
Height int Height int
Width int Width int
AreaHeight int
AreaWidth int
Top int Top int
Left int Left int
Crop bool Crop bool

236
resize.go
View file

@ -8,6 +8,7 @@ import "C"
import ( import (
"errors" "errors"
"fmt"
"math" "math"
) )
@ -19,7 +20,7 @@ func Resize(buf []byte, o Options) ([]byte, error) {
return nil, err return nil, err
} }
// defaults // Defaults
if o.Quality == 0 { if o.Quality == 0 {
o.Quality = QUALITY o.Quality = QUALITY
} }
@ -36,7 +37,6 @@ func Resize(buf []byte, o Options) ([]byte, error) {
debug("Options: %#v", o) debug("Options: %#v", o)
// get WxH
inWidth := int(image.Xsize) inWidth := int(image.Xsize)
inHeight := int(image.Ysize) inHeight := int(image.Ysize)
@ -58,7 +58,6 @@ func Resize(buf []byte, o Options) ([]byte, error) {
// Try to use libjpeg shrink-on-load // Try to use libjpeg shrink-on-load
if imageType == JPEG && shrink >= 2 { if imageType == JPEG && shrink >= 2 {
// Recalculate integral shrink and double residual
tmpImage, factor, err := shrinkJpegImage(buf, factor, shrink) tmpImage, factor, err := shrinkJpegImage(buf, factor, shrink)
if err != nil { if err != nil {
return nil, err return nil, err
@ -78,60 +77,44 @@ func Resize(buf []byte, o Options) ([]byte, error) {
shrink = int(math.Max(float64(math.Floor(factor*3.0/windowSize)), 1)) shrink = int(math.Max(float64(math.Floor(factor*3.0/windowSize)), 1))
} }
// Use vips_shrink with the integral reduction // Transform image if necessary
if shrink > 1 { if o.Width != inWidth || o.Height != inHeight {
image, residual, err = shrinkImage(image, o, residual, shrink) // Use vips_shrink with the integral reduction
if shrink > 1 {
image, residual, err = shrinkImage(image, o, residual, shrink)
if err != nil {
return nil, err
}
}
// Use vips_affine with the remaining float part
if residual != 0 {
image, err = vipsAffine(image, residual, o.Interpolator)
if err != nil {
return nil, err
}
}
debug("factor: %v, shrink: %v, residual: %v", factor, shrink, residual)
// Extract area from image
image, err = extractImage(image, o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
// Use vips_affine with the remaining float part // Rotate / flip image if necessary based on EXIF metadata
if residual != 0 { image, err = rotateImage(image, o)
image, err = vipsAffine(image, residual, o.Interpolator)
if err != nil {
return nil, err
}
}
debug("factor: %v, shrink: %v, residual: %v", factor, shrink, residual)
// Extract image
image, err = extractImage(image, o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if o.Rotate == 0 {
rotation, flip := calculateRotationAndFlip(image, o.Rotate)
if flip {
o.Flip = HORIZONTAL
}
if rotation > D0 {
o.Rotate = rotation
}
}
if o.Rotate > 0 {
image, err = vipsRotate(image, getAngle(o.Rotate))
if err != nil {
return nil, err
}
}
if o.Flip > 0 {
image, err = vipsFlip(image, o.Flip)
if err != nil {
return nil, err
}
}
saveOptions := vipsSaveOptions{ saveOptions := vipsSaveOptions{
Quality: o.Quality, Quality: o.Quality,
Type: o.Type, Type: o.Type,
Compression: o.Compression, Compression: o.Compression,
} }
// Finally save as buffer
buf, err = vipsSave(image, saveOptions) buf, err = vipsSave(image, saveOptions)
if err != nil { if err != nil {
return nil, err return nil, err
@ -142,58 +125,130 @@ func Resize(buf []byte, o Options) ([]byte, error) {
func extractImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, error) { func extractImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, error) {
var err error = nil var err error = nil
affinedWidth := int(image.Xsize) inWidth := int(image.Xsize)
affinedHeight := int(image.Ysize) inHeight := int(image.Ysize)
if affinedWidth != o.Width || affinedHeight != o.Height { switch {
width := int(math.Min(float64(affinedWidth), float64(o.Width))) case o.Crop:
height := int(math.Min(float64(affinedHeight), float64(o.Height))) width := int(math.Min(float64(inWidth), float64(o.Width)))
height := int(math.Min(float64(inHeight), float64(o.Height)))
switch { left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity)
case o.Crop: image, err = vipsExtract(image, left, top, width, height)
left, top := calculateCrop(affinedWidth, affinedHeight, o.Width, o.Height, o.Gravity) break
image, err = vipsExtract(image, left, top, width, height) case o.Embed:
break left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2
case o.Embed: image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend)
left, top := (o.Width-affinedWidth)/2, (o.Height-affinedHeight)/2 break
image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend) case o.Top > 0 && o.Left > 0:
break if o.AreaWidth == 0 || o.AreaHeight == 0 {
case o.Top > 0 && o.Left > 0: err = errors.New(fmt.Sprintf("Invalid area to extract %dx%d", o.AreaWidth, o.AreaHeight))
image, err = vipsExtract(image, o.Left, o.Top, width, height) } else {
break image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight)
} }
break
} }
return image, err return image, err
} }
func rotateImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, error) {
var err error
rotation, flip := calculateRotationAndFlip(image, o.Rotate)
if flip {
o.Flip = HORIZONTAL
}
if rotation > D0 && o.Rotate == 0 {
o.Rotate = rotation
}
if o.Rotate > 0 {
image, err = vipsRotate(image, getAngle(o.Rotate))
}
if o.Flip > 0 {
image, err = vipsFlip(image, o.Flip)
}
return image, err
}
func shrinkImage(image *C.struct__VipsImage, o Options, residual float64, shrink int) (*C.struct__VipsImage, float64, error) {
// Use vips_shrink with the integral reduction
image, err := vipsShrink(image, shrink)
if err != nil {
return nil, 0, err
}
// Recalculate residual float based on dimensions of required vs shrunk images
residualx := float64(o.Width) / float64(image.Xsize)
residualy := float64(o.Height) / float64(image.Ysize)
if o.Crop {
residual = math.Max(residualx, residualy)
} else {
residual = math.Min(residualx, residualy)
}
return image, residual, nil
}
func shrinkJpegImage(buf []byte, factor float64, shrink int) (*C.struct__VipsImage, float64, error) {
var image *C.struct__VipsImage
var err error
shrinkOnLoad := 1
// Recalculate integral shrink and double residual
switch {
case shrink >= 8:
factor = factor / 8
shrinkOnLoad = 8
case shrink >= 4:
factor = factor / 4
shrinkOnLoad = 4
case shrink >= 2:
factor = factor / 2
shrinkOnLoad = 2
}
// Reload input using shrink-on-load
if shrinkOnLoad > 1 {
image, err = vipsShrinkJpeg(buf, shrinkOnLoad)
}
return image, factor, err
}
func imageCalculations(o Options, inWidth, inHeight int) float64 { func imageCalculations(o Options, inWidth, inHeight int) float64 {
factor := 1.0 factor := 1.0
xfactor := float64(inWidth) / float64(o.Width)
yfactor := float64(inHeight) / float64(o.Height)
switch { switch {
// Fixed width and height // Fixed width and height
case o.Width > 0 && o.Height > 0: case o.Width > 0 && o.Height > 0:
xf := float64(inWidth) / float64(o.Width)
yf := float64(inHeight) / float64(o.Height)
if o.Crop { if o.Crop {
factor = math.Min(xf, yf) factor = math.Min(xfactor, yfactor)
} else { } else {
factor = math.Max(xf, yf) factor = math.Max(xfactor, yfactor)
} }
// Fixed width, auto height // Fixed width, auto height
case o.Width > 0: case o.Width > 0:
factor = float64(inWidth) / float64(o.Width) factor = xfactor
o.Height = int(math.Floor(float64(inHeight) / factor)) o.Height = int(math.Floor(float64(inHeight) / factor))
// Fixed height, auto width // Fixed height, auto width
case o.Height > 0: case o.Height > 0:
factor = float64(inHeight) / float64(o.Height) factor = yfactor
o.Width = int(math.Floor(float64(inWidth) / factor)) o.Width = int(math.Floor(float64(inWidth) / factor))
// Identity transform
default: default:
// Identity transform
o.Width = inWidth o.Width = inWidth
o.Height = inHeight o.Height = inHeight
break
} }
debug("Resolution %dx%d", o.Width, o.Height)
return factor return factor
} }
@ -263,53 +318,6 @@ func calculateRotationAndFlip(image *C.struct__VipsImage, angle Angle) (Angle, b
return rotate, flip return rotate, flip
} }
func shrinkImage(image *C.struct__VipsImage, o Options, residual float64, shrink int) (*C.struct__VipsImage, float64, error) {
// Use vips_shrink with the integral reduction
image, err := vipsShrink(image, shrink)
if err != nil {
return nil, 0, err
}
// Recalculate residual float based on dimensions of required vs shrunk images
residualx := float64(o.Width) / float64(image.Xsize)
residualy := float64(o.Height) / float64(image.Ysize)
if o.Crop {
residual = math.Max(residualx, residualy)
} else {
residual = math.Min(residualx, residualy)
}
return image, residual, nil
}
func shrinkJpegImage(buf []byte, factor float64, shrink int) (*C.struct__VipsImage, float64, error) {
shrinkOnLoad := 1
switch {
case shrink >= 8:
factor = factor / 8
shrinkOnLoad = 8
case shrink >= 4:
factor = factor / 4
shrinkOnLoad = 4
case shrink >= 2:
factor = factor / 2
shrinkOnLoad = 2
}
if shrinkOnLoad > 1 {
// Reload input using shrink-on-load
image, err := vipsShrinkJpeg(buf, shrinkOnLoad)
if err != nil {
return nil, factor, err
}
return image, factor, err
}
return nil, factor, nil
}
func getAngle(angle Angle) Angle { func getAngle(angle Angle) Angle {
divisor := angle % 90 divisor := angle % 90
if divisor != 0 { if divisor != 0 {

View file

@ -7,17 +7,8 @@ import (
) )
func TestResize(t *testing.T) { func TestResize(t *testing.T) {
options := Options{Width: 800, Height: 600, Crop: false, Rotate: 270} options := Options{Width: 800, Height: 600}
img, err := os.Open("fixtures/test.jpg") buf, _ := Read("fixtures/test.jpg")
if err != nil {
t.Fatal(err)
}
defer img.Close()
buf, err := ioutil.ReadAll(img)
if err != nil {
t.Fatal(err)
}
newImg, err := Resize(buf, options) newImg, err := Resize(buf, options)
if err != nil { if err != nil {
@ -28,7 +19,45 @@ func TestResize(t *testing.T) {
t.Fatal("Image is not jpeg") t.Fatal("Image is not jpeg")
} }
err = ioutil.WriteFile("fixtures/test_out.jpg", newImg, 0644) err = Write("fixtures/test_out.jpg", newImg)
if err != nil {
t.Fatal("Cannot save the image")
}
}
func TestRotate(t *testing.T) {
options := Options{Width: 800, Height: 600, Rotate: 270}
buf, _ := Read("fixtures/test.jpg")
newImg, err := Resize(buf, options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
}
if DetermineImageType(newImg) != JPEG {
t.Fatal("Image is not jpeg")
}
err = Write("fixtures/test_rotate_out.jpg", newImg)
if err != nil {
t.Fatal("Cannot save the image")
}
}
func TestInvalidRotate(t *testing.T) {
options := Options{Width: 800, Height: 600, Rotate: 111}
buf, _ := Read("fixtures/test.jpg")
newImg, err := Resize(buf, options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
}
if DetermineImageType(newImg) != JPEG {
t.Fatal("Image is not jpeg")
}
err = Write("fixtures/test_invalid_rotate_out.jpg", newImg)
if err != nil { if err != nil {
t.Fatal("Cannot save the image") t.Fatal("Cannot save the image")
} }
@ -63,7 +92,7 @@ func TestConvert(t *testing.T) {
t.Fatal("Invalid image size") t.Fatal("Invalid image size")
} }
err = ioutil.WriteFile("fixtures/test_out.png", newImg, 0644) err = Write("fixtures/test_out.png", newImg)
if err != nil { if err != nil {
t.Fatal("Cannot save the image") t.Fatal("Cannot save the image")
} }
@ -98,7 +127,7 @@ func TestResizePngWithTransparency(t *testing.T) {
t.Fatal("Invalid image size") t.Fatal("Invalid image size")
} }
err = ioutil.WriteFile("fixtures/transparent_out.png", newImg, 0644) err = Write("fixtures/transparent_out.png", newImg)
if err != nil { if err != nil {
t.Fatal("Cannot save the image") t.Fatal("Cannot save the image")
} }

6
vips.h
View file

@ -157,10 +157,10 @@ vips_init_image(void *buf, size_t len, int imageType, VipsImage **out) {
#endif #endif
} }
if (out != NULL) { // Listen for "postclose" signal to delete input buffer
// Listen for "postclose" signal to delete input buffer //if (out != NULL) {
//g_signal_connect(out, "postclose", G_CALLBACK(vips_malloc_cb), buf); //g_signal_connect(out, "postclose", G_CALLBACK(vips_malloc_cb), buf);
} //}
return code; return code;
}; };