diff --git a/README.md b/README.md index 41b7850..914f9e8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ bimg is designed to be a small and efficient library with a generic and useful s It uses internally libvips, 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, TIFF and Magick formats and it can output to JPEG, PNG and WEBP. It supports common [image transformation](#supported-image-operations) operations such as crop, resize, rotate... and conversion between multiple formats. +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... and conversion between multiple formats. For getting started, take a look to the [examples](#examples) and [programmatic API](https://godoc.org/github.com/h2non/bimg) documentation. diff --git a/image.go b/image.go index 5889efc..60b1717 100644 --- a/image.go +++ b/image.go @@ -4,6 +4,7 @@ type Image struct { buffer []byte } +// Resize the image to fixed width and height func (i *Image) Resize(width, height int) ([]byte, error) { options := Options{ Width: width, @@ -12,6 +13,7 @@ func (i *Image) Resize(width, height int) ([]byte, error) { 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{ Top: top, @@ -22,6 +24,17 @@ func (i *Image) Extract(top, left, width, height int) ([]byte, error) { return i.Process(options) } +// Enlarge the image from the by X/Y axis +func (i *Image) Enlarge(width, height int) ([]byte, error) { + options := Options{ + Width: width, + Height: height, + Enlarge: true, + } + return i.Process(options) +} + +// Crop an image by width and height func (i *Image) Crop(width, height int) ([]byte, error) { options := Options{ Width: width, @@ -31,6 +44,25 @@ func (i *Image) Crop(width, height int) ([]byte, error) { return i.Process(options) } +// Crop an image by width (auto height) +func (i *Image) CropByWidth(width int) ([]byte, error) { + options := Options{ + Width: width, + Crop: true, + } + return i.Process(options) +} + +// Crop an image by height (auto width) +func (i *Image) CropByHeight(height int) ([]byte, error) { + options := Options{ + Height: height, + Crop: true, + } + return i.Process(options) +} + +// Thumbnail the image by the a given width by aspect ratio 4:4 func (i *Image) Thumbnail(pixels int) ([]byte, error) { options := Options{ Width: pixels, @@ -41,21 +73,25 @@ func (i *Image) Thumbnail(pixels int) ([]byte, error) { return i.Process(options) } +// Rotate the image by given angle degrees (0, 90, 180 or 270) func (i *Image) Rotate(a Angle) ([]byte, error) { options := Options{Rotate: a} return i.Process(options) } +// Flip the image about the vertical Y axis func (i *Image) Flip() ([]byte, error) { options := Options{Flip: VERTICAL} return i.Process(options) } +// Flop the image about the horizontal X axis func (i *Image) Convert(t ImageType) ([]byte, error) { options := Options{Type: t} return i.Process(options) } +// Transform the image by custom options func (i *Image) Process(o Options) ([]byte, error) { image, err := Resize(i.buffer, o) if err != nil { @@ -65,18 +101,22 @@ func (i *Image) Process(o Options) ([]byte, error) { return image, nil } -func (i *Image) Type() string { - return DetermineImageTypeName(i.buffer) -} - +// Get image metadata (size, alpha channel, profile, EXIF rotation) func (i *Image) Metadata() (ImageMetadata, error) { return Metadata(i.buffer) } +// Get image type format (jpeg, png, webp, tiff) +func (i *Image) Type() string { + return DetermineImageTypeName(i.buffer) +} + +// Get image size func (i *Image) Size() (ImageSize, error) { return Size(i.buffer) } +// Creates a new image func NewImage(buf []byte) *Image { return &Image{buf} } diff --git a/image_test.go b/image_test.go index 46ccc52..0d9b3be 100644 --- a/image_test.go +++ b/image_test.go @@ -10,6 +10,11 @@ func TestImageResize(t *testing.T) { if err != nil { t.Errorf("Cannot process the image: %#v", err) } + + if assertSize(buf, 300, 240) { + t.Error("Invalid image size") + } + Write("fixtures/test_resize_out.jpg", buf) } @@ -18,23 +23,64 @@ func TestImageExtract(t *testing.T) { if err != nil { t.Errorf("Cannot process the image: %#v", err) } + + if assertSize(buf, 300, 300) { + t.Error("Invalid image size") + } + Write("fixtures/test_extract_out.jpg", buf) } +func TestImageEnlarge(t *testing.T) { + buf, err := initImage("test.png").Enlarge(500, 380) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + if assertSize(buf, 500, 380) { + t.Error("Invalid image size") + } + + Write("fixtures/test_enlarge_out.jpg", buf) +} + func TestImageCrop(t *testing.T) { buf, err := initImage("test.jpg").Crop(800, 600) if err != nil { t.Errorf("Cannot process the image: %#v", err) } + + if assertSize(buf, 800, 600) { + t.Error("Invalid image size") + } + Write("fixtures/test_crop_out.jpg", buf) } -func TestImageFlip(t *testing.T) { - buf, err := initImage("test.jpg").Flip() +func TestImageCropByWidth(t *testing.T) { + buf, err := initImage("test.jpg").CropByWidth(600) if err != nil { t.Errorf("Cannot process the image: %#v", err) } - Write("fixtures/test_flip_out.jpg", buf) + + if assertSize(buf, 600, 375) { + t.Error("Invalid image size") + } + + Write("fixtures/test_crop_width_out.jpg", buf) +} + +func TestImageCropByHeight(t *testing.T) { + buf, err := initImage("test.jpg").CropByHeight(300) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + + if assertSize(buf, 800, 480) { + t.Error("Invalid image size") + } + + Write("fixtures/test_crop_height_out.jpg", buf) } func TestImageThumbnail(t *testing.T) { @@ -42,9 +88,22 @@ func TestImageThumbnail(t *testing.T) { if err != nil { t.Errorf("Cannot process the image: %#v", err) } + + if assertSize(buf, 100, 100) { + t.Error("Invalid image size") + } + Write("fixtures/test_thumbnail_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) { buf, err := initImage("test_flip_out.jpg").Rotate(90) if err != nil { @@ -81,3 +140,14 @@ func initImage(file string) *Image { buf, _ := Read(path.Join("fixtures", file)) return NewImage(buf) } + +func assertSize(buf []byte, width, height int) bool { + size, err := NewImage(buf).Size() + if err != nil { + return false + } + if size.Width != 220 || size.Height != 300 { + return false + } + return true +} diff --git a/resize.go b/resize.go index c3c0c0e..dcfa723 100644 --- a/resize.go +++ b/resize.go @@ -45,14 +45,14 @@ func Resize(buf []byte, o Options) ([]byte, error) { inHeight := int(image.Ysize) // image calculations - factor := imageCalculations(o, inWidth, inHeight) + factor := imageCalculations(&o, inWidth, inHeight) shrink := int(math.Max(math.Floor(factor), 1)) residual := float64(shrink) / factor // Do not enlarge the output if the input width *or* height are already less than the required dimensions if o.Enlarge == false { if inWidth < o.Width && inHeight < o.Height { - factor = 1 + factor = 1.0 shrink = 1 residual = 0 o.Width = inWidth @@ -169,7 +169,6 @@ func rotateImage(image *C.struct__VipsImage, o Options) (*C.struct__VipsImage, e if o.Rotate > 0 { image, err = vipsRotate(image, getAngle(o.Rotate)) } - if o.Flip > 0 { image, err = vipsFlip(image, o.Flip) } @@ -223,7 +222,7 @@ func shrinkJpegImage(buf []byte, input *C.struct__VipsImage, factor float64, shr return image, factor, err } -func imageCalculations(o Options, inWidth, inHeight int) float64 { +func imageCalculations(o *Options, inWidth, inHeight int) float64 { factor := 1.0 xfactor := float64(inWidth) / float64(o.Width) yfactor := float64(inHeight) / float64(o.Height) diff --git a/vips.go b/vips.go index 87805ce..fbd7b80 100644 --- a/vips.go +++ b/vips.go @@ -8,7 +8,6 @@ import "C" import ( "errors" - //"fmt" "runtime" "strings" "sync" @@ -20,44 +19,51 @@ var ( initialized bool = false ) -type vipsImage C.struct__VipsImage - func init() { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 { - panic("unsupported old vips version") + panic("unsupported old vips version!") } Initialize() - - 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 } +// Explicit thread-safe start of libvips. +// You should only call this function if you previously shutdown libvips func Initialize() { + m.Lock() + runtime.LockOSThread() + defer m.Unlock() + defer runtime.UnlockOSThread() + err := C.vips_init(C.CString("bimg")) if err != 0 { Shutdown() panic("unable to start vips!") } - m.Lock() - defer m.Unlock() + 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 initialized = true } +// Explicit thread-safe libvips shutdown. Call this to drop caches. +// If libvips was already initialized, the function is no-op func Shutdown() { + m.Lock() + defer m.Unlock() + if initialized == true { - m.Lock() - defer m.Unlock() C.vips_shutdown() initialized = false } } +// Output to stdout collected data for debugging purposes +func VipsDebug() { + C.im__print_all() +} + func vipsRotate(image *C.struct__VipsImage, angle Angle) (*C.struct__VipsImage, error) { var out *C.struct__VipsImage defer C.g_object_unref(C.gpointer(image)) @@ -197,26 +203,6 @@ func vipsImageType(buf []byte) ImageType { return imageType } -func vipsExifOrientation(image *C.struct__VipsImage) int { - return int(C.vips_exif_orientation(image)) -} - -func vipsHasAlpha(image *C.struct__VipsImage) bool { - return int(C.has_alpha_channel(image)) > 0 -} - -func vipsHasProfile(image *C.struct__VipsImage) bool { - return int(C.has_profile_embed(image)) > 0 -} - -func vipsWindowSize(name string) float64 { - return float64(C.interpolator_window_size(C.CString(name))) -} - -func vipsSpace(image *C.struct__VipsImage) string { - return C.GoString(C.vips_enum_nick_bridge(image)) -} - type vipsSaveOptions struct { Quality int Compression int @@ -255,9 +241,30 @@ func vipsSave(image *C.struct__VipsImage, o vipsSaveOptions) ([]byte, error) { return buf, nil } +func vipsExifOrientation(image *C.struct__VipsImage) int { + return int(C.vips_exif_orientation(image)) +} + +func vipsHasAlpha(image *C.struct__VipsImage) bool { + return int(C.has_alpha_channel(image)) > 0 +} + +func vipsHasProfile(image *C.struct__VipsImage) bool { + return int(C.has_profile_embed(image)) > 0 +} + +func vipsWindowSize(name string) float64 { + return float64(C.interpolator_window_size(C.CString(name))) +} + +func vipsSpace(image *C.struct__VipsImage) string { + return C.GoString(C.vips_enum_nick_bridge(image)) +} + func catchVipsError() error { s := C.GoString(C.vips_error_buffer()) C.vips_error_clear() C.vips_thread_shutdown() + // clean image memory buffer? return errors.New(s) } diff --git a/vips.h b/vips.h index bba3e7f..46c0b7d 100644 --- a/vips.h +++ b/vips.h @@ -151,8 +151,10 @@ vips_init_image(void *buf, size_t len, int imageType, VipsImage **out) { code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); } else if (imageType == TIFF) { code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); +#if (VIPS_MAJOR_VERSION >= 8) } else if (imageType == MAGICK) { code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); +#endif } // Listen for "postclose" signal to delete input buffer