Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shrink-on-load for webp. #198

Merged
merged 1 commit into from
Oct 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added fixtures/vertical.webp
Binary file not shown.
51 changes: 27 additions & 24 deletions resizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "C"

import (
"errors"
"fmt"
"math"
)

Expand Down Expand Up @@ -67,9 +68,11 @@ func resizer(buf []byte, o Options) ([]byte, error) {
}
}

// Try to use libjpeg shrink-on-load
if imageType == JPEG && shrink >= 2 {
tmpImage, factor, err := shrinkJpegImage(buf, image, factor, shrink)
// Try to use libjpeg/libwebp shrink-on-load
supportsShrinkOnLoad := imageType == WEBP && VipsMajorVersion >= 8 && VipsMinorVersion >= 3
supportsShrinkOnLoad = supportsShrinkOnLoad || imageType == JPEG
if supportsShrinkOnLoad && shrink >= 2 {
tmpImage, factor, err := shrinkOnLoad(buf, image, imageType, factor, shrink)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -412,27 +415,31 @@ func shrinkImage(image *C.VipsImage, o Options, residual float64, shrink int) (*
return image, residual, nil
}

func shrinkJpegImage(buf []byte, input *C.VipsImage, factor float64, shrink int) (*C.VipsImage, float64, error) {
func shrinkOnLoad(buf []byte, input *C.VipsImage, imageType ImageType, factor float64, shrink int) (*C.VipsImage, float64, error) {
var image *C.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 {
if imageType == JPEG && shrink >= 2 {
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
}

image, err = vipsShrinkJpeg(buf, input, shrinkOnLoad)
} else if imageType == WEBP {
image, err = vipsShrinkWebp(buf, input, shrink)
} else {
return nil, 0, fmt.Errorf("%v doesn't support shrink on load", ImageTypeName(imageType))
}

return image, factor, err
Expand All @@ -446,11 +453,7 @@ func imageCalculations(o *Options, inWidth, inHeight int) float64 {
switch {
// Fixed width and height
case o.Width > 0 && o.Height > 0:
if o.Crop {
factor = math.Min(xfactor, yfactor)
} else {
factor = math.Max(xfactor, yfactor)
}
factor = math.Min(xfactor, yfactor)
// Fixed width, auto height
case o.Width > 0:
if o.Crop {
Expand Down
176 changes: 110 additions & 66 deletions resizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"io/ioutil"
"os"
"path"
"strconv"
"testing"
)

Expand All @@ -35,84 +34,129 @@ func TestResize(t *testing.T) {
}

func TestResizeVerticalImage(t *testing.T) {
tests := []struct {
format ImageType
options Options
tests := []Options{
{Width: 800, Height: 600},
{Width: 1000, Height: 1000},
{Width: 1000, Height: 1500},
{Width: 1000},
{Height: 1500},
{Width: 100, Height: 50},
{Width: 2000, Height: 2000},
{Width: 500, Height: 1000},
{Width: 500},
{Height: 500},
{Crop: true, Width: 500, Height: 1000},
{Crop: true, Enlarge: true, Width: 2000, Height: 1400},
{Enlarge: true, Force: true, Width: 2000, Height: 2000},
{Force: true, Width: 2000, Height: 2000},
}

bufJpeg, err := Read("fixtures/vertical.jpg")
if err != nil {
t.Fatal(err)
}
bufWebp, err := Read("fixtures/vertical.webp")
if err != nil {
t.Fatal(err)
}

images := []struct {
format ImageType
buf []byte
}{
{JPEG, Options{Width: 800, Height: 600}},
{JPEG, Options{Width: 1000, Height: 1000}},
{JPEG, Options{Width: 1000, Height: 1500}},
{JPEG, Options{Width: 1000}},
{JPEG, Options{Height: 1500}},
{JPEG, Options{Width: 100, Height: 50}},
{JPEG, Options{Width: 2000, Height: 2000}},
{JPEG, Options{Width: 500, Height: 1000}},
{JPEG, Options{Width: 500}},
{JPEG, Options{Height: 500}},
{JPEG, Options{Crop: true, Width: 500, Height: 1000}},
{JPEG, Options{Crop: true, Enlarge: true, Width: 2000, Height: 1400}},
{JPEG, Options{Enlarge: true, Force: true, Width: 2000, Height: 2000}},
{JPEG, Options{Force: true, Width: 2000, Height: 2000}},
}

buf, _ := Read("fixtures/vertical.jpg")
for _, test := range tests {
image, err := Resize(buf, test.options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", test.options, err)
}
{JPEG, bufJpeg},
{WEBP, bufWebp},
}

if DetermineImageType(image) != test.format {
t.Fatalf("Image format is invalid. Expected: %#v", test.format)
}
for _, source := range images {
for _, options := range tests {
image, err := Resize(source.buf, options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
}

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)
}
format := DetermineImageType(image)
if format != source.format {
t.Fatalf("Image format is invalid. Expected: %#v got %v", ImageTypeName(source.format), ImageTypeName(format))
}

Write("fixtures/test_vertical_"+strconv.Itoa(test.options.Width)+"x"+strconv.Itoa(test.options.Height)+"_out.jpg", image)
size, _ := Size(image)
if options.Height > 0 && size.Height != options.Height {
t.Fatalf("Invalid height: %d", size.Height)
}
if options.Width > 0 && size.Width != options.Width {
t.Fatalf("Invalid width: %d", size.Width)
}

Write(
fmt.Sprintf(
"fixtures/test_vertical_%dx%d_out.%s",
options.Width,
options.Height,
ImageTypeName(source.format)),
image)
}
}
}

func TestResizeCustomSizes(t *testing.T) {
tests := []struct {
format ImageType
options Options
tests := []Options{
{Width: 800, Height: 600},
{Width: 1000, Height: 1000},
{Width: 100, Height: 50},
{Width: 2000, Height: 2000},
{Width: 500, Height: 1000},
{Width: 500},
{Height: 500},
{Crop: true, Width: 500, Height: 1000},
{Crop: true, Enlarge: true, Width: 2000, Height: 1400},
{Enlarge: true, Force: true, Width: 2000, Height: 2000},
{Force: true, Width: 2000, Height: 2000},
}

bufJpeg, err := Read("fixtures/test.jpg")
if err != nil {
t.Fatal(err)
}
bufWebp, err := Read("fixtures/test.webp")
if err != nil {
t.Fatal(err)
}

images := []struct {
format ImageType
buf []byte
}{
{JPEG, Options{Width: 800, Height: 600}},
{JPEG, Options{Width: 1000, Height: 1000}},
{JPEG, Options{Width: 100, Height: 50}},
{JPEG, Options{Width: 2000, Height: 2000}},
{JPEG, Options{Width: 500, Height: 1000}},
{JPEG, Options{Width: 500}},
{JPEG, Options{Height: 500}},
{JPEG, Options{Crop: true, Width: 500, Height: 1000}},
{JPEG, Options{Crop: true, Enlarge: true, Width: 2000, Height: 1400}},
{JPEG, Options{Enlarge: true, Force: true, Width: 2000, Height: 2000}},
{JPEG, Options{Force: true, Width: 2000, Height: 2000}},
{JPEG, bufJpeg},
{WEBP, bufWebp},
}

buf, _ := Read("fixtures/test.jpg")
for _, test := range tests {
image, err := Resize(buf, test.options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", test.options, err)
}
for _, source := range images {
for _, options := range tests {
image, err := Resize(source.buf, options)
if err != nil {
t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
}

if DetermineImageType(image) != test.format {
t.Fatalf("Image format is invalid. Expected: %#v", test.format)
}
if DetermineImageType(image) != source.format {
t.Fatalf("Image format is invalid. Expected: %#v", source.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)
size, _ := Size(image)

invalidHeight := options.Height > 0 && size.Height != options.Height
if !options.Crop && invalidHeight {
t.Fatalf("Invalid height: %d, expected %d", size.Height, options.Height)
}

invalidWidth := options.Width > 0 && size.Width != options.Width
if !options.Crop && invalidWidth {
t.Fatalf("Invalid width: %d, expected %d", size.Width, options.Width)
}

if options.Crop && invalidHeight && invalidWidth {
t.Fatalf("Invalid width or height: %dx%d, expected %dx%d (crop)", size.Width, size.Height, options.Width, options.Height)
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions vips.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,19 @@ func vipsShrinkJpeg(buf []byte, input *C.VipsImage, shrink int) (*C.VipsImage, e
return image, nil
}

func vipsShrinkWebp(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_webpload_buffer_shrink(ptr, C.size_t(len(buf)), &image, C.int(shrink))
if err != 0 {
return nil, catchVipsError()
}

return image, nil
}

func vipsShrink(input *C.VipsImage, shrink int) (*C.VipsImage, error) {
var image *C.VipsImage
defer C.g_object_unref(C.gpointer(input))
Expand Down
5 changes: 5 additions & 0 deletions vips.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ 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_webpload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) {
return vips_webpload_buffer(buf, len, out, "shrink", shrink, NULL);
}

int
vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) {
return vips_flip(in, out, direction, NULL);
Expand Down