1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2024-11-29 08:22:11 +02:00
imgproxy/process.go

606 lines
14 KiB
Go
Raw Normal View History

2017-06-20 15:58:55 +02:00
package main
import (
2018-10-05 17:17:36 +02:00
"context"
2017-06-20 15:58:55 +02:00
"math"
2017-09-27 10:42:49 +02:00
"runtime"
2018-11-08 12:34:21 +02:00
"golang.org/x/sync/errgroup"
2017-09-27 10:42:49 +02:00
)
const msgSmartCropNotSupported = "Smart crop is not supported by used version of libvips"
func extractMeta(img *vipsImage) (int, int, int, bool) {
width := img.Width()
height := img.Height()
2018-12-02 15:02:19 +02:00
angle := vipsAngleD0
2017-10-18 00:23:17 +02:00
flip := false
orientation := img.Orientation()
2019-03-22 16:57:10 +02:00
if orientation >= 5 && orientation <= 8 {
width, height = height, width
}
if orientation == 3 || orientation == 4 {
angle = vipsAngleD180
}
if orientation == 5 || orientation == 6 {
angle = vipsAngleD90
}
if orientation == 7 || orientation == 8 {
angle = vipsAngleD270
}
if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
2017-10-18 00:23:17 +02:00
flip = true
}
return width, height, angle, flip
}
2018-12-11 12:02:23 +02:00
func calcScale(width, height int, po *processingOptions, imgtype imageType) float64 {
2018-10-28 14:20:58 +02:00
var scale float64
2017-06-20 15:58:55 +02:00
2018-11-15 13:19:27 +02:00
srcW, srcH := float64(width), float64(height)
if (po.Width == 0 || po.Width == width) && (po.Height == 0 || po.Height == height) {
scale = 1
2018-10-28 14:20:58 +02:00
} else {
2018-11-15 13:19:27 +02:00
wr := float64(po.Width) / srcW
hr := float64(po.Height) / srcH
2019-06-24 14:50:17 +02:00
rt := po.Resize
if rt == resizeAuto {
srcD := width - height
dstD := po.Width - po.Height
if (srcD >= 0 && dstD >= 0) || (srcD < 0 && dstD < 0) {
rt = resizeFill
} else {
rt = resizeFit
}
}
switch {
case po.Width == 0:
2018-11-15 13:19:27 +02:00
scale = hr
case po.Height == 0:
2018-11-15 13:19:27 +02:00
scale = wr
case rt == resizeFit:
2018-11-15 13:19:27 +02:00
scale = math.Min(wr, hr)
default:
2018-11-15 13:19:27 +02:00
scale = math.Max(wr, hr)
}
}
2018-12-11 12:02:23 +02:00
if !po.Enlarge && scale > 1 && imgtype != imageTypeSVG {
2019-06-26 10:37:08 +02:00
scale = 1
}
scale *= po.Dpr
2019-06-26 10:37:08 +02:00
2018-10-28 14:20:58 +02:00
if srcW*scale < 1 {
scale = 1 / srcW
}
2018-10-28 14:20:58 +02:00
if srcH*scale < 1 {
scale = 1 / srcH
2017-06-20 15:58:55 +02:00
}
2018-10-28 14:20:58 +02:00
return scale
2017-06-20 15:58:55 +02:00
}
func canScaleOnLoad(imgtype imageType, scale float64) bool {
if imgtype == imageTypeSVG {
return true
}
if conf.DisableShrinkOnLoad || scale >= 1 {
return false
}
return imgtype == imageTypeJPEG || imgtype == imageTypeWEBP
}
func calcJpegShink(scale float64, imgtype imageType) int {
shrink := int(1.0 / scale)
switch {
case shrink >= 8:
return 8
case shrink >= 4:
return 4
case shrink >= 2:
return 2
2017-10-17 23:12:58 +02:00
}
return 1
}
2018-11-15 13:19:27 +02:00
func calcCrop(width, height, cropWidth, cropHeight int, gravity *gravityOptions) (left, top int) {
if gravity.Type == gravityFocusPoint {
2019-09-11 12:40:07 +02:00
pointX := scaleInt(width, gravity.X)
pointY := scaleInt(height, gravity.Y)
2018-10-19 11:47:11 +02:00
2018-11-15 13:19:27 +02:00
left = maxInt(0, minInt(pointX-cropWidth/2, width-cropWidth))
top = maxInt(0, minInt(pointY-cropHeight/2, height-cropHeight))
2018-10-19 11:47:11 +02:00
return
}
2019-06-20 14:49:25 +02:00
offX, offY := int(gravity.X), int(gravity.Y)
left = (width-cropWidth+1)/2 + offX
top = (height-cropHeight+1)/2 + offY
2017-06-20 15:58:55 +02:00
2018-11-15 13:19:27 +02:00
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
2019-06-20 14:49:25 +02:00
top = 0 + offY
2017-09-27 10:42:49 +02:00
}
2017-06-20 15:58:55 +02:00
2018-11-15 13:19:27 +02:00
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
2019-06-20 14:49:25 +02:00
left = width - cropWidth - offX
2017-06-20 15:58:55 +02:00
}
2018-11-15 13:19:27 +02:00
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
2019-06-20 14:49:25 +02:00
top = height - cropHeight - offY
2017-06-20 15:58:55 +02:00
}
2018-11-15 13:19:27 +02:00
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
2019-06-20 14:49:25 +02:00
left = 0 + offX
2017-09-27 10:42:49 +02:00
}
2019-06-20 14:49:25 +02:00
left = maxInt(0, minInt(left, width-cropWidth))
top = maxInt(0, minInt(top, height-cropHeight))
2017-09-27 10:42:49 +02:00
return
}
func cropImage(img *vipsImage, cropWidth, cropHeight int, gravity *gravityOptions) error {
if cropWidth == 0 && cropHeight == 0 {
return nil
}
imgWidth, imgHeight := img.Width(), img.Height()
2019-09-11 12:40:07 +02:00
cropWidth = minNonZeroInt(cropWidth, imgWidth)
cropHeight = minNonZeroInt(cropHeight, imgHeight)
2019-06-07 20:27:34 +02:00
if cropWidth >= imgWidth && cropHeight >= imgHeight {
return nil
}
if gravity.Type == gravitySmart {
if err := img.CopyMemory(); err != nil {
return err
}
if err := img.SmartCrop(cropWidth, cropHeight); err != nil {
return err
}
2019-06-07 20:27:34 +02:00
// Applying additional modifications after smart crop causes SIGSEGV on Alpine
// so we have to copy memory after it
return img.CopyMemory()
}
2019-06-07 20:27:34 +02:00
left, top := calcCrop(imgWidth, imgHeight, cropWidth, cropHeight, gravity)
return img.Crop(left, top, cropWidth, cropHeight)
}
2019-08-28 13:08:53 +02:00
func prepareWatermark(wm *vipsImage, wmData *watermarkData, opts *watermarkOptions, imgWidth, imgHeight int) error {
if err := wm.Load(wmData.data, wmData.imgtype, 1, 1.0, 1); err != nil {
return err
}
2019-09-11 12:40:07 +02:00
po := newProcessingOptions()
po.Resize = resizeFit
po.Dpr = 1
po.Enlarge = true
po.Format = wmData.imgtype
2019-08-28 13:08:53 +02:00
if opts.Scale > 0 {
2019-09-11 12:40:07 +02:00
po.Width = maxInt(scaleInt(imgWidth, opts.Scale), 1)
po.Height = maxInt(scaleInt(imgHeight, opts.Scale), 1)
2019-08-28 13:08:53 +02:00
}
2019-09-11 12:40:07 +02:00
if err := transformImage(context.Background(), wm, wmData.data, po, wmData.imgtype); err != nil {
2019-08-28 13:08:53 +02:00
return err
}
if err := wm.EnsureAlpha(); err != nil {
return nil
}
if opts.Replicate {
2019-09-11 11:45:11 +02:00
return wm.Replicate(imgWidth, imgHeight)
2019-08-28 13:08:53 +02:00
}
2019-09-11 11:45:11 +02:00
return wm.Embed(opts.Gravity, imgWidth, imgHeight, opts.OffsetX, opts.OffsetY, rgbColor{0, 0, 0})
2019-08-28 13:08:53 +02:00
}
func applyWatermark(img *vipsImage, wmData *watermarkData, opts *watermarkOptions, framesCount int) error {
wm := new(vipsImage)
defer wm.Clear()
width := img.Width()
height := img.Height()
if err := prepareWatermark(wm, wmData, opts, width, height/framesCount); err != nil {
return err
}
if framesCount > 1 {
if err := wm.Replicate(width, height); err != nil {
return err
}
}
opacity := opts.Opacity * conf.WatermarkOpacity
return img.ApplyWatermark(wm, opacity)
}
func transformImage(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
2018-11-15 13:19:27 +02:00
var err error
2018-10-02 14:20:23 +02:00
srcWidth, srcHeight, angle, flip := extractMeta(img)
cropWidth, cropHeight := po.Crop.Width, po.Crop.Height
2019-06-20 14:49:25 +02:00
cropGravity := po.Crop.Gravity
if cropGravity.Type == gravityUnknown {
cropGravity = po.Gravity
}
2019-09-11 12:40:07 +02:00
widthToScale := minNonZeroInt(cropWidth, srcWidth)
heightToScale := minNonZeroInt(cropHeight, srcHeight)
scale := calcScale(widthToScale, heightToScale, po, imgtype)
2018-11-15 13:19:27 +02:00
2019-09-11 12:40:07 +02:00
cropWidth = scaleInt(cropWidth, scale)
cropHeight = scaleInt(cropHeight, scale)
cropGravity.X *= scale
cropGravity.Y *= scale
2019-03-22 16:57:10 +02:00
if scale != 1 && data != nil && canScaleOnLoad(imgtype, scale) {
if imgtype == imageTypeWEBP || imgtype == imageTypeSVG {
// Do some scale-on-load
if err = img.Load(data, imgtype, 1, scale, 1); err != nil {
2018-12-11 12:02:23 +02:00
return err
}
} else if imgtype == imageTypeJPEG {
2018-12-11 12:02:23 +02:00
// Do some shrink-on-load
if shrink := calcJpegShink(scale, imgtype); shrink != 1 {
if err = img.Load(data, imgtype, shrink, 1.0, 1); err != nil {
return err
2019-03-22 16:57:10 +02:00
}
2018-12-11 12:02:23 +02:00
}
}
// Update scale after scale-on-load
newWidth, newHeight, _, _ := extractMeta(img)
2019-09-11 12:40:07 +02:00
widthToScale = scaleInt(widthToScale, float64(newWidth)/float64(srcWidth))
heightToScale = scaleInt(heightToScale, float64(newHeight)/float64(srcHeight))
scale = calcScale(widthToScale, heightToScale, po, imgtype)
2019-03-22 16:57:10 +02:00
}
if err = img.Rad2Float(); err != nil {
2019-03-22 16:57:10 +02:00
return err
}
2019-06-26 09:07:19 +02:00
iccImported := false
convertToLinear := conf.UseLinearColorspace && (scale != 1 || po.Dpr != 1)
2019-04-01 17:30:24 +02:00
2019-06-26 09:07:19 +02:00
if convertToLinear || !img.IsSRGB() {
if err = img.ImportColourProfile(true); err != nil {
return err
}
2019-06-26 09:07:19 +02:00
iccImported = true
}
2019-06-26 09:07:19 +02:00
if convertToLinear {
if err = img.LinearColourspace(); err != nil {
2019-04-01 17:30:24 +02:00
return err
}
2019-06-26 09:07:19 +02:00
} else {
if err = img.RgbColourspace(); err != nil {
return err
}
2019-03-22 16:57:10 +02:00
}
hasAlpha := img.HasAlpha()
2019-03-22 16:57:10 +02:00
if scale != 1 {
if err = img.Resize(scale, hasAlpha); err != nil {
2019-03-22 16:57:10 +02:00
return err
}
}
2018-10-05 17:17:36 +02:00
checkTimeout(ctx)
if angle != vipsAngleD0 || flip {
if err = img.CopyMemory(); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
if angle != vipsAngleD0 {
if err = img.Rotate(angle); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
2017-09-27 10:42:49 +02:00
}
2017-09-27 21:17:17 +02:00
if flip {
if err = img.Flip(); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
}
}
2018-10-05 17:17:36 +02:00
checkTimeout(ctx)
2017-09-27 21:17:17 +02:00
2019-09-11 12:40:07 +02:00
dprWidth := scaleInt(po.Width, po.Dpr)
dprHeight := scaleInt(po.Height, po.Dpr)
if cropGravity.Type == po.Gravity.Type && cropGravity.Type != gravityFocusPoint {
2019-09-11 12:40:07 +02:00
cropWidth = minNonZeroInt(cropWidth, dprWidth)
cropHeight = minNonZeroInt(cropHeight, dprHeight)
2017-09-27 10:42:49 +02:00
2019-06-20 14:49:25 +02:00
sumGravity := gravityOptions{
Type: cropGravity.Type,
X: cropGravity.X + po.Gravity.X,
Y: cropGravity.Y + po.Gravity.Y,
}
if err = cropImage(img, cropWidth, cropHeight, &sumGravity); err != nil {
2018-11-15 13:19:27 +02:00
return err
}
} else {
if err = cropImage(img, cropWidth, cropHeight, &cropGravity); err != nil {
return err
}
if err = cropImage(img, dprWidth, dprHeight, &po.Gravity); err != nil {
2018-11-15 13:19:27 +02:00
return err
}
}
checkTimeout(ctx)
2019-06-26 09:07:19 +02:00
if !iccImported {
if err = img.ImportColourProfile(false); err != nil {
return err
}
2019-04-01 17:30:24 +02:00
}
2019-06-26 09:07:19 +02:00
if err = img.RgbColourspace(); err != nil {
return err
}
if hasAlpha && (po.Flatten || po.Format == imageTypeJPEG) {
if err = img.Flatten(po.Background); err != nil {
2018-11-08 12:34:21 +02:00
return err
2018-10-02 14:20:23 +02:00
}
}
2018-09-07 16:46:16 +02:00
if po.Blur > 0 {
if err = img.Blur(po.Blur); err != nil {
2018-11-08 12:34:21 +02:00
return err
2018-09-07 16:46:16 +02:00
}
}
if po.Sharpen > 0 {
if err = img.Sharpen(po.Sharpen); err != nil {
2018-11-08 12:34:21 +02:00
return err
2018-09-07 16:46:16 +02:00
}
}
2019-06-17 12:15:10 +02:00
if po.Extend && (po.Width > img.Width() || po.Height > img.Height()) {
2019-06-17 12:13:34 +02:00
if err = img.Embed(gravityCenter, po.Width, po.Height, 0, 0, po.Background); err != nil {
return err
}
}
2018-10-05 17:17:36 +02:00
checkTimeout(ctx)
2018-09-07 16:46:16 +02:00
2019-08-28 13:08:53 +02:00
if po.Watermark.Enabled && watermark != nil {
if err = applyWatermark(img, watermark, &po.Watermark, 1); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
}
2019-06-10 16:15:49 +02:00
return img.RgbColourspace()
2018-11-08 12:34:21 +02:00
}
func transformAnimated(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
imgWidth := img.Width()
2018-11-08 12:34:21 +02:00
frameHeight, err := img.GetInt("page-height")
2019-06-03 12:53:47 +02:00
if err != nil {
2018-11-08 12:34:21 +02:00
return err
}
2019-06-11 17:42:21 +02:00
framesCount := minInt(img.Height()/frameHeight, conf.MaxAnimationFrames)
2019-06-03 12:53:47 +02:00
// Double check dimensions because animated image has many frames
if err = checkDimensions(imgWidth, frameHeight*framesCount); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
2019-06-03 12:53:47 +02:00
// Vips 8.8+ supports n-pages and doesn't load the whole animated image on header access
if nPages, _ := img.GetInt("n-pages"); nPages > 0 {
scale := 1.0
// Don't do scale on load if we need to crop
if po.Crop.Width == 0 && po.Crop.Height == 0 {
scale = calcScale(imgWidth, frameHeight, po, imgtype)
}
2019-06-03 12:53:47 +02:00
if nPages > framesCount || canScaleOnLoad(imgtype, scale) {
logNotice("Animated scale on load")
// Do some scale-on-load and load only the needed frames
if err = img.Load(data, imgtype, 1, scale, framesCount); err != nil {
2019-06-03 12:53:47 +02:00
return err
}
}
imgWidth = img.Width()
2019-06-03 12:53:47 +02:00
frameHeight, err = img.GetInt("page-height")
2019-06-03 12:53:47 +02:00
if err != nil {
return err
}
}
delay, err := img.GetInt("gif-delay")
2018-11-08 12:34:21 +02:00
if err != nil {
return err
}
loop, err := img.GetInt("gif-loop")
2018-11-08 12:34:21 +02:00
if err != nil {
return err
}
frames := make([]*vipsImage, framesCount)
2018-11-08 12:34:21 +02:00
defer func() {
for _, frame := range frames {
frame.Clear()
2018-11-08 12:34:21 +02:00
}
}()
2019-08-28 13:08:53 +02:00
watermarkEnabled := po.Watermark.Enabled
po.Watermark.Enabled = false
defer func() { po.Watermark.Enabled = watermarkEnabled }()
2018-11-08 12:34:21 +02:00
var errg errgroup.Group
for i := 0; i < framesCount; i++ {
ind := i
errg.Go(func() error {
frame := new(vipsImage)
2018-11-08 12:34:21 +02:00
if err = img.Extract(frame, 0, ind*frameHeight, imgWidth, frameHeight); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
if err = transformImage(ctx, frame, nil, po, imgtype); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
frames[ind] = frame
return nil
})
}
if err = errg.Wait(); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
checkTimeout(ctx)
if err = img.Arrayjoin(frames); err != nil {
2018-11-08 12:34:21 +02:00
return err
}
2019-08-28 13:08:53 +02:00
if watermarkEnabled && watermark != nil {
if err = applyWatermark(img, watermark, &po.Watermark, framesCount); err != nil {
return err
}
}
img.SetInt("page-height", frames[0].Height())
img.SetInt("gif-delay", delay)
img.SetInt("gif-loop", loop)
img.SetInt("n-pages", framesCount)
2018-11-08 12:34:21 +02:00
return nil
}
func processImage(ctx context.Context) ([]byte, context.CancelFunc, error) {
2019-01-09 14:41:00 +02:00
runtime.LockOSThread()
defer runtime.UnlockOSThread()
2018-11-08 12:34:21 +02:00
if newRelicEnabled {
newRelicCancel := startNewRelicSegment(ctx, "Processing image")
defer newRelicCancel()
}
if prometheusEnabled {
defer startPrometheusDuration(prometheusProcessingDuration)()
}
defer vipsCleanup()
2018-11-08 12:34:21 +02:00
po := getProcessingOptions(ctx)
2018-11-08 12:34:21 +02:00
data := getImageData(ctx).Bytes()
imgtype := getImageType(ctx)
if po.Format == imageTypeUnknown {
switch {
case po.PreferWebP && vipsTypeSupportSave[imageTypeWEBP]:
po.Format = imageTypeWEBP
case vipsTypeSupportSave[imgtype] && imgtype != imageTypeHEIC:
po.Format = imgtype
default:
po.Format = imageTypeJPEG
}
} else if po.EnforceWebP && vipsTypeSupportSave[imageTypeWEBP] {
po.Format = imageTypeWEBP
}
if !vipsSupportSmartcrop {
if po.Gravity.Type == gravitySmart {
logWarning(msgSmartCropNotSupported)
po.Gravity.Type = gravityCenter
}
if po.Crop.Gravity.Type == gravitySmart {
logWarning(msgSmartCropNotSupported)
po.Crop.Gravity.Type = gravityCenter
}
}
if po.Resize == resizeCrop {
logWarning("`crop` resizing type is deprecated and will be removed in future versions. Use `crop` processing option instead")
po.Crop.Width, po.Crop.Height = po.Width, po.Height
po.Resize = resizeFit
po.Width, po.Height = 0, 0
}
2019-06-11 17:42:21 +02:00
animationSupport := conf.MaxAnimationFrames > 1 && vipsSupportAnimation(imgtype) && vipsSupportAnimation(po.Format)
2019-06-03 12:53:47 +02:00
pages := 1
if animationSupport {
pages = -1
}
img := new(vipsImage)
defer img.Clear()
if err := img.Load(data, imgtype, 1, 1.0, pages); err != nil {
return nil, func() {}, err
2018-11-08 12:34:21 +02:00
}
if animationSupport && img.IsAnimated() {
if err := transformAnimated(ctx, img, data, po, imgtype); err != nil {
return nil, func() {}, err
2018-11-08 12:34:21 +02:00
}
} else {
if err := transformImage(ctx, img, data, po, imgtype); err != nil {
return nil, func() {}, err
2018-10-19 11:47:11 +02:00
}
}
2018-11-08 12:34:21 +02:00
checkTimeout(ctx)
if po.Format == imageTypeGIF {
if err := img.CastUchar(); err != nil {
return nil, func() {}, err
2018-11-08 12:34:21 +02:00
}
checkTimeout(ctx)
}
return img.Save(po.Format, po.Quality)
2017-06-20 15:58:55 +02:00
}