refactor(resize): extract

master
Tomas Aparicio 11 years ago
parent d9eac32a9a
commit 3904953399

@ -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)
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.
Thanks to libvips it's typically 4x faster than using the quickest ImageMagick and GraphicsMagick settings
or Go native image processing package, and in some cases it's even 8x faster processing JPEG images.
It supports JPEG, PNG, WEBP, TIFF and Magick image formats.
bimg is designed to be a small and efficient library with a specific but useful and generic set of features.
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.
bimg can read JPEG, PNG, WEBP, TIFF and Magick formats and it can output to JPEG, PNG and WEBP.
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).
`Work in progress`
**Note**: bimg is still a beta package. PR and issues are highly appreciated
## Prerequisites
@ -38,10 +41,10 @@ The [install script](https://github.com/lovell/sharp/blob/master/preinstall.sh)
- Enlarge
- Crop
- Rotate
- Flip/Flop
- Flip
- Extract area
- Extract image metadata (size, format, profile, orientation...)
- Image conversion to multiple formats
- EXIF metadata (size, alpha channel, profile, orientation...)
- Format conversion
## Performance

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

@ -19,3 +19,20 @@ func TestRead(t *testing.T) {
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)
}
}

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

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

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

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

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

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

Loading…
Cancel
Save