package bimg /* #cgo pkg-config: vips #include "vips.h" */ import "C" import ( "errors" "fmt" "math" "os" "runtime" "strings" "sync" "unsafe" d "github.com/tj/go-debug" ) // debug is internally used to var debug = d.Debug("bimg") // VipsVersion exposes the current libvips semantic version const VipsVersion = string(C.VIPS_VERSION) // VipsMajorVersion exposes the current libvips major version number const VipsMajorVersion = int(C.VIPS_MAJOR_VERSION) // VipsMinorVersion exposes the current libvips minor version number const VipsMinorVersion = int(C.VIPS_MINOR_VERSION) const ( maxCacheMem = 100 * 1024 * 1024 maxCacheSize = 500 ) var ( m sync.Mutex initialized bool ) // VipsMemoryInfo represents the memory stats provided by libvips. type VipsMemoryInfo struct { Memory int64 MemoryHighwater int64 Allocations int64 } // vipsSaveOptions represents the internal option used to talk with libvips. type vipsSaveOptions struct { Quality int Compression int Type ImageType Interlace bool NoProfile bool StripMetadata bool OutputICC string // Absolute path to the output ICC profile Interpretation Interpretation } type vipsWatermarkOptions struct { Width C.int DPI C.int Margin C.int NoReplicate C.int Opacity C.float Background [3]C.double } type vipsWatermarkImageOptions struct { Left C.int Top C.int Opacity C.float } type vipsWatermarkTextOptions struct { Text *C.char Font *C.char } func init() { Initialize() } // Initialize is used to explicitly start libvips in thread-safe way. // Only call this function if you have previously turned off libvips. func Initialize() { if C.VIPS_MAJOR_VERSION <= 7 && C.VIPS_MINOR_VERSION < 40 { panic("unsupported libvips version!") } m.Lock() runtime.LockOSThread() defer m.Unlock() defer runtime.UnlockOSThread() err := C.vips_init(C.CString("bimg")) if err != 0 { panic("unable to start vips!") } // Set libvips cache params C.vips_cache_set_max_mem(maxCacheMem) C.vips_cache_set_max(maxCacheSize) // Define a custom thread concurrency limit in libvips (this may generate thread-unsafe issues) // See: https://github.com/jcupitt/libvips/issues/261#issuecomment-92850414 if os.Getenv("VIPS_CONCURRENCY") == "" { C.vips_concurrency_set(1) } // Enable libvips cache tracing if os.Getenv("VIPS_TRACE") != "" { C.vips_enable_cache_set_trace() } initialized = true } // Shutdown is used to shutdown libvips in a thread-safe way. // You can call this to drop caches as well. // If libvips was already initialized, the function is no-op func Shutdown() { m.Lock() defer m.Unlock() if initialized { C.vips_shutdown() initialized = false } } // VipsCacheSetMaxMem Sets the maximum amount of tracked memory allowed before the vips operation cache // begins to drop entries. func VipsCacheSetMaxMem(maxCacheMem int) { C.vips_cache_set_max_mem(C.size_t(maxCacheMem)) } // VipsCacheSetMax sets the maximum number of operations to keep in the vips operation cache. func VipsCacheSetMax(maxCacheSize int) { C.vips_cache_set_max(C.int(maxCacheSize)) } // VipsCacheDropAll drops the vips operation cache, freeing the allocated memory. func VipsCacheDropAll() { C.vips_cache_drop_all() } // VipsDebugInfo outputs to stdout libvips collected data. Useful for debugging. func VipsDebugInfo() { C.im__print_all() } // VipsMemory gets memory info stats from libvips (cache size, memory allocs...) func VipsMemory() VipsMemoryInfo { return VipsMemoryInfo{ Memory: int64(C.vips_tracked_get_mem()), MemoryHighwater: int64(C.vips_tracked_get_mem_highwater()), Allocations: int64(C.vips_tracked_get_allocs()), } } // VipsIsTypeSupported returns true if the given image type // is supported by the current libvips compilation. func VipsIsTypeSupported(t ImageType) bool { if t == JPEG { return int(C.vips_type_find_bridge(C.JPEG)) != 0 } if t == WEBP { return int(C.vips_type_find_bridge(C.WEBP)) != 0 } if t == PNG { return int(C.vips_type_find_bridge(C.PNG)) != 0 } if t == GIF { return int(C.vips_type_find_bridge(C.GIF)) != 0 } if t == PDF { return int(C.vips_type_find_bridge(C.PDF)) != 0 } if t == SVG { return int(C.vips_type_find_bridge(C.SVG)) != 0 } if t == TIFF { return int(C.vips_type_find_bridge(C.TIFF)) != 0 } if t == MAGICK { return int(C.vips_type_find_bridge(C.MAGICK)) != 0 } return false } // VipsIsTypeSupportedSave returns true if the given image type // is supported by the current libvips compilation for the // save operation. func VipsIsTypeSupportedSave(t ImageType) bool { if t == JPEG { return int(C.vips_type_find_save_bridge(C.JPEG)) != 0 } if t == WEBP { return int(C.vips_type_find_save_bridge(C.WEBP)) != 0 } if t == PNG { return int(C.vips_type_find_save_bridge(C.PNG)) != 0 } if t == TIFF { return int(C.vips_type_find_save_bridge(C.TIFF)) != 0 } return false } func vipsExifOrientation(image *C.VipsImage) int { return int(C.vips_exif_orientation(image)) } func vipsHasAlpha(image *C.VipsImage) bool { return int(C.has_alpha_channel(image)) > 0 } func vipsHasProfile(image *C.VipsImage) bool { return int(C.has_profile_embed(image)) > 0 } func vipsWindowSize(name string) float64 { cname := C.CString(name) defer C.free(unsafe.Pointer(cname)) return float64(C.interpolator_window_size(cname)) } func vipsSpace(image *C.VipsImage) string { return C.GoString(C.vips_enum_nick_bridge(image)) } func vipsRotate(image *C.VipsImage, angle Angle) (*C.VipsImage, error) { var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_rotate(image, &out, C.int(angle)) if err != 0 { return nil, catchVipsError() } return out, nil } func vipsFlip(image *C.VipsImage, direction Direction) (*C.VipsImage, error) { var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_flip_bridge(image, &out, C.int(direction)) if err != 0 { return nil, catchVipsError() } return out, nil } func vipsZoom(image *C.VipsImage, zoom int) (*C.VipsImage, error) { var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_zoom_bridge(image, &out, C.int(zoom), C.int(zoom)) if err != 0 { return nil, catchVipsError() } return out, nil } func vipsWatermark(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { var out *C.VipsImage // Defaults noReplicate := 0 if w.NoReplicate { noReplicate = 1 } text := C.CString(w.Text) font := C.CString(w.Font) background := [3]C.double{C.double(w.Background.R), C.double(w.Background.G), C.double(w.Background.B)} textOpts := vipsWatermarkTextOptions{text, font} opts := vipsWatermarkOptions{C.int(w.Width), C.int(w.DPI), C.int(w.Margin), C.int(noReplicate), C.float(w.Opacity), background} defer C.free(unsafe.Pointer(text)) defer C.free(unsafe.Pointer(font)) err := C.vips_watermark(image, &out, (*C.WatermarkTextOptions)(unsafe.Pointer(&textOpts)), (*C.WatermarkOptions)(unsafe.Pointer(&opts))) if err != 0 { return nil, catchVipsError() } return out, nil } func vipsRead(buf []byte) (*C.VipsImage, ImageType, error) { var image *C.VipsImage imageType := vipsImageType(buf) if imageType == UNKNOWN { return nil, UNKNOWN, errors.New("Unsupported image format") } length := C.size_t(len(buf)) imageBuf := unsafe.Pointer(&buf[0]) err := C.vips_init_image(imageBuf, length, C.int(imageType), &image) if err != 0 { return nil, UNKNOWN, catchVipsError() } return image, imageType, nil } func vipsColourspaceIsSupportedBuffer(buf []byte) (bool, error) { image, _, err := vipsRead(buf) if err != nil { return false, err } C.g_object_unref(C.gpointer(image)) return vipsColourspaceIsSupported(image), nil } func vipsColourspaceIsSupported(image *C.VipsImage) bool { return int(C.vips_colourspace_issupported_bridge(image)) == 1 } func vipsInterpretationBuffer(buf []byte) (Interpretation, error) { image, _, err := vipsRead(buf) if err != nil { return InterpretationError, err } C.g_object_unref(C.gpointer(image)) return vipsInterpretation(image), nil } func vipsInterpretation(image *C.VipsImage) Interpretation { return Interpretation(C.vips_image_guess_interpretation_bridge(image)) } func vipsFlattenBackground(image *C.VipsImage, background Color) (*C.VipsImage, error) { var outImage *C.VipsImage backgroundC := [3]C.double{ C.double(background.R), C.double(background.G), C.double(background.B), } if vipsHasAlpha(image) { err := C.vips_flatten_background_brigde(image, &outImage, backgroundC[0], backgroundC[1], backgroundC[2]) if int(err) != 0 { return nil, catchVipsError() } C.g_object_unref(C.gpointer(image)) image = outImage } return image, nil } func vipsPreSave(image *C.VipsImage, o *vipsSaveOptions) (*C.VipsImage, error) { var outImage *C.VipsImage // Remove ICC profile metadata if o.NoProfile { C.remove_profile(image) } // Use a default interpretation and cast it to C type if o.Interpretation == 0 { o.Interpretation = InterpretationSRGB } interpretation := C.VipsInterpretation(o.Interpretation) // Apply the proper colour space if vipsColourspaceIsSupported(image) { err := C.vips_colourspace_bridge(image, &outImage, interpretation) if int(err) != 0 { return nil, catchVipsError() } image = outImage } if o.OutputICC != "" && vipsHasProfile(image) { debug("Embedded ICC profile found, trying to convert to %s", o.OutputICC) outputIccPath := C.CString(o.OutputICC) defer C.free(unsafe.Pointer(outputIccPath)) err := C.vips_icc_transform_bridge(image, &outImage, outputIccPath) if int(err) != 0 { return nil, catchVipsError() } image = outImage } return image, nil } func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) { defer C.g_object_unref(C.gpointer(image)) tmpImage, err := vipsPreSave(image, &o) if err != nil { return nil, err } // When an image has an unsupported color space, vipsPreSave // returns the pointer of the image passed to it unmodified. // When this occurs, we must take care to not dereference the // original image a second time; we may otherwise erroneously // free the object twice. if tmpImage != image { defer C.g_object_unref(C.gpointer(tmpImage)) } length := C.size_t(0) saveErr := C.int(0) interlace := C.int(boolToInt(o.Interlace)) quality := C.int(o.Quality) strip := C.int(boolToInt(o.StripMetadata)) if o.Type != 0 && !IsTypeSupportedSave(o.Type) { return nil, fmt.Errorf("VIPS cannot save to %#v", ImageTypes[o.Type]) } var ptr unsafe.Pointer switch o.Type { case WEBP: saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, strip, quality) case PNG: saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, strip, C.int(o.Compression), quality, interlace) case TIFF: saveErr = C.vips_tiffsave_bridge(tmpImage, &ptr, &length) default: saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, strip, quality, interlace) } if int(saveErr) != 0 { return nil, catchVipsError() } buf := C.GoBytes(ptr, C.int(length)) // Clean up C.g_free(C.gpointer(ptr)) C.vips_error_clear() return buf, nil } func getImageBuffer(image *C.VipsImage) ([]byte, error) { var ptr unsafe.Pointer length := C.size_t(0) interlace := C.int(0) quality := C.int(100) err := C.int(0) err = C.vips_jpegsave_bridge(image, &ptr, &length, 1, quality, interlace) if int(err) != 0 { return nil, catchVipsError() } defer C.g_free(C.gpointer(ptr)) defer C.vips_error_clear() return C.GoBytes(ptr, C.int(length)), nil } func vipsExtract(image *C.VipsImage, left, top, width, height int) (*C.VipsImage, error) { var buf *C.VipsImage defer C.g_object_unref(C.gpointer(image)) if width > MaxSize || height > MaxSize { return nil, errors.New("Maximum image size exceeded") } top, left = max(top), max(left) err := C.vips_extract_area_bridge(image, &buf, C.int(left), C.int(top), C.int(width), C.int(height)) if err != 0 { return nil, catchVipsError() } return buf, nil } func vipsSmartCrop(image *C.VipsImage, width, height int) (*C.VipsImage, error) { var buf *C.VipsImage defer C.g_object_unref(C.gpointer(image)) if width > MaxSize || height > MaxSize { return nil, errors.New("Maximum image size exceeded") } err := C.vips_smartcrop_bridge(image, &buf, C.int(width), C.int(height)) if err != 0 { return nil, catchVipsError() } return buf, nil } func vipsTrim(image *C.VipsImage) (int, int, int, int, error) { defer C.g_object_unref(C.gpointer(image)) top := C.int(0) left := C.int(0) width := C.int(0) height := C.int(0) err := C.vips_find_trim_bridge(image, &top, &left, &width, &height) if err != 0 { return 0, 0, 0, 0, catchVipsError() } return int(top), int(left), int(width), int(height), nil } func vipsShrinkJpeg(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_jpegload_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)) err := C.vips_shrink_bridge(input, &image, C.double(float64(shrink)), C.double(float64(shrink))) if err != 0 { return nil, catchVipsError() } return image, nil } func vipsReduce(input *C.VipsImage, xshrink float64, yshrink float64) (*C.VipsImage, error) { var image *C.VipsImage defer C.g_object_unref(C.gpointer(input)) err := C.vips_reduce_bridge(input, &image, C.double(xshrink), C.double(yshrink)) if err != 0 { return nil, catchVipsError() } return image, nil } func vipsEmbed(input *C.VipsImage, left, top, width, height int, extend Extend, background Color) (*C.VipsImage, error) { var image *C.VipsImage // Max extend value, see: http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VipsExtend if extend > 5 { extend = ExtendBackground } defer C.g_object_unref(C.gpointer(input)) err := C.vips_embed_bridge(input, &image, C.int(left), C.int(top), C.int(width), C.int(height), C.int(extend), C.double(background.R), C.double(background.G), C.double(background.B)) if err != 0 { return nil, catchVipsError() } return image, nil } func vipsAffine(input *C.VipsImage, residualx, residualy float64, i Interpolator) (*C.VipsImage, error) { var image *C.VipsImage cstring := C.CString(i.String()) interpolator := C.vips_interpolate_new(cstring) defer C.free(unsafe.Pointer(cstring)) defer C.g_object_unref(C.gpointer(input)) defer C.g_object_unref(C.gpointer(interpolator)) err := C.vips_affine_interpolator(input, &image, C.double(residualx), 0, 0, C.double(residualy), interpolator) if err != 0 { return nil, catchVipsError() } return image, nil } func vipsImageType(buf []byte) ImageType { if len(buf) < 12 { return UNKNOWN } if buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF { return JPEG } if IsTypeSupported(GIF) && buf[0] == 0x47 && buf[1] == 0x49 && buf[2] == 0x46 { return GIF } if buf[0] == 0x89 && buf[1] == 0x50 && buf[2] == 0x4E && buf[3] == 0x47 { return PNG } if IsTypeSupported(TIFF) && ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) { return TIFF } if IsTypeSupported(PDF) && buf[0] == 0x25 && buf[1] == 0x50 && buf[2] == 0x44 && buf[3] == 0x46 { return PDF } if IsTypeSupported(WEBP) && buf[8] == 0x57 && buf[9] == 0x45 && buf[10] == 0x42 && buf[11] == 0x50 { return WEBP } if IsTypeSupported(SVG) && IsSVGImage(buf) { return SVG } if IsTypeSupported(MAGICK) && strings.HasSuffix(readImageType(buf), "MagickBuffer") { return MAGICK } return UNKNOWN } func readImageType(buf []byte) string { length := C.size_t(len(buf)) imageBuf := unsafe.Pointer(&buf[0]) load := C.vips_foreign_find_load_buffer(imageBuf, length) return C.GoString(load) } func catchVipsError() error { s := C.GoString(C.vips_error_buffer()) C.vips_error_clear() C.vips_thread_shutdown() return errors.New(s) } func boolToInt(b bool) int { if b { return 1 } return 0 } func vipsGaussianBlur(image *C.VipsImage, o GaussianBlur) (*C.VipsImage, error) { var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_gaussblur_bridge(image, &out, C.double(o.Sigma), C.double(o.MinAmpl)) if err != 0 { return nil, catchVipsError() } return out, nil } func vipsSharpen(image *C.VipsImage, o Sharpen) (*C.VipsImage, error) { var out *C.VipsImage defer C.g_object_unref(C.gpointer(image)) err := C.vips_sharpen_bridge(image, &out, C.int(o.Radius), C.double(o.X1), C.double(o.Y2), C.double(o.Y3), C.double(o.M1), C.double(o.M2)) if err != 0 { return nil, catchVipsError() } return out, nil } func max(x int) int { return int(math.Max(float64(x), 0)) } func vipsDrawWatermark(image *C.VipsImage, o WatermarkImage) (*C.VipsImage, error) { var out *C.VipsImage watermark, _, e := vipsRead(o.Buf) if e != nil { return nil, e } opts := vipsWatermarkImageOptions{C.int(o.Left), C.int(o.Top), C.float(o.Opacity)} err := C.vips_watermark_image(image, watermark, &out, (*C.WatermarkImageOptions)(unsafe.Pointer(&opts))) if err != 0 { return nil, catchVipsError() } return out, nil }