Files
ersatztv/ErsatzTV.Core/Images/RemoteImageDecodeBudget.cs
T

55 lines
2.2 KiB
C#

namespace ErsatzTV.Core.Images;
/// <summary>
/// The decode-budget policy for a remote image, as pure arithmetic so it can be enforced both at
/// render time (graphics engine) and at save time (logo download) without materializing
/// multi-gigabyte images. Extracted from ImageElementBase for reuse. (ersatztv#525, from #511.)
/// </summary>
public static class RemoteImageDecodeBudget
{
/// <summary>
/// Ceiling on TOTAL decoded pixels — width x height x frames, as one product. Checking
/// dimensions and frame count independently does not bound the decode: a 60 KiB 2500x2500 x600
/// GIF passes both a 50 MP dimension check and a 600 frame check and costs ~14 GiB.
/// </summary>
public const long MaxRemoteDecodedPixels = 50_000_000;
/// <summary>Frame ceiling, a cheap legible guard against absurd counts of tiny frames.</summary>
public const int MaxRemoteFrames = 600;
public static void EnsureDimensionsAffordable(int width, int height, Uri uri)
{
long pixels = (long)width * height;
if (pixels > MaxRemoteDecodedPixels)
{
throw new InvalidOperationException(
$"Remote image {uri} is {width}x{height} ({pixels} pixels), over the "
+ $"{MaxRemoteDecodedPixels} pixel limit");
}
}
public static int AffordableFrames(int width, int height)
{
long perFrame = Math.Max((long)width * height, 1);
return (int)Math.Clamp(MaxRemoteDecodedPixels / perFrame, 1, MaxRemoteFrames);
}
public static void EnsureDecodeAffordable(int width, int height, int frameCount, Uri uri)
{
int frames = Math.Max(frameCount, 1);
if (frames > MaxRemoteFrames)
{
throw new InvalidOperationException(
$"Remote image {uri} has {frames} frames, over the {MaxRemoteFrames} frame limit");
}
long totalPixels = (long)width * height * frames;
if (totalPixels > MaxRemoteDecodedPixels)
{
throw new InvalidOperationException(
$"Remote image {uri} decodes to {width}x{height} x{frames} frames "
+ $"({totalPixels} pixels), over the {MaxRemoteDecodedPixels} pixel limit");
}
}
}