mirror of
https://github.com/imgproxy/imgproxy.git
synced 2024-11-24 08:12:38 +02:00
874 lines
20 KiB
Go
874 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"runtime"
|
|
|
|
"github.com/imgproxy/imgproxy/v2/imagemeta"
|
|
)
|
|
|
|
const (
|
|
msgSmartCropNotSupported = "Smart crop is not supported by used version of libvips"
|
|
|
|
// https://chromium.googlesource.com/webm/libwebp/+/refs/heads/master/src/webp/encode.h#529
|
|
webpMaxDimension = 16383.0
|
|
)
|
|
|
|
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 {
|
|
return imgtype == imageTypeSVG ||
|
|
imgtype == imageTypeICO ||
|
|
vipsTypeSupportLoad[imgtype]
|
|
}
|
|
|
|
func imageTypeSaveSupport(imgtype imageType) bool {
|
|
return imgtype == imageTypeSVG || vipsTypeSupportSave[imgtype]
|
|
}
|
|
|
|
func imageTypeGoodForWeb(imgtype imageType) bool {
|
|
return imgtype != imageTypeTIFF &&
|
|
imgtype != imageTypeBMP
|
|
}
|
|
|
|
func canSwitchFormat(src, dst, want imageType) bool {
|
|
return imageTypeSaveSupport(want) &&
|
|
(!vipsSupportAnimation(src) ||
|
|
(dst != imageTypeUnknown && !vipsSupportAnimation(dst)) ||
|
|
vipsSupportAnimation(want))
|
|
}
|
|
|
|
func extractMeta(img *vipsImage, baseAngle int, useOrientation bool) (int, int, int, bool) {
|
|
width := img.Width()
|
|
height := img.Height()
|
|
|
|
angle := 0
|
|
flip := false
|
|
|
|
if useOrientation {
|
|
orientation := img.Orientation()
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
if (angle+baseAngle)%180 != 0 {
|
|
width, height = height, width
|
|
}
|
|
|
|
return width, height, angle, flip
|
|
}
|
|
|
|
func calcScale(width, height int, po *processingOptions, imgtype imageType) float64 {
|
|
var shrink float64
|
|
|
|
srcW, srcH := float64(width), float64(height)
|
|
dstW, dstH := float64(po.Width), float64(po.Height)
|
|
|
|
if po.Width == 0 {
|
|
dstW = srcW
|
|
}
|
|
|
|
if po.Height == 0 {
|
|
dstH = srcH
|
|
}
|
|
|
|
if dstW == srcW && dstH == srcH {
|
|
shrink = 1
|
|
} else {
|
|
wshrink := srcW / dstW
|
|
hshrink := srcH / dstH
|
|
|
|
rt := po.ResizingType
|
|
|
|
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:
|
|
shrink = hshrink
|
|
case po.Height == 0:
|
|
shrink = wshrink
|
|
case rt == resizeFit:
|
|
shrink = math.Max(wshrink, hshrink)
|
|
default:
|
|
shrink = math.Min(wshrink, hshrink)
|
|
}
|
|
}
|
|
|
|
if !po.Enlarge && shrink < 1 && imgtype != imageTypeSVG {
|
|
shrink = 1
|
|
}
|
|
|
|
shrink /= po.Dpr
|
|
|
|
if shrink > srcW {
|
|
shrink = srcW
|
|
}
|
|
|
|
if shrink > srcH {
|
|
shrink = srcH
|
|
}
|
|
|
|
return 1.0 / shrink
|
|
}
|
|
|
|
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 {
|
|
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
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
func calcPosition(width, height, innerWidth, innerHeight int, gravity *gravityOptions, allowOverflow bool) (left, top int) {
|
|
if gravity.Type == gravityFocusPoint {
|
|
pointX := scaleInt(width, gravity.X)
|
|
pointY := scaleInt(height, gravity.Y)
|
|
|
|
left = pointX - innerWidth/2
|
|
top = pointY - innerHeight/2
|
|
} else {
|
|
offX, offY := int(gravity.X), int(gravity.Y)
|
|
|
|
left = (width-innerWidth+1)/2 + offX
|
|
top = (height-innerHeight+1)/2 + offY
|
|
|
|
if gravity.Type == gravityNorth || gravity.Type == gravityNorthEast || gravity.Type == gravityNorthWest {
|
|
top = 0 + offY
|
|
}
|
|
|
|
if gravity.Type == gravityEast || gravity.Type == gravityNorthEast || gravity.Type == gravitySouthEast {
|
|
left = width - innerWidth - offX
|
|
}
|
|
|
|
if gravity.Type == gravitySouth || gravity.Type == gravitySouthEast || gravity.Type == gravitySouthWest {
|
|
top = height - innerHeight - offY
|
|
}
|
|
|
|
if gravity.Type == gravityWest || gravity.Type == gravityNorthWest || gravity.Type == gravitySouthWest {
|
|
left = 0 + offX
|
|
}
|
|
}
|
|
|
|
var minX, maxX, minY, maxY int
|
|
|
|
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
|
|
}
|
|
|
|
left = maxInt(minX, minInt(left, maxX))
|
|
top = maxInt(minY, minInt(top, maxY))
|
|
|
|
return
|
|
}
|
|
|
|
func cropImage(img *vipsImage, cropWidth, cropHeight int, gravity *gravityOptions) error {
|
|
if cropWidth == 0 && cropHeight == 0 {
|
|
return nil
|
|
}
|
|
|
|
imgWidth, imgHeight := img.Width(), img.Height()
|
|
|
|
cropWidth = minNonZeroInt(cropWidth, imgWidth)
|
|
cropHeight = minNonZeroInt(cropHeight, imgHeight)
|
|
|
|
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
|
|
}
|
|
// Applying additional modifications after smart crop causes SIGSEGV on Alpine
|
|
// so we have to copy memory after it
|
|
return img.CopyMemory()
|
|
}
|
|
|
|
left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, false)
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
po := newProcessingOptions()
|
|
po.ResizingType = resizeFit
|
|
po.Dpr = 1
|
|
po.Enlarge = true
|
|
po.Format = wmData.Type
|
|
|
|
if opts.Scale > 0 {
|
|
po.Width = maxInt(scaleInt(imgWidth, opts.Scale), 1)
|
|
po.Height = maxInt(scaleInt(imgHeight, opts.Scale), 1)
|
|
}
|
|
|
|
if err := transformImage(context.Background(), wm, wmData.Data, po, wmData.Type); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := wm.EnsureAlpha(); err != nil {
|
|
return nil
|
|
}
|
|
|
|
if opts.Replicate {
|
|
return wm.Replicate(imgWidth, imgHeight)
|
|
}
|
|
|
|
left, top := calcPosition(imgWidth, imgHeight, wm.Width(), wm.Height(), &opts.Gravity, true)
|
|
|
|
return wm.Embed(imgWidth, imgHeight, left, top, rgbColor{0, 0, 0}, true)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 {
|
|
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 {
|
|
return err
|
|
}
|
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return err
|
|
}
|
|
trimmed = true
|
|
}
|
|
|
|
srcWidth, srcHeight, angle, flip := extractMeta(img, po.Rotate, po.AutoRotate)
|
|
|
|
cropWidth := calcCropSize(srcWidth, po.Crop.Width)
|
|
cropHeight := calcCropSize(srcHeight, po.Crop.Height)
|
|
|
|
cropGravity := po.Crop.Gravity
|
|
if cropGravity.Type == gravityUnknown {
|
|
cropGravity = po.Gravity
|
|
}
|
|
|
|
widthToScale := minNonZeroInt(cropWidth, srcWidth)
|
|
heightToScale := minNonZeroInt(cropHeight, srcHeight)
|
|
|
|
scale := calcScale(widthToScale, heightToScale, po, imgtype)
|
|
|
|
if cropWidth > 0 {
|
|
cropWidth = maxInt(1, scaleInt(cropWidth, scale))
|
|
}
|
|
if cropHeight > 0 {
|
|
cropHeight = maxInt(1, scaleInt(cropHeight, scale))
|
|
}
|
|
if cropGravity.Type != gravityFocusPoint {
|
|
cropGravity.X *= scale
|
|
cropGravity.Y *= scale
|
|
}
|
|
|
|
if !trimmed && scale != 1 && data != nil && canScaleOnLoad(imgtype, scale) {
|
|
jpegShrink := calcJpegShink(scale, imgtype)
|
|
|
|
if imgtype != imageTypeJPEG || jpegShrink != 1 {
|
|
// Do some scale-on-load
|
|
if err = img.Load(data, imgtype, jpegShrink, scale, 1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update scale after scale-on-load
|
|
newWidth, newHeight, _, _ := extractMeta(img, po.Rotate, po.AutoRotate)
|
|
if srcWidth > srcHeight {
|
|
scale = float64(srcWidth) * scale / float64(newWidth)
|
|
} else {
|
|
scale = float64(srcHeight) * scale / float64(newHeight)
|
|
}
|
|
if srcWidth == scaleInt(srcWidth, scale) && srcHeight == scaleInt(srcHeight, scale) {
|
|
scale = 1.0
|
|
}
|
|
}
|
|
|
|
if err = img.Rad2Float(); err != nil {
|
|
return err
|
|
}
|
|
|
|
iccImported := false
|
|
convertToLinear := conf.UseLinearColorspace && scale != 1
|
|
|
|
if convertToLinear || !img.IsSRGB() {
|
|
if err = img.ImportColourProfile(); err != nil {
|
|
return err
|
|
}
|
|
iccImported = true
|
|
}
|
|
|
|
if convertToLinear {
|
|
if err = img.LinearColourspace(); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err = img.RgbColourspace(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
hasAlpha := img.HasAlpha()
|
|
|
|
if scale != 1 {
|
|
if err = img.Resize(scale, hasAlpha); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = img.Rotate(angle); err != nil {
|
|
return err
|
|
}
|
|
|
|
if flip {
|
|
if err = img.Flip(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = img.Rotate(po.Rotate); err != nil {
|
|
return err
|
|
}
|
|
|
|
dprWidth := scaleInt(po.Width, po.Dpr)
|
|
dprHeight := scaleInt(po.Height, po.Dpr)
|
|
|
|
if err = cropImage(img, cropWidth, cropHeight, &cropGravity); err != nil {
|
|
return err
|
|
}
|
|
if err = cropImage(img, dprWidth, dprHeight, &po.Gravity); err != nil {
|
|
return err
|
|
}
|
|
|
|
if po.Format == imageTypeWEBP {
|
|
webpLimitShrink := float64(maxInt(img.Width(), img.Height())) / webpMaxDimension
|
|
|
|
if webpLimitShrink > 1.0 {
|
|
if err = img.Resize(1.0/webpLimitShrink, hasAlpha); err != nil {
|
|
return err
|
|
}
|
|
logWarning("WebP dimension size is limited to %d. The image is rescaled to %dx%d", int(webpMaxDimension), img.Width(), img.Height())
|
|
|
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
keepProfile := !po.StripColorProfile && po.Format.SupportsColourProfile()
|
|
|
|
if iccImported {
|
|
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
|
|
}
|
|
}
|
|
} 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
|
|
if err = img.TransformColourProfile(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = img.RgbColourspace(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !keepProfile {
|
|
if err = img.RemoveColourProfile(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
transparentBg := po.Format.SupportsAlpha() && !po.Flatten
|
|
|
|
if hasAlpha && !transparentBg {
|
|
if err = img.Flatten(po.Background); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return err
|
|
}
|
|
|
|
if po.Blur > 0 {
|
|
if err = img.Blur(po.Blur); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if po.Sharpen > 0 {
|
|
if err = img.Sharpen(po.Sharpen); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return err
|
|
}
|
|
|
|
if po.Extend.Enabled && (dprWidth > img.Width() || dprHeight > img.Height()) {
|
|
offX, offY := calcPosition(dprWidth, dprHeight, img.Width(), img.Height(), &po.Extend.Gravity, false)
|
|
if err = img.Embed(dprWidth, dprHeight, offX, offY, po.Background, transparentBg); err != nil {
|
|
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,
|
|
transparentBg,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if po.Watermark.Enabled && watermark != nil {
|
|
if err = applyWatermark(img, watermark, &po.Watermark, 1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = img.RgbColourspace(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := img.CastUchar(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if po.StripMetadata {
|
|
if err := img.Strip(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return copyMemoryAndCheckTimeout(ctx, img)
|
|
}
|
|
|
|
func transformAnimated(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error {
|
|
if po.Trim.Enabled {
|
|
logWarning("Trim is not supported for animated images")
|
|
po.Trim.Enabled = false
|
|
}
|
|
|
|
imgWidth := img.Width()
|
|
|
|
frameHeight, err := img.GetInt("page-height")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
framesCount := minInt(img.Height()/frameHeight, conf.MaxAnimationFrames)
|
|
|
|
// Double check dimensions because animated image has many frames
|
|
if err = checkDimensions(imgWidth, frameHeight*framesCount); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Vips 8.8+ supports n-pages and doesn't load the whole animated image on header access
|
|
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 {
|
|
return err
|
|
}
|
|
}
|
|
|
|
delay, err := img.GetIntSliceDefault("delay", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}()
|
|
|
|
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
|
|
|
|
if err = transformImage(ctx, frame, nil, po, imgtype); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = copyMemoryAndCheckTimeout(ctx, frame); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = img.Arrayjoin(frames); err != nil {
|
|
return err
|
|
}
|
|
|
|
if watermarkEnabled && watermark != nil {
|
|
if err = applyWatermark(img, watermark, &po.Watermark, framesCount); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = img.CastUchar(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return err
|
|
}
|
|
|
|
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())
|
|
img.SetIntSlice("delay", delay)
|
|
img.SetInt("loop", loop)
|
|
img.SetInt("n-pages", framesCount)
|
|
|
|
// Legacy fields
|
|
// TODO: remove this in major update
|
|
if gifLoop >= 0 {
|
|
img.SetInt("gif-loop", gifLoop)
|
|
}
|
|
if gifDelay >= 0 {
|
|
img.SetInt("gif-delay", gifDelay)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getIcoData(imgdata *imageData) (*imageData, error) {
|
|
icoMeta, err := imagemeta.DecodeIcoMeta(bytes.NewReader(imgdata.Data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
offset := icoMeta.BestImageOffset()
|
|
size := icoMeta.BestImageSize()
|
|
|
|
data := imgdata.Data[offset : offset+size]
|
|
|
|
var format string
|
|
|
|
meta, err := imagemeta.DecodeMeta(bytes.NewReader(data))
|
|
if err != nil {
|
|
// 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()
|
|
}
|
|
|
|
if imgtype, ok := imageTypes[format]; ok && vipsTypeSupportLoad[imgtype] {
|
|
return &imageData{
|
|
Data: data,
|
|
Type: imgtype,
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("Can't load %s from ICO", meta.Format())
|
|
}
|
|
|
|
func saveImageToFitBytes(ctx context.Context, po *processingOptions, img *vipsImage) ([]byte, context.CancelFunc, error) {
|
|
var diff float64
|
|
quality := po.getQuality()
|
|
|
|
for {
|
|
result, cancel, err := img.Save(po.Format, quality)
|
|
if len(result) <= po.MaxBytes || quality <= 10 || err != nil {
|
|
return result, cancel, err
|
|
}
|
|
cancel()
|
|
|
|
checkTimeout(ctx)
|
|
|
|
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) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
if newRelicEnabled {
|
|
newRelicCancel := startNewRelicSegment(ctx, "Processing image")
|
|
defer newRelicCancel()
|
|
}
|
|
|
|
if prometheusEnabled {
|
|
defer startPrometheusDuration(prometheusProcessingDuration)()
|
|
}
|
|
|
|
defer vipsCleanup()
|
|
|
|
po := getProcessingOptions(ctx)
|
|
imgdata := getImageData(ctx)
|
|
|
|
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
|
|
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
|
|
}
|
|
|
|
if po.Format == imageTypeSVG {
|
|
if imgdata.Type != imageTypeSVG {
|
|
return []byte{}, func() {}, errConvertingNonSvgToSvg
|
|
}
|
|
|
|
return imgdata.Data, func() {}, nil
|
|
}
|
|
|
|
if imgdata.Type == imageTypeSVG && !vipsTypeSupportLoad[imageTypeSVG] {
|
|
return []byte{}, func() {}, errSourceImageTypeNotSupported
|
|
}
|
|
|
|
if imgdata.Type == imageTypeICO {
|
|
icodata, err := getIcoData(imgdata)
|
|
if err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
|
|
imgdata = icodata
|
|
}
|
|
|
|
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.ResizingType == 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 = float64(po.Width), float64(po.Height)
|
|
|
|
po.ResizingType = resizeFit
|
|
po.Width, po.Height = 0, 0
|
|
}
|
|
|
|
animationSupport := conf.MaxAnimationFrames > 1 && vipsSupportAnimation(imgdata.Type) && vipsSupportAnimation(po.Format)
|
|
|
|
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
|
|
}
|
|
|
|
if animationSupport && img.IsAnimated() {
|
|
if err := transformAnimated(ctx, img, imgdata.Data, po, imgdata.Type); err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
} else {
|
|
if err := transformImage(ctx, img, imgdata.Data, po, imgdata.Type); err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
}
|
|
|
|
if err := copyMemoryAndCheckTimeout(ctx, img); err != nil {
|
|
return nil, func() {}, err
|
|
}
|
|
|
|
if po.MaxBytes > 0 && canFitToBytes(po.Format) {
|
|
return saveImageToFitBytes(ctx, po, img)
|
|
}
|
|
|
|
return img.Save(po.Format, po.getQuality())
|
|
}
|