Browse Source

Add `IMGPROXY_MAX_RESULT_DIMENSION` config and `max_result_dimension` processing option

DarthSim 3 weeks ago
parent
commit
a5a587eb39
6 changed files with 502 additions and 0 deletions
  1. 1 0
      CHANGELOG.md
  2. 4 0
      config/config.go
  3. 20 0
      options/processing_options.go
  4. 46 0
      processing/prepare.go
  5. 429 0
      processing/processing_test.go
  6. 2 0
      security/options.go

+ 1 - 0
CHANGELOG.md

@@ -2,6 +2,7 @@
 
 ## [Unreleased]
 ### Added
+- Add [IMGPROXY_MAX_RESULT_DIMENSION](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_MAX_RESULT_DIMENSION) config and [max_result_dimension](https://docs.imgproxy.net/latest/usage/processing#max-result-dimension) processing option.
 - Add `imgproxy.source_image_origin` attribute to New Relic, DataDog, and OpenTelemetry traces.
 - Add `imgproxy.source_image_url` and `imgproxy.source_image_origin` attributes to `downloading_image` spans in New Relic, DataDog, and OpenTelemetry traces.
 - Add `imgproxy.processing_options` attribute to `processing_image` spans in New Relic, DataDog, and OpenTelemetry traces.

+ 4 - 0
config/config.go

@@ -47,6 +47,7 @@ var (
 	MaxRedirects                int
 	PngUnlimited                bool
 	SvgUnlimited                bool
+	MaxResultDimension          int
 	AllowSecurityOptions        bool
 
 	JpegProgressive       bool
@@ -252,6 +253,7 @@ func Reset() {
 	MaxRedirects = 10
 	PngUnlimited = false
 	SvgUnlimited = false
+	MaxResultDimension = 0
 	AllowSecurityOptions = false
 
 	JpegProgressive = false
@@ -483,6 +485,8 @@ func Configure() error {
 	configurators.Bool(&PngUnlimited, "IMGPROXY_PNG_UNLIMITED")
 	configurators.Bool(&SvgUnlimited, "IMGPROXY_SVG_UNLIMITED")
 
+	configurators.Int(&MaxResultDimension, "IMGPROXY_MAX_RESULT_DIMENSION")
+
 	configurators.Bool(&AllowSecurityOptions, "IMGPROXY_ALLOW_SECURITY_OPTIONS")
 
 	configurators.Bool(&JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")

+ 20 - 0
options/processing_options.go

@@ -966,6 +966,24 @@ func applyMaxAnimationFrameResolutionOption(po *ProcessingOptions, args []string
 	return nil
 }
 
+func applyMaxResultDimensionOption(po *ProcessingOptions, args []string) error {
+	if err := security.IsSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		return newOptionArgumentError("Invalid max_result_dimension arguments: %v", args)
+	}
+
+	if x, err := strconv.Atoi(args[0]); err == nil {
+		po.SecurityOptions.MaxResultDimension = x
+	} else {
+		return newOptionArgumentError("Invalid max_result_dimension: %s", args[0])
+	}
+
+	return nil
+}
+
 func applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
 	switch name {
 	case "resize", "rs":
@@ -1056,6 +1074,8 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
 		return applyMaxAnimationFramesOption(po, args)
 	case "max_animation_frame_resolution", "mafr":
 		return applyMaxAnimationFrameResolutionOption(po, args)
+	case "max_result_dimension", "mrd":
+		return applyMaxResultDimensionOption(po, args)
 	}
 
 	return newUnknownOptionError("processing", name)

+ 46 - 0
processing/prepare.go

@@ -204,6 +204,50 @@ func (pctx *pipelineContext) calcSizes(widthToScale, heightToScale int, po *opti
 	}
 }
 
+func (pctx *pipelineContext) limitScale(widthToScale, heightToScale int, po *options.ProcessingOptions) {
+	maxresultDim := po.SecurityOptions.MaxResultDimension
+
+	if maxresultDim <= 0 {
+		return
+	}
+
+	outWidth := imath.MinNonZero(pctx.scaledWidth, pctx.resultCropWidth)
+	outHeight := imath.MinNonZero(pctx.scaledHeight, pctx.resultCropHeight)
+
+	if po.Extend.Enabled {
+		outWidth = imath.Max(outWidth, pctx.targetWidth)
+		outHeight = imath.Max(outHeight, pctx.targetHeight)
+	} else if po.ExtendAspectRatio.Enabled {
+		outWidth = imath.Max(outWidth, pctx.extendAspectRatioWidth)
+		outHeight = imath.Max(outHeight, pctx.extendAspectRatioHeight)
+	}
+
+	if po.Padding.Enabled {
+		outWidth += imath.ScaleToEven(po.Padding.Left, pctx.dprScale) + imath.ScaleToEven(po.Padding.Right, pctx.dprScale)
+		outHeight += imath.ScaleToEven(po.Padding.Top, pctx.dprScale) + imath.ScaleToEven(po.Padding.Bottom, pctx.dprScale)
+	}
+
+	if maxresultDim > 0 && (outWidth > maxresultDim || outHeight > maxresultDim) {
+		downScale := float64(maxresultDim) / float64(imath.Max(outWidth, outHeight))
+
+		pctx.wscale *= downScale
+		pctx.hscale *= downScale
+
+		// Prevent scaling below 1px
+		if minWScale := 1.0 / float64(widthToScale); pctx.wscale < minWScale {
+			pctx.wscale = minWScale
+		}
+		if minHScale := 1.0 / float64(heightToScale); pctx.hscale < minHScale {
+			pctx.hscale = minHScale
+		}
+
+		pctx.dprScale *= downScale
+
+		// Recalculate the sizes after changing the scales
+		pctx.calcSizes(widthToScale, heightToScale, po)
+	}
+}
+
 func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
 	pctx.imgtype = imagetype.Unknown
 	if imgdata != nil {
@@ -233,5 +277,7 @@ func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio
 
 	pctx.calcSizes(widthToScale, heightToScale, po)
 
+	pctx.limitScale(widthToScale, heightToScale, po)
+
 	return nil
 }

+ 429 - 0
processing/processing_test.go

@@ -560,6 +560,435 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 	}
 }
 
+func (s *ProcessingTestSuite) TestResultSizeLimit() {
+	imgdata := s.openFile("test2.jpg")
+
+	po := options.NewProcessingOptions()
+
+	testCases := []struct {
+		limit        int
+		width        int
+		height       int
+		resizingType options.ResizeType
+		enlarge      bool
+		extend       bool
+		extendAR     bool
+		padding      options.PaddingOptions
+		rotate       int
+		outWidth     int
+		outHeight    int
+	}{
+		{
+			limit:        1000,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFit,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFit,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        50,
+			width:        0,
+			height:       0,
+			resizingType: options.ResizeFit,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        100,
+			width:        0,
+			height:       100,
+			resizingType: options.ResizeFit,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        150,
+			height:       0,
+			resizingType: options.ResizeFit,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFit,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFit,
+			enlarge:      true,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       2000,
+			resizingType: options.ResizeFit,
+			extend:       true,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       2000,
+			resizingType: options.ResizeFit,
+			extendAR:     true,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        100,
+			height:       150,
+			resizingType: options.ResizeFit,
+			rotate:       90,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        0,
+			height:       0,
+			resizingType: options.ResizeFit,
+			rotate:       90,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        200,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFit,
+			padding: options.PaddingOptions{
+				Enabled: true,
+				Top:     100,
+				Right:   200,
+				Bottom:  300,
+				Left:    400,
+			},
+			outWidth:  200,
+			outHeight: 129,
+		},
+		{
+			limit:        1000,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFill,
+			outWidth:     100,
+			outHeight:    100,
+		},
+		{
+			limit:        50,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFill,
+			outWidth:     50,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        1000,
+			height:       50,
+			resizingType: options.ResizeFill,
+			outWidth:     50,
+			outHeight:    13,
+		},
+		{
+			limit:        50,
+			width:        100,
+			height:       1000,
+			resizingType: options.ResizeFill,
+			outWidth:     50,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        0,
+			height:       0,
+			resizingType: options.ResizeFill,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        100,
+			width:        0,
+			height:       100,
+			resizingType: options.ResizeFill,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        150,
+			height:       0,
+			resizingType: options.ResizeFill,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFill,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFill,
+			enlarge:      true,
+			outWidth:     100,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       2000,
+			resizingType: options.ResizeFill,
+			extend:       true,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       2000,
+			resizingType: options.ResizeFill,
+			extendAR:     true,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        100,
+			height:       150,
+			resizingType: options.ResizeFill,
+			rotate:       90,
+			outWidth:     67,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        0,
+			height:       0,
+			resizingType: options.ResizeFill,
+			rotate:       90,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        200,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFill,
+			padding: options.PaddingOptions{
+				Enabled: true,
+				Top:     100,
+				Right:   200,
+				Bottom:  300,
+				Left:    400,
+			},
+			outWidth:  200,
+			outHeight: 144,
+		},
+		{
+			limit:        1000,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFillDown,
+			outWidth:     100,
+			outHeight:    100,
+		},
+		{
+			limit:        50,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFillDown,
+			outWidth:     50,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        1000,
+			height:       50,
+			resizingType: options.ResizeFillDown,
+			outWidth:     50,
+			outHeight:    3,
+		},
+		{
+			limit:        50,
+			width:        100,
+			height:       1000,
+			resizingType: options.ResizeFillDown,
+			outWidth:     5,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        0,
+			height:       0,
+			resizingType: options.ResizeFillDown,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        100,
+			width:        0,
+			height:       100,
+			resizingType: options.ResizeFillDown,
+			outWidth:     100,
+			outHeight:    50,
+		},
+		{
+			limit:        50,
+			width:        150,
+			height:       0,
+			resizingType: options.ResizeFillDown,
+			outWidth:     50,
+			outHeight:    25,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFillDown,
+			outWidth:     100,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFillDown,
+			enlarge:      true,
+			outWidth:     100,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       2000,
+			resizingType: options.ResizeFillDown,
+			extend:       true,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       2000,
+			resizingType: options.ResizeFillDown,
+			extendAR:     true,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        1000,
+			height:       1500,
+			resizingType: options.ResizeFillDown,
+			rotate:       90,
+			outWidth:     67,
+			outHeight:    100,
+		},
+		{
+			limit:        100,
+			width:        0,
+			height:       0,
+			resizingType: options.ResizeFillDown,
+			rotate:       90,
+			outWidth:     50,
+			outHeight:    100,
+		},
+		{
+			limit:        200,
+			width:        100,
+			height:       100,
+			resizingType: options.ResizeFillDown,
+			padding: options.PaddingOptions{
+				Enabled: true,
+				Top:     100,
+				Right:   200,
+				Bottom:  300,
+				Left:    400,
+			},
+			outWidth:  200,
+			outHeight: 144,
+		},
+		{
+			limit:        200,
+			width:        1000,
+			height:       1000,
+			resizingType: options.ResizeFillDown,
+			padding: options.PaddingOptions{
+				Enabled: true,
+				Top:     100,
+				Right:   200,
+				Bottom:  300,
+				Left:    400,
+			},
+			outWidth:  200,
+			outHeight: 144,
+		},
+	}
+
+	for _, tc := range testCases {
+		name := fmt.Sprintf("%s_%dx%d_limit_%d", tc.resizingType, tc.width, tc.height, tc.limit)
+		if tc.enlarge {
+			name += "_enlarge"
+		}
+		if tc.extend {
+			name += "_extend"
+		}
+		if tc.extendAR {
+			name += "_extendAR"
+		}
+		if tc.rotate != 0 {
+			name += fmt.Sprintf("_rot_%d", tc.rotate)
+		}
+		if tc.padding.Enabled {
+			name += fmt.Sprintf("_padding_%dx%dx%dx%d", tc.padding.Top, tc.padding.Right, tc.padding.Bottom, tc.padding.Left)
+		}
+
+		s.Run(name, func() {
+			po.SecurityOptions.MaxResultDimension = tc.limit
+			po.Width = tc.width
+			po.Height = tc.height
+			po.ResizingType = tc.resizingType
+			po.Enlarge = tc.enlarge
+			po.Extend.Enabled = tc.extend
+			po.ExtendAspectRatio.Enabled = tc.extendAR
+			po.Rotate = tc.rotate
+			po.Padding = tc.padding
+
+			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))
 }

+ 2 - 0
security/options.go

@@ -9,6 +9,7 @@ type Options struct {
 	MaxSrcFileSize              int
 	MaxAnimationFrames          int
 	MaxAnimationFrameResolution int
+	MaxResultDimension          int
 }
 
 func DefaultOptions() Options {
@@ -17,6 +18,7 @@ func DefaultOptions() Options {
 		MaxSrcFileSize:              config.MaxSrcFileSize,
 		MaxAnimationFrames:          config.MaxAnimationFrames,
 		MaxAnimationFrameResolution: config.MaxAnimationFrameResolution,
+		MaxResultDimension:          config.MaxResultDimension,
 	}
 }