From a5a587eb39236ea11e1c45afbae7137a9876a19e Mon Sep 17 00:00:00 2001 From: DarthSim Date: Fri, 6 Jun 2025 19:28:41 +0300 Subject: [PATCH] Add `IMGPROXY_MAX_RESULT_DIMENSION` config and `max_result_dimension` processing option --- CHANGELOG.md | 1 + config/config.go | 4 + options/processing_options.go | 20 ++ processing/prepare.go | 46 ++++ processing/processing_test.go | 429 ++++++++++++++++++++++++++++++++++ security/options.go | 2 + 6 files changed, 502 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a85abf8a..9e185a43 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/config/config.go b/config/config.go index 17dca673..29236925 100644 --- a/config/config.go +++ b/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") diff --git a/options/processing_options.go b/options/processing_options.go index 09acbdf1..e675d498 100644 --- a/options/processing_options.go +++ b/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) diff --git a/processing/prepare.go b/processing/prepare.go index 3299493a..c5813375 100644 --- a/processing/prepare.go +++ b/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 } diff --git a/processing/processing_test.go b/processing/processing_test.go index ec621db3..fc15910f 100644 --- a/processing/processing_test.go +++ b/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)) } diff --git a/security/options.go b/security/options.go index 3a25f5c4..00b591df 100644 --- a/security/options.go +++ b/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, } }