1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2025-02-02 11:34:20 +02:00
imgproxy/process.go

904 lines
20 KiB
Go
Raw Normal View History

2017-06-20 16:58:55 +03:00
package main
import (
2019-10-03 21:43:33 +06:00
"bytes"
2018-10-05 21:17:36 +06:00
"context"
2019-10-03 21:43:33 +06:00
"fmt"
2017-06-20 16:58:55 +03:00
"math"
2017-09-27 14:42:49 +06:00
"runtime"
2018-11-08 16:34:21 +06:00
2020-02-27 22:30:31 +06:00
"github.com/imgproxy/imgproxy/v2/imagemeta"
2017-09-27 14:42:49 +06:00
)
const (
// https://chromium.googlesource.com/webm/libwebp/+/refs/heads/master/src/webp/encode.h#529
webpMaxDimension = 16383.0
)
2019-10-01 18:08:30 +06:00
var errConvertingNonSvgToSvg = newError(422, "Converting non-SVG images to SVG is not supported", "Converting non-SVG images to SVG is not supported")
func imageTypeLoadSupport(imgtype imageType) bool {
2019-10-03 21:43:33 +06:00
return imgtype == imageTypeSVG ||
imgtype == imageTypeICO ||
vipsTypeSupportLoad[imgtype]
2019-10-01 18:08:30 +06:00
}
func imageTypeSaveSupport(imgtype imageType) bool {
return imgtype == imageTypeSVG || vipsTypeSupportSave[imgtype]
}
2019-10-01 18:12:14 +06:00
func imageTypeGoodForWeb(imgtype imageType) bool {
return imgtype != imageTypeTIFF &&
2019-10-01 18:12:14 +06:00
imgtype != imageTypeBMP
}
func canSwitchFormat(src, dst, want imageType) bool {
return imageTypeSaveSupport(want) &&
(!vipsSupportAnimation(src) ||
(dst != imageTypeUnknown && !vipsSupportAnimation(dst)) ||
vipsSupportAnimation(want))
}
2021-01-13 20:51:19 +06:00
func extractMeta(img *vipsImage, baseAngle int, useOrientation bool) (int, int, int, bool) {
width := img.Width()
height := img.Height()
2018-12-02 19:02:19 +06:00
2021-01-13 20:51:19 +06:00
angle := 0
2017-10-18 04:23:17 +06:00
flip := false
2021-01-13 20:51:19 +06:00
if useOrientation {
orientation := img.Orientation()
2021-01-13 20:51:19 +06:00
if orientation == 3 || orientation == 4 {
angle = 180
}
if orientation == 5 || orientation == 6 {
angle = 90
}
if orientation == 7 || orientation == 8 {
angle = 270
}
if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
flip = true
}
}
2019-03-22 20:57:10 +06:00
2021-01-13 20:51:19 +06:00
if (angle+baseAngle)%180 != 0 {
width, height = height, width
}
return width, height, angle, flip
}
2021-03-29 17:13:28 +06:00
func calcScale(width, height int, po *processingOptions, imgtype imageType) (float64, float64) {
var wshrink, hshrink float64
2017-06-20 16:58:55 +03:00
2018-11-15 17:19:27 +06:00
srcW, srcH := float64(width), float64(height)
2019-09-16 23:09:28 +06:00
dstW, dstH := float64(po.Width), float64(po.Height)
2018-11-15 17:19:27 +06:00
2019-09-16 23:09:28 +06:00
if po.Width == 0 {
dstW = srcW
}
2021-03-29 17:13:28 +06:00
if dstW == srcW {
wshrink = 1
} else {
wshrink = srcW / dstW
}
2019-09-16 23:09:28 +06:00
if po.Height == 0 {
dstH = srcH
}
2021-03-29 17:13:28 +06:00
if dstH == srcH {
hshrink = 1
2018-10-28 18:20:58 +06:00
} else {
2021-03-29 17:13:28 +06:00
hshrink = srcH / dstH
}
2018-11-15 17:19:27 +06:00
2021-03-29 17:13:28 +06:00
if wshrink != 1 || hshrink != 1 {
2019-10-11 17:05:20 +06:00
rt := po.ResizingType
2019-06-24 18:50:17 +06:00
if rt == resizeAuto {
2021-03-29 17:13:28 +06:00
srcD := srcW - srcH
dstD := dstW - dstH
2019-06-24 18:50:17 +06:00
if (srcD >= 0 && dstD >= 0) || (srcD < 0 && dstD < 0) {
rt = resizeFill
} else {
rt = resizeFit
}
}
switch {
case po.Width == 0 && rt != resizeForce:
2021-03-29 17:13:28 +06:00
wshrink = hshrink
case po.Height == 0 && rt != resizeForce:
2021-03-29 17:13:28 +06:00
hshrink = wshrink
case rt == resizeFit:
2021-03-29 17:13:28 +06:00
wshrink = math.Max(wshrink, hshrink)
hshrink = wshrink
2021-03-29 17:35:16 +06:00
case rt == resizeFill || rt == resizeFillDown:
2021-03-29 17:13:28 +06:00
wshrink = math.Min(wshrink, hshrink)
hshrink = wshrink
2018-11-15 17:19:27 +06:00
}
}
2021-03-29 17:13:28 +06:00
if !po.Enlarge && imgtype != imageTypeSVG {
if wshrink < 1 {
hshrink /= wshrink
wshrink = 1
}
if hshrink < 1 {
wshrink /= hshrink
hshrink = 1
}
}
if po.MinWidth > 0 {
if minShrink := srcW / float64(po.MinWidth); minShrink < wshrink {
hshrink /= wshrink / minShrink
wshrink = minShrink
}
}
if po.MinHeight > 0 {
if minShrink := srcH / float64(po.MinHeight); minShrink < hshrink {
wshrink /= hshrink / minShrink
hshrink = minShrink
}
}
2021-03-29 17:13:28 +06:00
wshrink /= po.Dpr
hshrink /= po.Dpr
2019-06-26 14:37:08 +06:00
2021-03-29 17:13:28 +06:00
if wshrink > srcW {
wshrink = srcW
}
2021-03-29 17:13:28 +06:00
if hshrink > srcH {
hshrink = srcH
2017-06-20 16:58:55 +03:00
}
2021-03-29 17:13:28 +06:00
return 1.0 / wshrink, 1.0 / hshrink
2017-06-20 16:58:55 +03: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 canFitToBytes(imgtype imageType) bool {
switch imgtype {
2020-09-16 16:15:01 +06:00
case imageTypeJPEG, imageTypeWEBP, imageTypeAVIF, imageTypeTIFF:
return true
default:
return false
}
}
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-18 03:12:58 +06:00
}
return 1
}
2021-01-18 21:14:15 +06:00
func calcCropSize(orig int, crop float64) int {
switch {
case crop == 0.0:
return 0
case crop >= 1.0:
return int(crop)
default:
return maxInt(1, scaleInt(orig, crop))
}
}
2019-12-25 16:50:02 +06:00
func calcPosition(width, height, innerWidth, innerHeight int, gravity *gravityOptions, allowOverflow bool) (left, top int) {
2018-11-15 17:19:27 +06:00
if gravity.Type == gravityFocusPoint {
2019-09-11 16:40:07 +06:00
pointX := scaleInt(width, gravity.X)
pointY := scaleInt(height, gravity.Y)
2018-10-19 15:47:11 +06:00
2019-12-25 16:50:02 +06:00
left = pointX - innerWidth/2
top = pointY - innerHeight/2
} else {
offX, offY := int(gravity.X), int(gravity.Y)
2018-10-19 15:47:11 +06:00
2019-12-25 16:50:02 +06:00
left = (width-innerWidth+1)/2 + offX
top = (height-innerHeight+1)/2 + offY
2018-10-19 15:47:11 +06:00
2019-12-25 16:50:02 +06:00
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
top = 0 + offY
}
2019-06-20 18:49:25 +06:00
2019-12-25 16:50:02 +06:00
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
left = width - innerWidth - offX
}
2017-06-20 16:58:55 +03:00
2019-12-25 16:50:02 +06:00
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
top = height - innerHeight - offY
}
2017-06-20 16:58:55 +03:00
2019-12-25 16:50:02 +06:00
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
left = 0 + offX
}
2017-06-20 16:58:55 +03:00
}
2019-12-25 16:50:02 +06:00
var minX, maxX, minY, maxY int
2017-06-20 16:58:55 +03:00
2019-12-25 16:50:02 +06:00
if allowOverflow {
minX, maxX = -innerWidth+1, width-1
minY, maxY = -innerHeight+1, height-1
} else {
minX, maxX = 0, width-innerWidth
minY, maxY = 0, height-innerHeight
2017-09-27 14:42:49 +06:00
}
2019-12-25 16:50:02 +06:00
left = maxInt(minX, minInt(left, maxX))
top = maxInt(minY, minInt(top, maxY))
2019-06-20 18:49:25 +06:00
2017-09-27 14:42:49 +06: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 16:40:07 +06:00
cropWidth = minNonZeroInt(cropWidth, imgWidth)
cropHeight = minNonZeroInt(cropHeight, imgHeight)
2019-06-08 00:27:34 +06: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-08 00:27:34 +06:00
// Applying additional modifications after smart crop causes SIGSEGV on Alpine
// so we have to copy memory after it
return img.CopyMemory()
}
2019-12-25 16:50:02 +06:00
left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, false)
2019-06-08 00:27:34 +06:00
return img.Crop(left, top, cropWidth, cropHeight)
}
func prepareWatermark(wm *vipsImage, wmData *imageData, opts *watermarkOptions, imgWidth, imgHeight int) error {
if err := wm.Load(wmData.Data, wmData.Type, 1, 1.0, 1); err != nil {
2019-08-28 17:08:53 +06:00
return err
}
2019-09-11 16:40:07 +06:00
po := newProcessingOptions()
2019-10-11 17:05:20 +06:00
po.ResizingType = resizeFit
2019-09-11 16:40:07 +06:00
po.Dpr = 1
po.Enlarge = true
po.Format = wmData.Type
2019-08-28 17:08:53 +06:00
if opts.Scale > 0 {
2019-09-11 16:40:07 +06:00
po.Width = maxInt(scaleInt(imgWidth, opts.Scale), 1)
po.Height = maxInt(scaleInt(imgHeight, opts.Scale), 1)
2019-08-28 17:08:53 +06:00
}
if err := transformImage(context.Background(), wm, wmData.Data, po, wmData.Type); err != nil {
2019-08-28 17:08:53 +06:00
return err
}
if err := wm.EnsureAlpha(); err != nil {
return nil
}
if opts.Replicate {
2019-09-11 15:45:11 +06:00
return wm.Replicate(imgWidth, imgHeight)
2019-08-28 17:08:53 +06:00
}
left, top := calcPosition(imgWidth, imgHeight, wm.Width(), wm.Height(), &opts.Gravity, true)
2019-12-25 16:50:02 +06:00
return wm.Embed(imgWidth, imgHeight, left, top, rgbColor{0, 0, 0}, true)
2019-08-28 17:08:53 +06:00
}
func applyWatermark(img *vipsImage, wmData *imageData, opts *watermarkOptions, framesCount int) error {
if err := img.RgbColourspace(); err != nil {
return err
}
if err := img.CopyMemory(); err != nil {
return err
}
2019-08-28 17:08:53 +06:00
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 copyMemoryAndCheckTimeout(ctx context.Context, img *vipsImage) error {
err := img.CopyMemory()
checkTimeout(ctx)
return err
}
func transformImage(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
2020-01-17 15:54:50 +06:00
var (
err error
trimmed bool
)
if po.Trim.Enabled {
if err = img.Trim(po.Trim.Threshold, po.Trim.Smart, po.Trim.Color, po.Trim.EqualHor, po.Trim.EqualVer); err != nil {
2020-01-17 15:54:50 +06:00
return err
}
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
return err
}
2020-01-17 15:54:50 +06:00
trimmed = true
}
2018-10-02 18:20:23 +06:00
2021-01-13 20:51:19 +06:00
srcWidth, srcHeight, angle, flip := extractMeta(img, po.Rotate, po.AutoRotate)
2021-01-18 21:14:15 +06:00
cropWidth := calcCropSize(srcWidth, po.Crop.Width)
cropHeight := calcCropSize(srcHeight, po.Crop.Height)
2019-06-20 18:49:25 +06:00
cropGravity := po.Crop.Gravity
if cropGravity.Type == gravityUnknown {
cropGravity = po.Gravity
}
2019-09-11 16:40:07 +06:00
widthToScale := minNonZeroInt(cropWidth, srcWidth)
heightToScale := minNonZeroInt(cropHeight, srcHeight)
2021-03-29 17:13:28 +06:00
wscale, hscale := calcScale(widthToScale, heightToScale, po, imgtype)
2018-11-15 17:19:27 +06:00
2021-01-18 21:14:15 +06:00
if cropWidth > 0 {
2021-03-29 17:13:28 +06:00
cropWidth = maxInt(1, scaleInt(cropWidth, wscale))
2021-01-18 21:14:15 +06:00
}
if cropHeight > 0 {
2021-03-29 17:13:28 +06:00
cropHeight = maxInt(1, scaleInt(cropHeight, hscale))
2021-01-18 21:14:15 +06:00
}
if cropGravity.Type != gravityFocusPoint {
2021-03-29 17:13:28 +06:00
cropGravity.X *= wscale
cropGravity.Y *= hscale
}
2019-03-22 20:57:10 +06:00
2021-03-29 17:13:28 +06:00
prescale := math.Max(wscale, hscale)
if !trimmed && prescale != 1 && data != nil && canScaleOnLoad(imgtype, prescale) {
jpegShrink := calcJpegShink(prescale, imgtype)
2020-09-03 14:46:16 +06:00
if imgtype != imageTypeJPEG || jpegShrink != 1 {
// Do some scale-on-load
2021-03-29 17:13:28 +06:00
if err = img.Load(data, imgtype, jpegShrink, prescale, 1); err != nil {
2018-12-11 16:02:23 +06:00
return err
}
}
2021-03-29 17:13:28 +06:00
// Update scales after scale-on-load
2021-01-13 20:51:19 +06:00
newWidth, newHeight, _, _ := extractMeta(img, po.Rotate, po.AutoRotate)
2021-03-29 17:13:28 +06:00
wscale = float64(srcWidth) * wscale / float64(newWidth)
if srcWidth == scaleInt(srcWidth, wscale) {
wscale = 1.0
2020-09-28 15:57:39 +06:00
}
2021-03-29 17:13:28 +06:00
hscale = float64(srcHeight) * hscale / float64(newHeight)
if srcHeight == scaleInt(srcHeight, hscale) {
hscale = 1.0
2020-09-28 15:57:39 +06:00
}
2019-03-22 20:57:10 +06:00
}
if err = img.Rad2Float(); err != nil {
2019-03-22 20:57:10 +06:00
return err
}
2019-06-26 13:07:19 +06:00
iccImported := false
2021-03-29 17:13:28 +06:00
convertToLinear := conf.UseLinearColorspace && (wscale != 1 || hscale != 1)
2019-04-01 21:30:24 +06:00
if convertToLinear {
2020-11-18 12:22:23 +06:00
if err = img.ImportColourProfile(); err != nil {
return err
}
2019-06-26 13:07:19 +06:00
iccImported = true
}
2019-06-26 13:07:19 +06:00
if convertToLinear {
if err = img.LinearColourspace(); err != nil {
2019-04-01 21:30:24 +06:00
return err
}
2019-06-26 13:07:19 +06:00
} else {
if err = img.RgbColourspace(); err != nil {
return err
}
2019-03-22 20:57:10 +06:00
}
hasAlpha := img.HasAlpha()
2021-03-29 17:13:28 +06:00
if wscale != 1 || hscale != 1 {
if err = img.Resize(wscale, hscale, hasAlpha); err != nil {
2019-03-22 20:57:10 +06:00
return err
}
}
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
2020-04-22 19:02:50 +06:00
return err
}
if err = img.Rotate(angle); err != nil {
return err
2020-04-22 19:02:50 +06:00
}
2020-04-22 19:02:50 +06:00
if flip {
if err = img.Flip(); err != nil {
return err
}
}
2021-01-13 20:51:19 +06:00
if err = img.Rotate(po.Rotate); err != nil {
return err
}
2020-01-15 18:25:14 +06:00
if err = cropImage(img, cropWidth, cropHeight, &cropGravity); err != nil {
return err
}
2021-03-29 17:35:16 +06:00
// Crop image to the result size
resultWidth := scaleInt(po.Width, po.Dpr)
resultHeight := scaleInt(po.Height, po.Dpr)
if po.ResizingType == resizeFillDown {
if resultWidth > img.Width() {
resultHeight = scaleInt(resultHeight, float64(img.Width())/float64(resultWidth))
resultWidth = img.Width()
}
if resultHeight > img.Height() {
resultWidth = scaleInt(resultWidth, float64(img.Height())/float64(resultHeight))
resultHeight = img.Height()
}
}
if err = cropImage(img, resultWidth, resultHeight, &po.Gravity); err != nil {
2020-01-15 18:25:14 +06:00
return err
2018-11-15 17:19:27 +06:00
}
2020-08-31 16:43:40 +06:00
if po.Format == imageTypeWEBP {
webpLimitShrink := float64(maxInt(img.Width(), img.Height())) / webpMaxDimension
if webpLimitShrink > 1.0 {
2021-03-29 17:13:28 +06:00
scale := 1.0 / webpLimitShrink
if err = img.Resize(scale, scale, hasAlpha); err != nil {
2020-08-31 16:43:40 +06:00
return err
}
logWarning("WebP dimension size is limited to %d. The image is rescaled to %dx%d", int(webpMaxDimension), img.Width(), img.Height())
2020-12-28 16:41:07 +06:00
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
return err
}
2020-08-31 16:43:40 +06:00
}
}
2021-01-12 20:43:09 +06:00
keepProfile := !po.StripColorProfile && po.Format.SupportsColourProfile()
2020-11-18 12:22:23 +06:00
if iccImported {
2021-01-12 20:43:09 +06:00
if keepProfile {
// We imported ICC profile and want to keep it,
// so we need to export it
if err = img.ExportColourProfile(); err != nil {
return err
}
} else {
// We imported ICC profile but don't want to keep it,
// so we need to export image to sRGB for maximum compatibility
if err = img.ExportColourProfileToSRGB(); err != nil {
return err
}
2020-11-18 12:22:23 +06:00
}
2021-01-12 20:43:09 +06:00
} else if !keepProfile {
// We don't import ICC profile and don't want to keep it,
// so we need to transform it to sRGB for maximum compatibility
2020-11-18 12:22:23 +06:00
if err = img.TransformColourProfile(); err != nil {
return err
}
2019-06-26 13:07:19 +06:00
}
2021-01-12 20:43:09 +06:00
if err = img.RgbColourspace(); err != nil {
return err
}
if !keepProfile {
if err = img.RemoveColourProfile(); err != nil {
return err
}
}
2020-08-21 18:56:30 +06:00
transparentBg := po.Format.SupportsAlpha() && !po.Flatten
2020-08-21 18:56:30 +06:00
if hasAlpha && !transparentBg {
if err = img.Flatten(po.Background); err != nil {
2018-11-08 16:34:21 +06:00
return err
2018-10-02 18:20:23 +06:00
}
}
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
2020-04-22 19:02:50 +06:00
return err
}
2018-09-07 20:46:16 +06:00
if po.Blur > 0 {
if err = img.Blur(po.Blur); err != nil {
2018-11-08 16:34:21 +06:00
return err
2018-09-07 20:46:16 +06:00
}
}
if po.Sharpen > 0 {
if err = img.Sharpen(po.Sharpen); err != nil {
2018-11-08 16:34:21 +06:00
return err
2018-09-07 20:46:16 +06:00
}
}
2021-04-27 13:23:22 +03:00
if po.Pixelate > 1 {
pixels := minInt(po.Pixelate, minInt(img.Width(), img.Height()))
if err = img.Pixelate(pixels); err != nil {
return err
}
}
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
return err
}
2021-03-29 17:35:16 +06:00
if po.Extend.Enabled && (resultWidth > img.Width() || resultHeight > img.Height()) {
offX, offY := calcPosition(resultWidth, resultHeight, img.Width(), img.Height(), &po.Extend.Gravity, false)
if err = img.Embed(resultWidth, resultHeight, offX, offY, po.Background, transparentBg); err != nil {
2019-06-17 16:13:34 +06:00
return err
}
}
if po.Padding.Enabled {
paddingTop := scaleInt(po.Padding.Top, po.Dpr)
paddingRight := scaleInt(po.Padding.Right, po.Dpr)
paddingBottom := scaleInt(po.Padding.Bottom, po.Dpr)
paddingLeft := scaleInt(po.Padding.Left, po.Dpr)
if err = img.Embed(
img.Width()+paddingLeft+paddingRight,
img.Height()+paddingTop+paddingBottom,
paddingLeft,
paddingTop,
po.Background,
2020-08-21 18:56:30 +06:00
transparentBg,
); err != nil {
return err
}
}
2019-08-28 17:08:53 +06:00
if po.Watermark.Enabled && watermark != nil {
if err = applyWatermark(img, watermark, &po.Watermark, 1); err != nil {
2018-11-08 16:34:21 +06:00
return err
}
}
2020-09-11 17:20:12 +06:00
if err = img.RgbColourspace(); err != nil {
return err
}
if err := img.CastUchar(); err != nil {
return err
}
2021-01-12 20:43:09 +06:00
if po.StripMetadata {
if err := img.Strip(); err != nil {
return err
}
}
2020-09-11 17:20:12 +06:00
return copyMemoryAndCheckTimeout(ctx, img)
2018-11-08 16:34:21 +06:00
}
func transformAnimated(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
2020-01-17 15:54:50 +06:00
if po.Trim.Enabled {
logWarning("Trim is not supported for animated images")
po.Trim.Enabled = false
}
imgWidth := img.Width()
2018-11-08 16:34:21 +06:00
frameHeight, err := img.GetInt("page-height")
2019-06-03 16:53:47 +06:00
if err != nil {
2018-11-08 16:34:21 +06:00
return err
}
2019-06-11 21:42:21 +06:00
framesCount := minInt(img.Height()/frameHeight, conf.MaxAnimationFrames)
2019-06-03 16:53:47 +06:00
// Double check dimensions because animated image has many frames
if err = checkDimensions(imgWidth, frameHeight*framesCount); err != nil {
2018-11-08 16:34:21 +06:00
return err
}
2019-06-03 16:53:47 +06:00
// Vips 8.8+ supports n-pages and doesn't load the whole animated image on header access
2021-02-22 19:30:55 +06:00
if nPages, _ := img.GetIntDefault("n-pages", 0); nPages > framesCount {
// Load only the needed frames
if err = img.Load(data, imgtype, 1, 1.0, framesCount); err != nil {
2019-06-03 16:53:47 +06:00
return err
}
}
2021-02-22 19:30:55 +06:00
delay, err := img.GetIntSliceDefault("delay", nil)
2018-11-08 16:34:21 +06:00
if err != nil {
return err
}
2021-02-22 19:30:55 +06:00
loop, err := img.GetIntDefault("loop", 0)
if err != nil {
return err
}
// Legacy fields
// TODO: remove this in major update
gifLoop, err := img.GetIntDefault("gif-loop", -1)
if err != nil {
return err
}
gifDelay, err := img.GetIntDefault("gif-delay", -1)
2018-11-08 16:34:21 +06:00
if err != nil {
return err
}
2019-08-28 17:08:53 +06:00
watermarkEnabled := po.Watermark.Enabled
po.Watermark.Enabled = false
defer func() { po.Watermark.Enabled = watermarkEnabled }()
frames := make([]*vipsImage, framesCount)
defer func() {
for _, frame := range frames {
if frame != nil {
frame.Clear()
2018-11-08 16:34:21 +06:00
}
}
}()
2018-11-08 16:34:21 +06:00
for i := 0; i < framesCount; i++ {
frame := new(vipsImage)
if err = img.Extract(frame, 0, i*frameHeight, imgWidth, frameHeight); err != nil {
return err
}
frames[i] = frame
2020-07-01 19:26:20 +06:00
if err = transformImage(ctx, frame, nil, po, imgtype); err != nil {
return err
}
2018-11-08 16:34:21 +06:00
if err = copyMemoryAndCheckTimeout(ctx, frame); err != nil {
return err
}
2018-11-08 16:34:21 +06:00
}
if err = img.Arrayjoin(frames); err != nil {
2018-11-08 16:34:21 +06:00
return err
}
2019-08-28 17:08:53 +06:00
if watermarkEnabled && watermark != nil {
if err = applyWatermark(img, watermark, &po.Watermark, framesCount); err != nil {
return err
}
}
2020-09-11 17:20:12 +06:00
if err = img.CastUchar(); err != nil {
return err
}
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
return err
}
2021-02-22 19:30:55 +06:00
if len(delay) == 0 {
delay = make([]int, framesCount)
for i := range delay {
delay[i] = 40
}
} else if len(delay) > framesCount {
delay = delay[:framesCount]
}
img.SetInt("page-height", frames[0].Height())
2021-02-22 19:30:55 +06:00
img.SetIntSlice("delay", delay)
img.SetInt("loop", loop)
img.SetInt("n-pages", framesCount)
2018-11-08 16:34:21 +06:00
2021-02-22 19:30:55 +06:00
// Legacy fields
// TODO: remove this in major update
if gifLoop >= 0 {
img.SetInt("gif-loop", gifLoop)
}
if gifDelay >= 0 {
img.SetInt("gif-delay", gifDelay)
}
2018-11-08 16:34:21 +06:00
return nil
}
2019-10-03 21:43:33 +06:00
func getIcoData(imgdata *imageData) (*imageData, error) {
2019-12-25 15:06:15 +06:00
icoMeta, err := imagemeta.DecodeIcoMeta(bytes.NewReader(imgdata.Data))
2019-10-03 21:43:33 +06:00
if err != nil {
return nil, err
}
2019-12-25 15:06:15 +06:00
offset := icoMeta.BestImageOffset()
size := icoMeta.BestImageSize()
2019-10-03 21:43:33 +06:00
data := imgdata.Data[offset : offset+size]
2020-02-14 19:32:05 +06:00
var format string
2019-12-25 15:06:15 +06:00
meta, err := imagemeta.DecodeMeta(bytes.NewReader(data))
2019-10-03 21:43:33 +06:00
if err != nil {
2020-02-14 19:32:05 +06:00
// Looks like it's BMP with an incomplete header
if d, err := imagemeta.FixBmpHeader(data); err == nil {
format = "bmp"
data = d
} else {
return nil, err
}
} else {
format = meta.Format()
2019-10-03 21:43:33 +06:00
}
2020-02-14 19:32:05 +06:00
if imgtype, ok := imageTypes[format]; ok && vipsTypeSupportLoad[imgtype] {
2019-10-03 21:43:33 +06:00
return &imageData{
Data: data,
Type: imgtype,
}, nil
}
2019-12-25 15:06:15 +06:00
return nil, fmt.Errorf("Can't load %s from ICO", meta.Format())
2019-10-03 21:43:33 +06:00
}
func saveImageToFitBytes(ctx context.Context, po *processingOptions, img *vipsImage) ([]byte, context.CancelFunc, error) {
2019-11-29 18:40:01 +06:00
var diff float64
2021-01-13 17:58:36 +06:00
quality := po.getQuality()
2019-11-29 18:40:01 +06:00
for {
2021-01-12 20:43:09 +06:00
result, cancel, err := img.Save(po.Format, quality)
2019-11-29 18:40:01 +06:00
if len(result) <= po.MaxBytes || quality <= 10 || err != nil {
return result, cancel, err
}
cancel()
checkTimeout(ctx)
2019-11-29 18:40:01 +06:00
delta := float64(len(result)) / float64(po.MaxBytes)
switch {
case delta > 3:
diff = 0.25
case delta > 1.5:
diff = 0.5
default:
diff = 0.75
}
quality = int(float64(quality) * diff)
}
}
func processImage(ctx context.Context) ([]byte, context.CancelFunc, error) {
2019-01-09 18:41:00 +06:00
runtime.LockOSThread()
defer runtime.UnlockOSThread()
2021-03-23 12:37:25 +03:00
defer startDataDogSpan(ctx, "processing_image")()
2021-03-23 12:36:08 +03:00
defer startNewRelicSegment(ctx, "Processing image")()
defer startPrometheusDuration(prometheusProcessingDuration)()
2018-11-08 16:34:21 +06:00
defer vipsCleanup()
2018-11-08 16:34:21 +06:00
po := getProcessingOptions(ctx)
imgdata := getImageData(ctx)
2018-11-08 16:34:21 +06:00
switch {
case po.Format == imageTypeUnknown:
switch {
case po.PreferAvif && canSwitchFormat(imgdata.Type, imageTypeUnknown, imageTypeAVIF):
po.Format = imageTypeAVIF
case po.PreferWebP && canSwitchFormat(imgdata.Type, imageTypeUnknown, imageTypeWEBP):
po.Format = imageTypeWEBP
2019-10-01 18:12:14 +06:00
case imageTypeSaveSupport(imgdata.Type) && imageTypeGoodForWeb(imgdata.Type):
po.Format = imgdata.Type
default:
po.Format = imageTypeJPEG
}
case po.EnforceAvif && canSwitchFormat(imgdata.Type, po.Format, imageTypeAVIF):
po.Format = imageTypeAVIF
case po.EnforceWebP && canSwitchFormat(imgdata.Type, po.Format, imageTypeWEBP):
po.Format = imageTypeWEBP
}
2019-10-01 18:08:30 +06:00
if po.Format == imageTypeSVG {
if imgdata.Type != imageTypeSVG {
return []byte{}, func() {}, errConvertingNonSvgToSvg
}
return imgdata.Data, func() {}, nil
}
2019-10-01 18:21:31 +06:00
if imgdata.Type == imageTypeSVG && !vipsTypeSupportLoad[imageTypeSVG] {
return []byte{}, func() {}, errSourceImageTypeNotSupported
}
2019-10-03 21:43:33 +06:00
if imgdata.Type == imageTypeICO {
icodata, err := getIcoData(imgdata)
if err != nil {
return nil, func() {}, err
}
imgdata = icodata
}
animationSupport := conf.MaxAnimationFrames > 1 && vipsSupportAnimation(imgdata.Type) && vipsSupportAnimation(po.Format)
2019-06-03 16:53:47 +06:00
pages := 1
if animationSupport {
pages = -1
}
img := new(vipsImage)
defer img.Clear()
if err := img.Load(imgdata.Data, imgdata.Type, 1, 1.0, pages); err != nil {
return nil, func() {}, err
2018-11-08 16:34:21 +06:00
}
if animationSupport && img.IsAnimated() {
if err := transformAnimated(ctx, img, imgdata.Data, po, imgdata.Type); err != nil {
return nil, func() {}, err
2018-11-08 16:34:21 +06:00
}
} else {
if err := transformImage(ctx, img, imgdata.Data, po, imgdata.Type); err != nil {
return nil, func() {}, err
2018-10-19 15:47:11 +06:00
}
}
if err := copyMemoryAndCheckTimeout(ctx, img); err != nil {
return nil, func() {}, err
}
2018-11-08 16:34:21 +06:00
if po.MaxBytes > 0 && canFitToBytes(po.Format) {
return saveImageToFitBytes(ctx, po, img)
}
2021-01-13 17:58:36 +06:00
return img.Save(po.Format, po.getQuality())
2017-06-20 16:58:55 +03:00
}