Merge pull request #17569 from vavallee/fix/17056-no-image-upscaling

Stop image endpoints from upscaling beyond the source resolution
This commit is contained in:
Cody Robibero
2026-08-10 16:10:17 -04:00
committed by GitHub
4 changed files with 124 additions and 1 deletions
@@ -11,7 +11,9 @@ 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.
return DrawingUtils.ScaleDownToFit(newSize, originalImageSize);
}
}
}
@@ -103,6 +103,35 @@ namespace MediaBrowser.Model.Drawing
return new ImageDimensions(newWidth, newHeight);
}
/// <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>
@@ -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);
}
}
@@ -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);
}
}