mirror of
https://github.com/imgproxy/imgproxy.git
synced 2024-11-24 08:12:38 +02:00
995 lines
22 KiB
Go
995 lines
22 KiB
Go
package main
|
|
|
|
/*
|
|
#cgo pkg-config: vips
|
|
#cgo LDFLAGS: -s -w
|
|
#cgo CFLAGS: -O3
|
|
#include "vips.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
var (
|
|
vipsSupportSmartcrop bool
|
|
vipsTypeSupportLoad = make(map[imageType]bool)
|
|
vipsTypeSupportSave = make(map[imageType]bool)
|
|
|
|
watermark *C.VipsImage
|
|
|
|
errSmartCropNotSupported = errors.New("Smart crop is not supported by used version of libvips")
|
|
)
|
|
|
|
type cConfig struct {
|
|
JpegProgressive C.int
|
|
PngInterlaced C.int
|
|
WatermarkOpacity C.double
|
|
}
|
|
|
|
var cConf cConfig
|
|
|
|
var cstrings = make(map[string]*C.char)
|
|
|
|
func initVips() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
if err := C.vips_initialize(); err != 0 {
|
|
C.vips_shutdown()
|
|
logFatal("unable to start vips!")
|
|
}
|
|
|
|
// Disable libvips cache. Since processing pipeline is fine tuned, we won't get much profit from it.
|
|
// Enabled cache can cause SIGSEGV on Musl-based systems like Alpine.
|
|
C.vips_cache_set_max_mem(0)
|
|
C.vips_cache_set_max(0)
|
|
|
|
C.vips_concurrency_set(1)
|
|
|
|
if len(os.Getenv("IMGPROXY_VIPS_LEAK_CHECK")) > 0 {
|
|
C.vips_leak_set(C.gboolean(1))
|
|
}
|
|
|
|
if len(os.Getenv("IMGPROXY_VIPS_CACHE_TRACE")) > 0 {
|
|
C.vips_cache_set_trace(C.gboolean(1))
|
|
}
|
|
|
|
vipsSupportSmartcrop = C.vips_support_smartcrop() == 1
|
|
|
|
if int(C.vips_type_find_load_go(C.int(imageTypeJPEG))) != 0 {
|
|
vipsTypeSupportLoad[imageTypeJPEG] = true
|
|
}
|
|
if int(C.vips_type_find_load_go(C.int(imageTypePNG))) != 0 {
|
|
vipsTypeSupportLoad[imageTypePNG] = true
|
|
}
|
|
if int(C.vips_type_find_load_go(C.int(imageTypeWEBP))) != 0 {
|
|
vipsTypeSupportLoad[imageTypeWEBP] = true
|
|
}
|
|
if int(C.vips_type_find_load_go(C.int(imageTypeGIF))) != 0 {
|
|
vipsTypeSupportLoad[imageTypeGIF] = true
|
|
}
|
|
if int(C.vips_type_find_load_go(C.int(imageTypeSVG))) != 0 {
|
|
vipsTypeSupportLoad[imageTypeSVG] = true
|
|
}
|
|
|
|
// we load ICO with github.com/mat/besticon/ico and send decoded data to vips
|
|
vipsTypeSupportLoad[imageTypeICO] = true
|
|
|
|
if int(C.vips_type_find_save_go(C.int(imageTypeJPEG))) != 0 {
|
|
vipsTypeSupportSave[imageTypeJPEG] = true
|
|
}
|
|
if int(C.vips_type_find_save_go(C.int(imageTypePNG))) != 0 {
|
|
vipsTypeSupportSave[imageTypePNG] = true
|
|
}
|
|
if int(C.vips_type_find_save_go(C.int(imageTypeWEBP))) != 0 {
|
|
vipsTypeSupportSave[imageTypeWEBP] = true
|
|
}
|
|
if int(C.vips_type_find_save_go(C.int(imageTypeGIF))) != 0 {
|
|
vipsTypeSupportSave[imageTypeGIF] = true
|
|
}
|
|
if int(C.vips_type_find_save_go(C.int(imageTypeICO))) != 0 {
|
|
vipsTypeSupportSave[imageTypeICO] = true
|
|
}
|
|
|
|
if conf.JpegProgressive {
|
|
cConf.JpegProgressive = C.int(1)
|
|
}
|
|
|
|
if conf.PngInterlaced {
|
|
cConf.PngInterlaced = C.int(1)
|
|
}
|
|
|
|
cConf.WatermarkOpacity = C.double(conf.WatermarkOpacity)
|
|
|
|
if err := vipsPrepareWatermark(); err != nil {
|
|
logFatal(err.Error())
|
|
}
|
|
|
|
collectVipsMetrics()
|
|
}
|
|
|
|
func shutdownVips() {
|
|
C.clear_image(&watermark)
|
|
C.vips_shutdown()
|
|
}
|
|
|
|
func collectVipsMetrics() {
|
|
if prometheusEnabled {
|
|
go func() {
|
|
for range time.Tick(5 * time.Second) {
|
|
prometheusVipsMemory.Set(float64(C.vips_tracked_get_mem()))
|
|
prometheusVipsMaxMemory.Set(float64(C.vips_tracked_get_mem_highwater()))
|
|
prometheusVipsAllocs.Set(float64(C.vips_tracked_get_allocs()))
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func cachedCString(str string) *C.char {
|
|
if cstr, ok := cstrings[str]; ok {
|
|
return cstr
|
|
}
|
|
|
|
cstr := C.CString(str)
|
|
cstrings[str] = cstr
|
|
|
|
return cstr
|
|
}
|
|
|
|
func extractMeta(img *C.VipsImage) (int, int, int, bool) {
|
|
width := int(img.Xsize)
|
|
height := int(img.Ysize)
|
|
|
|
angle := C.VIPS_ANGLE_D0
|
|
flip := false
|
|
|
|
orientation := C.vips_get_exif_orientation(img)
|
|
if orientation >= 5 && orientation <= 8 {
|
|
width, height = height, width
|
|
}
|
|
if orientation == 3 || orientation == 4 {
|
|
angle = C.VIPS_ANGLE_D180
|
|
}
|
|
if orientation == 5 || orientation == 6 {
|
|
angle = C.VIPS_ANGLE_D90
|
|
}
|
|
if orientation == 7 || orientation == 8 {
|
|
angle = C.VIPS_ANGLE_D270
|
|
}
|
|
if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
|
|
flip = true
|
|
}
|
|
|
|
return width, height, angle, flip
|
|
}
|
|
|
|
func calcScale(width, height int, po *processingOptions, imgtype imageType) float64 {
|
|
// If we're going only to crop, we need only to scale down to DPR.
|
|
// Scaling up while cropping is not optimal on this stage, we'll do it later if needed.
|
|
if po.Resize == resizeCrop {
|
|
if po.Dpr < 1 {
|
|
return po.Dpr
|
|
}
|
|
return 1
|
|
}
|
|
|
|
var scale float64
|
|
|
|
srcW, srcH := float64(width), float64(height)
|
|
|
|
if (po.Width == 0 || po.Width == width) && (po.Height == 0 || po.Height == height) {
|
|
scale = 1
|
|
} else {
|
|
wr := float64(po.Width) / srcW
|
|
hr := float64(po.Height) / srcH
|
|
|
|
if po.Width == 0 {
|
|
scale = hr
|
|
} else if po.Height == 0 {
|
|
scale = wr
|
|
} else if po.Resize == resizeFit {
|
|
scale = math.Min(wr, hr)
|
|
} else {
|
|
scale = math.Max(wr, hr)
|
|
}
|
|
}
|
|
|
|
scale = scale * po.Dpr
|
|
|
|
if !po.Enlarge && scale > 1 && imgtype != imageTypeSVG {
|
|
return 1
|
|
}
|
|
|
|
if srcW*scale < 1 {
|
|
scale = 1 / srcW
|
|
}
|
|
|
|
if srcH*scale < 1 {
|
|
scale = 1 / srcH
|
|
}
|
|
|
|
return scale
|
|
}
|
|
|
|
func calcShink(scale float64, imgtype imageType) int {
|
|
switch imgtype {
|
|
case imageTypeWEBP:
|
|
return int(1.0 / scale)
|
|
case imageTypeJPEG:
|
|
shrink := int(1.0 / scale)
|
|
|
|
switch {
|
|
case shrink >= 16:
|
|
return 8
|
|
case shrink >= 8:
|
|
return 4
|
|
case shrink >= 4:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
func calcCrop(width, height, cropWidth, cropHeight int, gravity *gravityOptions) (left, top int) {
|
|
if gravity.Type == gravityFocusPoint {
|
|
pointX := int(float64(width) * gravity.X)
|
|
pointY := int(float64(height) * gravity.Y)
|
|
|
|
left = maxInt(0, minInt(pointX-cropWidth/2, width-cropWidth))
|
|
top = maxInt(0, minInt(pointY-cropHeight/2, height-cropHeight))
|
|
|
|
return
|
|
}
|
|
|
|
left = (width - cropWidth + 1) / 2
|
|
top = (height - cropHeight + 1) / 2
|
|
|
|
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
|
|
top = 0
|
|
}
|
|
|
|
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
|
|
left = width - cropWidth
|
|
}
|
|
|
|
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
|
|
top = height - cropHeight
|
|
}
|
|
|
|
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
|
|
left = 0
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func transformImage(ctx context.Context, img **C.VipsImage, data []byte, po *processingOptions, imgtype imageType) error {
|
|
var err error
|
|
|
|
imgWidth, imgHeight, angle, flip := extractMeta(*img)
|
|
|
|
hasAlpha := vipsImageHasAlpha(*img)
|
|
|
|
if scale := calcScale(imgWidth, imgHeight, po, imgtype); scale != 1 {
|
|
if imgtype == imageTypeSVG && data != nil {
|
|
// Load SVG with desired scale
|
|
if tmp, err := vipsLoadImage(data, imgtype, 1, scale, false); err == nil {
|
|
C.swap_and_clear(img, tmp)
|
|
} else {
|
|
return err
|
|
}
|
|
} else {
|
|
// Do some shrink-on-load
|
|
if scale < 1.0 && data != nil {
|
|
if shrink := calcShink(scale, imgtype); shrink != 1 {
|
|
scale = scale * float64(shrink)
|
|
|
|
if tmp, err := vipsLoadImage(data, imgtype, shrink, 1.0, false); err == nil {
|
|
C.swap_and_clear(img, tmp)
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = vipsResize(img, scale, hasAlpha); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update actual image size after resize
|
|
imgWidth, imgHeight, _, _ = extractMeta(*img)
|
|
}
|
|
|
|
if err = vipsImportColourProfile(img); err != nil {
|
|
return err
|
|
}
|
|
|
|
checkTimeout(ctx)
|
|
|
|
if angle != C.VIPS_ANGLE_D0 || flip {
|
|
if err = vipsImageCopyMemory(img); err != nil {
|
|
return err
|
|
}
|
|
|
|
if angle != C.VIPS_ANGLE_D0 {
|
|
if err = vipsRotate(img, angle); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if flip {
|
|
if err = vipsFlip(img); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
checkTimeout(ctx)
|
|
|
|
cropW, cropH := po.Width, po.Height
|
|
|
|
if po.Dpr < 1 || (po.Dpr > 1 && po.Resize != resizeCrop) {
|
|
cropW = int(float64(cropW) * po.Dpr)
|
|
cropH = int(float64(cropH) * po.Dpr)
|
|
}
|
|
|
|
if cropW == 0 {
|
|
cropW = imgWidth
|
|
} else {
|
|
cropW = minInt(cropW, imgWidth)
|
|
}
|
|
|
|
if cropH == 0 {
|
|
cropH = imgHeight
|
|
} else {
|
|
cropH = minInt(cropH, imgHeight)
|
|
}
|
|
|
|
if cropW < imgWidth || cropH < imgHeight {
|
|
if po.Gravity.Type == gravitySmart {
|
|
if err = vipsImageCopyMemory(img); err != nil {
|
|
return err
|
|
}
|
|
if err = vipsSmartCrop(img, cropW, cropH); err != nil {
|
|
return err
|
|
}
|
|
// Applying additional modifications after smart crop causes SIGSEGV on Alpine
|
|
// so we have to copy memory after it
|
|
if err = vipsImageCopyMemory(img); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
left, top := calcCrop(imgWidth, imgHeight, cropW, cropH, &po.Gravity)
|
|
if err = vipsCrop(img, left, top, cropW, cropH); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
checkTimeout(ctx)
|
|
}
|
|
|
|
if po.Enlarge && po.Resize == resizeCrop && po.Dpr > 1 {
|
|
// We didn't enlarge the image before, because is wasn't optimal. Now it's time to do it
|
|
if err = vipsResize(img, po.Dpr, hasAlpha); err != nil {
|
|
return err
|
|
}
|
|
if err = vipsImageCopyMemory(img); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if po.Expand && (po.Width > int((*img).Xsize) || po.Height > int((*img).Ysize)) {
|
|
if err = vipsEnsureAlpha(img); err != nil {
|
|
return err
|
|
}
|
|
|
|
hasAlpha = true
|
|
|
|
if err = vipsEmbed(img, gravityCenter, C.int(po.Width), C.int(po.Height), 0, 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if hasAlpha && (po.Flatten || po.Format == imageTypeJPEG) {
|
|
if err = vipsFlatten(img, po.Background); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if po.Blur > 0 {
|
|
if err = vipsBlur(img, po.Blur); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if po.Sharpen > 0 {
|
|
if err = vipsSharpen(img, po.Sharpen); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
checkTimeout(ctx)
|
|
|
|
if po.Watermark.Enabled {
|
|
if err = vipsApplyWatermark(img, &po.Watermark); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return vipsFixColourspace(img)
|
|
}
|
|
|
|
func transformGif(ctx context.Context, img **C.VipsImage, po *processingOptions) error {
|
|
imgWidth := int((*img).Xsize)
|
|
imgHeight := int((*img).Ysize)
|
|
|
|
// Double check dimensions because gif may have many frames
|
|
if err := checkDimensions(imgWidth, imgHeight); err != nil {
|
|
return err
|
|
}
|
|
|
|
frameHeight, err := vipsGetInt(*img, "page-height")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
delay, err := vipsGetInt(*img, "gif-delay")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
loop, err := vipsGetInt(*img, "gif-loop")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
framesCount := minInt(imgHeight/frameHeight, conf.MaxGifFrames)
|
|
|
|
frames := make([]*C.VipsImage, framesCount)
|
|
defer func() {
|
|
for _, frame := range frames {
|
|
C.clear_image(&frame)
|
|
}
|
|
}()
|
|
|
|
var errg errgroup.Group
|
|
|
|
for i := 0; i < framesCount; i++ {
|
|
ind := i
|
|
errg.Go(func() error {
|
|
var frame *C.VipsImage
|
|
|
|
if err := vipsExtract(*img, &frame, 0, ind*frameHeight, imgWidth, frameHeight); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := transformImage(ctx, &frame, nil, po, imageTypeGIF); err != nil {
|
|
return err
|
|
}
|
|
|
|
frames[ind] = frame
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := errg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
checkTimeout(ctx)
|
|
|
|
if err := vipsArrayjoin(frames, img); err != nil {
|
|
return err
|
|
}
|
|
|
|
vipsSetInt(*img, "page-height", int(frames[0].Ysize))
|
|
vipsSetInt(*img, "gif-delay", delay)
|
|
vipsSetInt(*img, "gif-loop", loop)
|
|
|
|
return nil
|
|
}
|
|
|
|
func processImage(ctx context.Context) ([]byte, context.CancelFunc, error) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
if newRelicEnabled {
|
|
newRelicCancel := startNewRelicSegment(ctx, "Processing image")
|
|
defer newRelicCancel()
|
|
}
|
|
|
|
if prometheusEnabled {
|
|
defer startPrometheusDuration(prometheusProcessingDuration)()
|
|
}
|
|
|
|
defer C.vips_cleanup()
|
|
|
|
po := getProcessingOptions(ctx)
|
|
data := getImageData(ctx).Bytes()
|
|
imgtype := getImageType(ctx)
|
|
|
|
if po.Gravity.Type == gravitySmart && !vipsSupportSmartcrop {
|
|
return nil, func() {}, errSmartCropNotSupported
|
|
}
|
|
|
|
if po.Format == imageTypeUnknown {
|
|
if vipsTypeSupportSave[imgtype] {
|
|
po.Format = imgtype
|
|
} else {
|
|
po.Format = imageTypeJPEG
|
|
}
|
|
}
|
|
|
|
img, err := vipsLoadImage(data, imgtype, 1, 1.0, po.Format == imageTypeGIF)
|
|
if err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
defer C.clear_image(&img)
|
|
|
|
if imgtype == imageTypeGIF && po.Format == imageTypeGIF && vipsIsAnimatedGif(img) {
|
|
if err := transformGif(ctx, &img, po); err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
} else {
|
|
if err := transformImage(ctx, &img, data, po, imgtype); err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
}
|
|
|
|
checkTimeout(ctx)
|
|
|
|
if po.Format == imageTypeGIF {
|
|
if err := vipsCastUchar(&img); err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
checkTimeout(ctx)
|
|
}
|
|
|
|
return vipsSaveImage(img, po.Format, po.Quality)
|
|
}
|
|
|
|
func vipsPrepareWatermark() error {
|
|
data, imgtype, cancel, err := watermarkData()
|
|
defer cancel()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
watermark, err = vipsLoadImage(data, imgtype, 1, 1.0, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_apply_opacity(watermark, &tmp, C.double(conf.WatermarkOpacity)) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(&watermark, tmp)
|
|
|
|
if tmp = C.vips_image_copy_memory(watermark); tmp == nil {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(&watermark, tmp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsLoadImage(data []byte, imgtype imageType, shrink int, svgScale float64, allPages bool) (*C.VipsImage, error) {
|
|
var img *C.VipsImage
|
|
|
|
err := C.int(0)
|
|
|
|
switch imgtype {
|
|
case imageTypeJPEG:
|
|
err = C.vips_jpegload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(shrink), &img)
|
|
case imageTypePNG:
|
|
err = C.vips_pngload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), &img)
|
|
case imageTypeWEBP:
|
|
err = C.vips_webpload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(shrink), &img)
|
|
case imageTypeGIF:
|
|
pages := C.int(1)
|
|
if allPages {
|
|
pages = -1
|
|
}
|
|
|
|
err = C.vips_gifload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), pages, &img)
|
|
case imageTypeSVG:
|
|
err = C.vips_svgload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.double(svgScale), &img)
|
|
case imageTypeICO:
|
|
rawData, width, height, icoErr := icoData(data)
|
|
if icoErr != nil {
|
|
return nil, icoErr
|
|
}
|
|
|
|
img = C.vips_image_new_from_memory_copy(unsafe.Pointer(&rawData[0]), C.size_t(width*height*4), C.int(width), C.int(height), 4, C.VIPS_FORMAT_UCHAR)
|
|
}
|
|
if err != 0 {
|
|
return nil, vipsError()
|
|
}
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func vipsSaveImage(img *C.VipsImage, imgtype imageType, quality int) ([]byte, context.CancelFunc, error) {
|
|
var ptr unsafe.Pointer
|
|
|
|
cancel := func() {
|
|
C.g_free_go(&ptr)
|
|
}
|
|
|
|
err := C.int(0)
|
|
|
|
imgsize := C.size_t(0)
|
|
|
|
switch imgtype {
|
|
case imageTypeJPEG:
|
|
err = C.vips_jpegsave_go(img, &ptr, &imgsize, 1, C.int(quality), cConf.JpegProgressive)
|
|
case imageTypePNG:
|
|
if err = C.vips_pngsave_go(img, &ptr, &imgsize, cConf.PngInterlaced, 1); err != 0 {
|
|
C.g_free_go(&ptr)
|
|
logWarning("Failed to save PNG; Trying not to embed icc profile")
|
|
err = C.vips_pngsave_go(img, &ptr, &imgsize, cConf.PngInterlaced, 0)
|
|
}
|
|
case imageTypeWEBP:
|
|
err = C.vips_webpsave_go(img, &ptr, &imgsize, 1, C.int(quality))
|
|
case imageTypeGIF:
|
|
err = C.vips_gifsave_go(img, &ptr, &imgsize)
|
|
case imageTypeICO:
|
|
err = C.vips_icosave_go(img, &ptr, &imgsize)
|
|
}
|
|
if err != 0 {
|
|
C.g_free_go(&ptr)
|
|
return nil, cancel, vipsError()
|
|
}
|
|
|
|
const maxBufSize = ^uint32(0)
|
|
|
|
b := (*[maxBufSize]byte)(ptr)[:int(imgsize):int(imgsize)]
|
|
|
|
return b, cancel, nil
|
|
}
|
|
|
|
func vipsArrayjoin(in []*C.VipsImage, out **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_arrayjoin_go(&in[0], &tmp, C.int(len(in))) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(out, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsIsAnimatedGif(img *C.VipsImage) bool {
|
|
return C.vips_is_animated_gif(img) > 0
|
|
}
|
|
|
|
func vipsImageHasAlpha(img *C.VipsImage) bool {
|
|
return C.vips_image_hasalpha_go(img) > 0
|
|
}
|
|
|
|
func vipsGetInt(img *C.VipsImage, name string) (int, error) {
|
|
var i C.int
|
|
|
|
if C.vips_image_get_int(img, cachedCString(name), &i) != 0 {
|
|
return 0, vipsError()
|
|
}
|
|
return int(i), nil
|
|
}
|
|
|
|
func vipsSetInt(img *C.VipsImage, name string, value int) {
|
|
C.vips_image_set_int(img, cachedCString(name), C.int(value))
|
|
}
|
|
|
|
func vipsCastUchar(img **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_image_get_format(*img) != C.VIPS_FORMAT_UCHAR {
|
|
if C.vips_cast_go(*img, &tmp, C.VIPS_FORMAT_UCHAR) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsResize(img **C.VipsImage, scale float64, hasAlpa bool) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if hasAlpa {
|
|
if C.vips_resize_with_premultiply(*img, &tmp, C.double(scale)) != 0 {
|
|
return vipsError()
|
|
}
|
|
} else {
|
|
if C.vips_resize_go(*img, &tmp, C.double(scale)) != 0 {
|
|
return vipsError()
|
|
}
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsRotate(img **C.VipsImage, angle int) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_rot_go(*img, &tmp, C.VipsAngle(angle)) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsFlip(img **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_flip_horizontal_go(*img, &tmp) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsCrop(img **C.VipsImage, left, top, width, height int) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_extract_area_go(*img, &tmp, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsExtract(in *C.VipsImage, out **C.VipsImage, left, top, width, height int) error {
|
|
if C.vips_extract_area_go(in, out, C.int(left), C.int(top), C.int(width), C.int(height)) != 0 {
|
|
return vipsError()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func vipsSmartCrop(img **C.VipsImage, width, height int) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_smartcrop_go(*img, &tmp, C.int(width), C.int(height)) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsEnsureAlpha(img **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_ensure_alpha(*img, &tmp) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsFlatten(img **C.VipsImage, bg rgbColor) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_flatten_go(*img, &tmp, C.double(bg.R), C.double(bg.G), C.double(bg.B)) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsBlur(img **C.VipsImage, sigma float32) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_gaussblur_go(*img, &tmp, C.double(sigma)) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsSharpen(img **C.VipsImage, sigma float32) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_sharpen_go(*img, &tmp, C.double(sigma)) != 0 {
|
|
return vipsError()
|
|
}
|
|
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsImportColourProfile(img **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_need_icc_import(*img) > 0 {
|
|
profile, err := cmykProfilePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if C.vips_icc_import_go(*img, &tmp, cachedCString(profile)) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsFixColourspace(img **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_image_guess_interpretation(*img) != C.VIPS_INTERPRETATION_sRGB {
|
|
if C.vips_colourspace_go(*img, &tmp, C.VIPS_INTERPRETATION_sRGB) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsImageCopyMemory(img **C.VipsImage) error {
|
|
var tmp *C.VipsImage
|
|
if tmp = C.vips_image_copy_memory(*img); tmp == nil {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
return nil
|
|
}
|
|
|
|
func vipsReplicate(img **C.VipsImage, width, height C.int) error {
|
|
var tmp *C.VipsImage
|
|
|
|
if C.vips_replicate_go(*img, &tmp, width, height) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsEmbed(img **C.VipsImage, gravity gravityType, width, height C.int, offX, offY C.int) error {
|
|
wmWidth := (*img).Xsize
|
|
wmHeight := (*img).Ysize
|
|
|
|
left := (width-wmWidth+1)/2 + offX
|
|
top := (height-wmHeight+1)/2 + offY
|
|
|
|
if gravity == gravityNorth || gravity == gravityNorthEast || gravity == gravityNorthWest {
|
|
top = offY
|
|
}
|
|
|
|
if gravity == gravityEast || gravity == gravityNorthEast || gravity == gravitySouthEast {
|
|
left = width - wmWidth - offX
|
|
}
|
|
|
|
if gravity == gravitySouth || gravity == gravitySouthEast || gravity == gravitySouthWest {
|
|
top = height - wmHeight - offY
|
|
}
|
|
|
|
if gravity == gravityWest || gravity == gravityNorthWest || gravity == gravitySouthWest {
|
|
left = offX
|
|
}
|
|
|
|
if left > width {
|
|
left = width - wmWidth
|
|
} else if left < -wmWidth {
|
|
left = 0
|
|
}
|
|
|
|
if top > height {
|
|
top = height - wmHeight
|
|
} else if top < -wmHeight {
|
|
top = 0
|
|
}
|
|
|
|
var tmp *C.VipsImage
|
|
if C.vips_embed_go(*img, &tmp, left, top, width, height) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsResizeWatermark(width, height int) (wm *C.VipsImage, err error) {
|
|
wmW := float64(watermark.Xsize)
|
|
wmH := float64(watermark.Ysize)
|
|
|
|
wr := float64(width) / wmW
|
|
hr := float64(height) / wmH
|
|
|
|
scale := math.Min(wr, hr)
|
|
|
|
if wmW*scale < 1 {
|
|
scale = 1 / wmW
|
|
}
|
|
|
|
if wmH*scale < 1 {
|
|
scale = 1 / wmH
|
|
}
|
|
|
|
if C.vips_resize_with_premultiply(watermark, &wm, C.double(scale)) != 0 {
|
|
err = vipsError()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func vipsApplyWatermark(img **C.VipsImage, opts *watermarkOptions) error {
|
|
if watermark == nil {
|
|
return nil
|
|
}
|
|
|
|
var wm, tmp *C.VipsImage
|
|
defer C.clear_image(&wm)
|
|
|
|
var err error
|
|
|
|
imgW := (*img).Xsize
|
|
imgH := (*img).Ysize
|
|
|
|
if opts.Scale == 0 {
|
|
if C.vips_copy_go(watermark, &wm) != 0 {
|
|
return vipsError()
|
|
}
|
|
} else {
|
|
wmW := maxInt(int(float64(imgW)*opts.Scale), 1)
|
|
wmH := maxInt(int(float64(imgH)*opts.Scale), 1)
|
|
|
|
if wm, err = vipsResizeWatermark(wmW, wmH); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.Replicate {
|
|
if err = vipsReplicate(&wm, imgW, imgH); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err = vipsEmbed(&wm, opts.Gravity, imgW, imgH, C.int(opts.OffsetX), C.int(opts.OffsetY)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if C.vips_apply_watermark(*img, wm, &tmp, C.double(opts.Opacity)) != 0 {
|
|
return vipsError()
|
|
}
|
|
C.swap_and_clear(img, tmp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func vipsError() error {
|
|
return errors.New(C.GoString(C.vips_error_buffer()))
|
|
}
|