diff --git a/fixtures/vertical.webp b/fixtures/vertical.webp new file mode 100644 index 00000000..117890e5 Binary files /dev/null and b/fixtures/vertical.webp differ diff --git a/resizer.go b/resizer.go index eab4d041..4b78b477 100644 --- a/resizer.go +++ b/resizer.go @@ -8,6 +8,7 @@ import "C" import ( "errors" + "fmt" "math" ) @@ -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 } @@ -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 @@ -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 { diff --git a/resizer_test.go b/resizer_test.go index cea7c6ca..fd1f2d79 100644 --- a/resizer_test.go +++ b/resizer_test.go @@ -9,7 +9,6 @@ import ( "io/ioutil" "os" "path" - "strconv" "testing" ) @@ -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) + } } } } diff --git a/vips.go b/vips.go index 1cc27743..e46b03fa 100644 --- a/vips.go +++ b/vips.go @@ -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)) diff --git a/vips.h b/vips.h index f7277519..569086a4 100644 --- a/vips.h +++ b/vips.h @@ -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);