Browse Source

Calculate all sizes at the start of processing pipeline

DarthSim 3 weeks ago
parent
commit
810cad5d26
7 changed files with 686 additions and 87 deletions
  1. 1 19
      processing/crop.go
  2. 24 38
      processing/extend.go
  3. 28 0
      processing/pipeline.go
  4. 68 17
      processing/prepare.go
  5. 565 0
      processing/processing_test.go
  6. 0 13
      processing/result_size.go
  7. BIN
      testdata/test2.jpg

+ 1 - 19
processing/crop.go

@@ -48,23 +48,5 @@ func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions,
 }
 
 func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
-	// Crop image to the result size
-	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
-
-	if po.ResizingType == options.ResizeFillDown {
-		diffW := float64(resultWidth) / float64(img.Width())
-		diffH := float64(resultHeight) / float64(img.Height())
-
-		switch {
-		case diffW > diffH && diffW > 1.0:
-			resultHeight = imath.Scale(img.Width(), float64(resultHeight)/float64(resultWidth))
-			resultWidth = img.Width()
-
-		case diffH > diffW && diffH > 1.0:
-			resultWidth = imath.Scale(img.Height(), float64(resultWidth)/float64(resultHeight))
-			resultHeight = img.Height()
-		}
-	}
-
-	return cropImage(img, resultWidth, resultHeight, &po.Gravity, pctx.dprScale)
+	return cropImage(img, pctx.resultCropWidth, pctx.resultCropHeight, &po.Gravity, pctx.dprScale)
 }

+ 24 - 38
processing/extend.go

@@ -2,61 +2,47 @@ package processing
 
 import (
 	"github.com/imgproxy/imgproxy/v3/imagedata"
-	"github.com/imgproxy/imgproxy/v3/imath"
 	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func extendImage(img *vips.Image, resultWidth, resultHeight int, opts *options.ExtendOptions, offsetScale float64, extendAr bool) error {
+func extendImage(img *vips.Image, width, height int, gravity *options.GravityOptions, offsetScale float64) error {
 	imgWidth := img.Width()
 	imgHeight := img.Height()
 
-	if !opts.Enabled || (resultWidth <= imgWidth && resultHeight <= imgHeight) {
+	if width <= imgWidth && height <= imgHeight {
 		return nil
 	}
 
-	if resultWidth <= 0 {
-		if extendAr {
-			return nil
-		}
-		resultWidth = imgWidth
+	if width <= 0 {
+		width = imgWidth
 	}
-	if resultHeight <= 0 {
-		if extendAr {
-			return nil
-		}
-		resultHeight = imgHeight
+	if height <= 0 {
+		height = imgHeight
 	}
 
-	if extendAr && resultWidth > imgWidth && resultHeight > imgHeight {
-		diffW := float64(resultWidth) / float64(imgWidth)
-		diffH := float64(resultHeight) / float64(imgHeight)
-
-		switch {
-		case diffH > diffW:
-			resultHeight = imath.Scale(imgWidth, float64(resultHeight)/float64(resultWidth))
-			resultWidth = imgWidth
-
-		case diffW > diffH:
-			resultWidth = imath.Scale(imgHeight, float64(resultWidth)/float64(resultHeight))
-			resultHeight = imgHeight
-
-		default:
-			// The image has the requested arpect ratio
-			return nil
-		}
-	}
-
-	offX, offY := calcPosition(resultWidth, resultHeight, imgWidth, imgHeight, &opts.Gravity, offsetScale, false)
-	return img.Embed(resultWidth, resultHeight, offX, offY)
+	offX, offY := calcPosition(width, height, imgWidth, imgHeight, gravity, offsetScale, false)
+	return img.Embed(width, height, offX, offY)
 }
 
 func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
-	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
-	return extendImage(img, resultWidth, resultHeight, &po.Extend, pctx.dprScale, false)
+	if !po.Extend.Enabled {
+		return nil
+	}
+
+	width, height := pctx.targetWidth, pctx.targetHeight
+	return extendImage(img, width, height, &po.Extend.Gravity, pctx.dprScale)
 }
 
 func extendAspectRatio(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
-	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
-	return extendImage(img, resultWidth, resultHeight, &po.ExtendAspectRatio, pctx.dprScale, true)
+	if !po.ExtendAspectRatio.Enabled {
+		return nil
+	}
+
+	width, height := pctx.extendAspectRatioWidth, pctx.extendAspectRatioHeight
+	if width == 0 || height == 0 {
+		return nil
+	}
+
+	return extendImage(img, width, height, &po.ExtendAspectRatio.Gravity, pctx.dprScale)
 }

+ 28 - 0
processing/pipeline.go

@@ -30,6 +30,34 @@ type pipelineContext struct {
 	hscale float64
 
 	dprScale float64
+
+	// The width we aim to get.
+	// Based on the requested width scaled according to processing options.
+	// Can be 0 if width is not specified in the processing options.
+	targetWidth int
+	// The height we aim to get.
+	// Based on the requested height scaled according to processing options.
+	// Can be 0 if height is not specified in the processing options.
+	targetHeight int
+
+	// The width of the image after cropping, scaling and rotating
+	scaledWidth int
+	// The height of the image after cropping, scaling and rotating
+	scaledHeight int
+
+	// The width of the result crop according to the resizing type
+	resultCropWidth int
+	// The height of the result crop according to the resizing type
+	resultCropHeight int
+
+	// The width of the image extended to the requested aspect ratio.
+	// Can be 0 if any of the dimensions is not specified in the processing options
+	// or if the image already has the requested aspect ratio.
+	extendAspectRatioWidth int
+	// The width of the image extended to the requested aspect ratio.
+	// Can be 0 if any of the dimensions is not specified in the processing options
+	// or if the image already has the requested aspect ratio.
+	extendAspectRatioHeight int
 }
 
 type pipelineStep func(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error

+ 68 - 17
processing/prepare.go

@@ -41,7 +41,18 @@ func extractMeta(img *vips.Image, baseAngle int, useOrientation bool) (int, int,
 	return width, height, angle, flip
 }
 
-func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagetype.Type) (float64, float64, float64) {
+func calcCropSize(orig int, crop float64) int {
+	switch {
+	case crop == 0.0:
+		return 0
+	case crop >= 1.0:
+		return int(crop)
+	default:
+		return imath.Max(1, imath.Scale(orig, crop))
+	}
+}
+
+func (pctx *pipelineContext) calcScale(width, height int, po *options.ProcessingOptions) {
 	var wshrink, hshrink float64
 
 	srcW, srcH := float64(width), float64(height)
@@ -98,22 +109,22 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 	wshrink /= po.ZoomWidth
 	hshrink /= po.ZoomHeight
 
-	dprScale := po.Dpr
+	pctx.dprScale = po.Dpr
 
-	if !po.Enlarge && imgtype != imagetype.SVG {
+	if !po.Enlarge && !pctx.imgtype.IsVector() {
 		minShrink := math.Min(wshrink, hshrink)
 		if minShrink < 1 {
 			wshrink /= minShrink
 			hshrink /= minShrink
 
 			if !po.Extend.Enabled {
-				dprScale /= minShrink
+				pctx.dprScale /= minShrink
 			}
 		}
 
 		// The minimum of wshrink and hshrink is the maximum dprScale value
 		// that can be used without enlarging the image.
-		dprScale = math.Min(dprScale, math.Min(wshrink, hshrink))
+		pctx.dprScale = math.Min(pctx.dprScale, math.Min(wshrink, hshrink))
 	}
 
 	if po.MinWidth > 0 {
@@ -130,8 +141,8 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 		}
 	}
 
-	wshrink /= dprScale
-	hshrink /= dprScale
+	wshrink /= pctx.dprScale
+	hshrink /= pctx.dprScale
 
 	if wshrink > srcW {
 		wshrink = srcW
@@ -141,17 +152,55 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 		hshrink = srcH
 	}
 
-	return 1.0 / wshrink, 1.0 / hshrink, dprScale
+	pctx.wscale = 1.0 / wshrink
+	pctx.hscale = 1.0 / hshrink
 }
 
-func calcCropSize(orig int, crop float64) int {
-	switch {
-	case crop == 0.0:
-		return 0
-	case crop >= 1.0:
-		return int(crop)
-	default:
-		return imath.Max(1, imath.Scale(orig, crop))
+func (pctx *pipelineContext) calcSizes(widthToScale, heightToScale int, po *options.ProcessingOptions) {
+	pctx.targetWidth = imath.Scale(po.Width, pctx.dprScale*po.ZoomWidth)
+	pctx.targetHeight = imath.Scale(po.Height, pctx.dprScale*po.ZoomHeight)
+
+	pctx.scaledWidth = imath.Scale(widthToScale, pctx.wscale)
+	pctx.scaledHeight = imath.Scale(heightToScale, pctx.hscale)
+
+	if po.ResizingType == options.ResizeFillDown && !po.Enlarge {
+		diffW := float64(pctx.targetWidth) / float64(pctx.scaledWidth)
+		diffH := float64(pctx.targetHeight) / float64(pctx.scaledHeight)
+
+		switch {
+		case diffW > diffH && diffW > 1.0:
+			pctx.resultCropHeight = imath.Scale(pctx.scaledWidth, float64(pctx.targetHeight)/float64(pctx.targetWidth))
+			pctx.resultCropWidth = pctx.scaledWidth
+
+		case diffH > diffW && diffH > 1.0:
+			pctx.resultCropWidth = imath.Scale(pctx.scaledHeight, float64(pctx.targetWidth)/float64(pctx.targetHeight))
+			pctx.resultCropHeight = pctx.scaledHeight
+
+		default:
+			pctx.resultCropWidth = pctx.targetWidth
+			pctx.resultCropHeight = pctx.targetHeight
+		}
+	} else {
+		pctx.resultCropWidth = pctx.targetWidth
+		pctx.resultCropHeight = pctx.targetHeight
+	}
+
+	if po.ExtendAspectRatio.Enabled && pctx.targetWidth > 0 && pctx.targetHeight > 0 {
+		outWidth := imath.MinNonZero(pctx.scaledWidth, pctx.resultCropWidth)
+		outHeight := imath.MinNonZero(pctx.scaledHeight, pctx.resultCropHeight)
+
+		diffW := float64(pctx.targetWidth) / float64(outWidth)
+		diffH := float64(pctx.targetHeight) / float64(outHeight)
+
+		switch {
+		case diffH > diffW:
+			pctx.extendAspectRatioHeight = imath.Scale(outWidth, float64(pctx.targetHeight)/float64(pctx.targetWidth))
+			pctx.extendAspectRatioWidth = outWidth
+
+		case diffW > diffH:
+			pctx.extendAspectRatioWidth = imath.Scale(outHeight, float64(pctx.targetWidth)/float64(pctx.targetHeight))
+			pctx.extendAspectRatioHeight = outHeight
+		}
 	}
 }
 
@@ -169,7 +218,7 @@ func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio
 	widthToScale := imath.MinNonZero(pctx.cropWidth, pctx.srcWidth)
 	heightToScale := imath.MinNonZero(pctx.cropHeight, pctx.srcHeight)
 
-	pctx.wscale, pctx.hscale, pctx.dprScale = calcScale(widthToScale, heightToScale, po, pctx.imgtype)
+	pctx.calcScale(widthToScale, heightToScale, po)
 
 	// The size of a vector image is not checked during download, yet it can be very large.
 	// So we should scale it down to the maximum allowed resolution
@@ -182,5 +231,7 @@ func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio
 		}
 	}
 
+	pctx.calcSizes(widthToScale, heightToScale, po)
+
 	return nil
 }

+ 565 - 0
processing/processing_test.go

@@ -0,0 +1,565 @@
+package processing
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/suite"
+
+	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/imagedata"
+	"github.com/imgproxy/imgproxy/v3/options"
+	"github.com/imgproxy/imgproxy/v3/security"
+	"github.com/imgproxy/imgproxy/v3/vips"
+)
+
+type ProcessingTestSuite struct {
+	suite.Suite
+}
+
+func (s *ProcessingTestSuite) SetupSuite() {
+	config.Reset()
+
+	s.Require().NoError(imagedata.Init())
+	s.Require().NoError(vips.Init())
+
+	logrus.SetOutput(io.Discard)
+}
+
+func (s *ProcessingTestSuite) openFile(name string) *imagedata.ImageData {
+	secopts := security.Options{
+		MaxSrcResolution:            10 * 1024 * 1024,
+		MaxSrcFileSize:              10 * 1024 * 1024,
+		MaxAnimationFrames:          100,
+		MaxAnimationFrameResolution: 10 * 1024 * 1024,
+	}
+
+	wd, err := os.Getwd()
+	s.Require().NoError(err)
+	path := filepath.Join(wd, "..", "testdata", name)
+
+	imagedata, err := imagedata.FromFile(path, "test image", secopts)
+	s.Require().NoError(err)
+
+	return imagedata
+}
+
+func (s *ProcessingTestSuite) checkSize(imgdata *imagedata.ImageData, width, height int) {
+	img := new(vips.Image)
+	err := img.Load(imgdata, 1, 1, 1)
+	s.Require().NoError(err)
+	defer img.Clear()
+
+	s.Require().Equal(width, img.Width(), "Width mismatch")
+	s.Require().Equal(height, img.Height(), "Height mismatch")
+}
+
+func (s *ProcessingTestSuite) TestResizeToFit() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFit
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 25},
+		{width: 50, height: 20, outWidth: 40, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 10},
+		{width: 300, height: 300, outWidth: 200, outHeight: 100},
+		{width: 300, height: 50, outWidth: 100, outHeight: 50},
+		{width: 100, height: 300, outWidth: 100, outHeight: 50},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 100},
+		{width: 300, height: 0, outWidth: 200, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFit
+	po.Enlarge = true
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 25},
+		{width: 50, height: 20, outWidth: 40, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 10},
+		{width: 300, height: 300, outWidth: 300, outHeight: 150},
+		{width: 300, height: 125, outWidth: 250, outHeight: 125},
+		{width: 250, height: 300, outWidth: 250, outHeight: 125},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 400, outHeight: 200},
+		{width: 300, height: 0, outWidth: 300, outHeight: 150},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFitExtend() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFit
+	po.Extend = options.ExtendOptions{
+		Enabled: true,
+		Gravity: options.GravityOptions{
+			Type: options.GravityCenter,
+		},
+	}
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 300, outHeight: 300},
+		{width: 300, height: 125, outWidth: 300, outHeight: 125},
+		{width: 250, height: 300, outWidth: 250, outHeight: 300},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 200},
+		{width: 300, height: 0, outWidth: 300, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFit
+	po.ExtendAspectRatio = options.ExtendOptions{
+		Enabled: true,
+		Gravity: options.GravityOptions{
+			Type: options.GravityCenter,
+		},
+	}
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 200, outHeight: 200},
+		{width: 300, height: 125, outWidth: 240, outHeight: 100},
+		{width: 250, height: 500, outWidth: 200, outHeight: 400},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 100},
+		{width: 300, height: 0, outWidth: 200, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFill() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFill
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 200, outHeight: 100},
+		{width: 300, height: 50, outWidth: 200, outHeight: 50},
+		{width: 100, height: 300, outWidth: 100, outHeight: 100},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 100},
+		{width: 300, height: 0, outWidth: 200, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFill
+	po.Enlarge = true
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 300, outHeight: 300},
+		{width: 300, height: 125, outWidth: 300, outHeight: 125},
+		{width: 250, height: 300, outWidth: 250, outHeight: 300},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 400, outHeight: 200},
+		{width: 300, height: 0, outWidth: 300, outHeight: 150},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillExtend() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFill
+	po.Extend = options.ExtendOptions{
+		Enabled: true,
+		Gravity: options.GravityOptions{
+			Type: options.GravityCenter,
+		},
+	}
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 300, outHeight: 300},
+		{width: 300, height: 125, outWidth: 300, outHeight: 125},
+		{width: 250, height: 300, outWidth: 250, outHeight: 300},
+		{width: 300, height: 50, outWidth: 300, outHeight: 50},
+		{width: 100, height: 300, outWidth: 100, outHeight: 300},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 200},
+		{width: 300, height: 0, outWidth: 300, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFill
+	po.ExtendAspectRatio = options.ExtendOptions{
+		Enabled: true,
+		Gravity: options.GravityOptions{
+			Type: options.GravityCenter,
+		},
+	}
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 200, outHeight: 200},
+		{width: 300, height: 125, outWidth: 240, outHeight: 100},
+		{width: 250, height: 500, outWidth: 200, outHeight: 400},
+		{width: 300, height: 50, outWidth: 300, outHeight: 50},
+		{width: 100, height: 300, outWidth: 100, outHeight: 300},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 100},
+		{width: 300, height: 0, outWidth: 200, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillDown() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFillDown
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 100, outHeight: 100},
+		{width: 300, height: 125, outWidth: 200, outHeight: 83},
+		{width: 250, height: 300, outWidth: 83, outHeight: 100},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 100},
+		{width: 300, height: 0, outWidth: 200, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFillDown
+	po.Enlarge = true
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 300, outHeight: 300},
+		{width: 300, height: 125, outWidth: 300, outHeight: 125},
+		{width: 250, height: 300, outWidth: 250, outHeight: 300},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 400, outHeight: 200},
+		{width: 300, height: 0, outWidth: 300, outHeight: 150},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFillDown
+	po.Extend = options.ExtendOptions{
+		Enabled: true,
+		Gravity: options.GravityOptions{
+			Type: options.GravityCenter,
+		},
+	}
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 300, outHeight: 300},
+		{width: 300, height: 125, outWidth: 300, outHeight: 125},
+		{width: 250, height: 300, outWidth: 250, outHeight: 300},
+		{width: 300, height: 50, outWidth: 300, outHeight: 50},
+		{width: 100, height: 300, outWidth: 100, outHeight: 300},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 200},
+		{width: 300, height: 0, outWidth: 300, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+	po.ResizingType = options.ResizeFillDown
+	po.ExtendAspectRatio = options.ExtendOptions{
+		Enabled: true,
+		Gravity: options.GravityOptions{
+			Type: options.GravityCenter,
+		},
+	}
+
+	testCases := []struct {
+		width     int
+		height    int
+		outWidth  int
+		outHeight int
+	}{
+		{width: 50, height: 50, outWidth: 50, outHeight: 50},
+		{width: 50, height: 20, outWidth: 50, outHeight: 20},
+		{width: 20, height: 50, outWidth: 20, outHeight: 50},
+		{width: 300, height: 300, outWidth: 100, outHeight: 100},
+		{width: 300, height: 125, outWidth: 200, outHeight: 83},
+		{width: 250, height: 300, outWidth: 83, outHeight: 100},
+		{width: 0, height: 50, outWidth: 100, outHeight: 50},
+		{width: 50, height: 0, outWidth: 50, outHeight: 25},
+		{width: 0, height: 200, outWidth: 200, outHeight: 100},
+		{width: 300, height: 0, outWidth: 200, outHeight: 100},
+	}
+
+	for _, tc := range testCases {
+		s.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func() {
+			po.Width = tc.width
+			po.Height = tc.height
+
+			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			s.Require().NoError(err)
+			s.Require().NotNil(outImgdata)
+
+			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+		})
+	}
+}
+
+func TestProcessing(t *testing.T) {
+	suite.Run(t, new(ProcessingTestSuite))
+}

+ 0 - 13
processing/result_size.go

@@ -1,13 +0,0 @@
-package processing
-
-import (
-	"github.com/imgproxy/imgproxy/v3/imath"
-	"github.com/imgproxy/imgproxy/v3/options"
-)
-
-func resultSize(po *options.ProcessingOptions, dprScale float64) (int, int) {
-	resultWidth := imath.Scale(po.Width, dprScale*po.ZoomWidth)
-	resultHeight := imath.Scale(po.Height, dprScale*po.ZoomHeight)
-
-	return resultWidth, resultHeight
-}

BIN
testdata/test2.jpg