namespace ErsatzTV.Core.Images; /// /// 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.) /// public static class RemoteImageDecodeBudget { /// /// 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. /// public const long MaxRemoteDecodedPixels = 50_000_000; /// Frame ceiling, a cheap legible guard against absurd counts of tiny frames. 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"); } } }