aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs6
-rw-r--r--MediaBrowser.Model/Drawing/DrawingUtils.cs29
-rw-r--r--tests/Jellyfin.Controller.Tests/Drawing/ImageHelperTests.cs64
-rw-r--r--tests/Jellyfin.Model.Tests/Drawing/DrawingUtilsTests.cs28
4 files changed, 126 insertions, 1 deletions
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index 9ef92bc981..c1d0203897 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -11,7 +11,11 @@ namespace MediaBrowser.Controller.Drawing
// Determine the output size based on incoming parameters
var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
- return newSize;
+
+ // Never encode larger than the source. Upscaling adds no detail, and the requested
+ // width/height are caller-controlled, so without this an unauthenticated request can
+ // pin a CPU and allocate several GB encoding a single image.
+ return DrawingUtils.ScaleDownToFit(newSize, originalImageSize);
}
}
}
diff --git a/MediaBrowser.Model/Drawing/DrawingUtils.cs b/MediaBrowser.Model/Drawing/DrawingUtils.cs
index 2040d26bbb..1fdd6a4a49 100644
--- a/MediaBrowser.Model/Drawing/DrawingUtils.cs
+++ b/MediaBrowser.Model/Drawing/DrawingUtils.cs
@@ -104,6 +104,35 @@ namespace MediaBrowser.Model.Drawing
}
/// <summary>
+ /// Scales a size down uniformly until it fits inside a bounding box.
+ /// Returns the original size if it already fits, so this never upscales.
+ /// </summary>
+ /// <param name="size">The size object.</param>
+ /// <param name="boundingBox">The box the result has to fit inside.</param>
+ /// <returns>A new size object, or <paramref name="size"/> if it already fits.</returns>
+ public static ImageDimensions ScaleDownToFit(ImageDimensions size, ImageDimensions boundingBox)
+ {
+ if (size.Width <= 0 || size.Height <= 0 || boundingBox.Width <= 0 || boundingBox.Height <= 0)
+ {
+ return size;
+ }
+
+ double widthRatio = size.Width / (double)boundingBox.Width;
+ double heightRatio = size.Height / (double)boundingBox.Height;
+ double scaleRatio = Math.Max(widthRatio, heightRatio);
+
+ if (scaleRatio <= 1)
+ {
+ return size;
+ }
+
+ var newWidth = Math.Clamp(Convert.ToInt32(Math.Round(size.Width / scaleRatio)), 1, boundingBox.Width);
+ var newHeight = Math.Clamp(Convert.ToInt32(Math.Round(size.Height / scaleRatio)), 1, boundingBox.Height);
+
+ return new ImageDimensions(newWidth, newHeight);
+ }
+
+ /// <summary>
/// Gets the new width.
/// </summary>
/// <param name="currentHeight">Height of the current.</param>
diff --git a/tests/Jellyfin.Controller.Tests/Drawing/ImageHelperTests.cs b/tests/Jellyfin.Controller.Tests/Drawing/ImageHelperTests.cs
new file mode 100644
index 0000000000..571cb7f0d4
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/Drawing/ImageHelperTests.cs
@@ -0,0 +1,64 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using Xunit;
+
+namespace Jellyfin.Controller.Tests.Drawing;
+
+public static class ImageHelperTests
+{
+ [Fact]
+ public static void GetNewImageSize_ExplicitSizeLargerThanSource_ClampsToSource()
+ {
+ // Regression test for https://github.com/jellyfin/jellyfin/issues/17056: the caller-supplied
+ // width/height were used verbatim, so a single request could ask for a 23100x23100 encode.
+ var options = new ImageProcessingOptions { Width = 23100, Height = 23100 };
+
+ var newSize = ImageHelper.GetNewImageSize(options, new ImageDimensions(600, 336));
+
+ Assert.Equal(336, newSize.Width);
+ Assert.Equal(336, newSize.Height);
+ }
+
+ [Fact]
+ public static void GetNewImageSize_WidthLargerThanSource_ClampsToSource()
+ {
+ var options = new ImageProcessingOptions { Width = 10000 };
+
+ var newSize = ImageHelper.GetNewImageSize(options, new ImageDimensions(600, 336));
+
+ Assert.Equal(600, newSize.Width);
+ Assert.Equal(336, newSize.Height);
+ }
+
+ [Fact]
+ public static void GetNewImageSize_FillLargerThanSource_ClampsToSource()
+ {
+ // ResizeFill already refused to upscale; this pins that behaviour.
+ var options = new ImageProcessingOptions { FillWidth = 23100, FillHeight = 23100 };
+
+ var newSize = ImageHelper.GetNewImageSize(options, new ImageDimensions(600, 336));
+
+ Assert.Equal(600, newSize.Width);
+ Assert.Equal(336, newSize.Height);
+ }
+
+ [Fact]
+ public static void GetNewImageSize_SmallerThanSource_StillDownscales()
+ {
+ var options = new ImageProcessingOptions { MaxWidth = 300 };
+
+ var newSize = ImageHelper.GetNewImageSize(options, new ImageDimensions(600, 336));
+
+ Assert.Equal(300, newSize.Width);
+ Assert.Equal(168, newSize.Height);
+ }
+
+ [Fact]
+ public static void GetNewImageSize_NoSizeRequested_ReturnsSource()
+ {
+ var newSize = ImageHelper.GetNewImageSize(new ImageProcessingOptions(), new ImageDimensions(600, 336));
+
+ Assert.Equal(600, newSize.Width);
+ Assert.Equal(336, newSize.Height);
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Drawing/DrawingUtilsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/DrawingUtilsTests.cs
new file mode 100644
index 0000000000..473b07a8a1
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Drawing/DrawingUtilsTests.cs
@@ -0,0 +1,28 @@
+using MediaBrowser.Model.Drawing;
+using Xunit;
+
+namespace Jellyfin.Model.Drawing;
+
+public static class DrawingUtilsTests
+{
+ [Theory]
+ // Already inside the box, returned untouched.
+ [InlineData(600, 336, 1920, 1080, 600, 336)]
+ [InlineData(1920, 1080, 1920, 1080, 1920, 1080)]
+ // Scaled down uniformly, requested aspect ratio preserved.
+ [InlineData(23100, 23100, 1920, 1080, 1080, 1080)]
+ [InlineData(3840, 2160, 1920, 1080, 1920, 1080)]
+ [InlineData(1200, 400, 600, 336, 600, 200)]
+ // Extreme ratios still produce at least one pixel per axis.
+ [InlineData(10000, 1, 100, 100, 100, 1)]
+ // Degenerate inputs are passed through rather than dividing by zero.
+ [InlineData(600, 336, 0, 0, 600, 336)]
+ [InlineData(0, 0, 1920, 1080, 0, 0)]
+ public static void ScaleDownToFit_Bounds_WithoutUpscaling(int width, int height, int boxWidth, int boxHeight, int expectedWidth, int expectedHeight)
+ {
+ var scaled = DrawingUtils.ScaleDownToFit(new ImageDimensions(width, height), new ImageDimensions(boxWidth, boxHeight));
+
+ Assert.Equal(expectedWidth, scaled.Width);
+ Assert.Equal(expectedHeight, scaled.Height);
+ }
+}