using System.Buffers.Binary; using System.IO; using System.Collections.Generic; using StbImageSharp; namespace codelanguage; /// Signature-based, platform-independent decoding for supported runtime icon rasters. internal static class RuntimeRasterDecoder { public static bool TryDecodeGifFrames(byte[] data, out int width, out int height, out List frames, out string error) { width = height = 0; frames = new List(); error = ""; if (data.Length < 6 || !(data.AsSpan(0, 6).SequenceEqual("GIF87a"u8) || data.AsSpan(0, 6).SequenceEqual("GIF89a"u8))) { error = "Invalid GIF signature."; return false; } try { using var stream = new MemoryStream(data, writable: false); long totalBytes = 0; foreach (var frame in ImageResult.AnimatedGifFramesFromStream(stream, ColorComponents.RedGreenBlueAlpha)) { if (frame.Width is < 1 or > 4096 || frame.Height is < 1 or > 4096 || frame.Data.Length != checked(frame.Width * frame.Height * 4)) { error = "GIF frame dimensions are outside supported bounds."; return false; } if (frames.Count == 0) { width = frame.Width; height = frame.Height; } if (frame.Width != width || frame.Height != height) { error = "GIF decoder returned inconsistent frame dimensions."; return false; } totalBytes += frame.Data.Length; if (totalBytes > 64L * 1024 * 1024 || frames.Count >= 1024) { error = "GIF animation exceeds runtime frame or memory limits."; frames.Clear(); width = height = 0; return false; } // The decoder may reuse its backing buffer when advancing the GIF enumerator. // Retain a snapshot per frame so later reads cannot all become the last frame. frames.Add(frame.Data.ToArray()); } if (frames.Count == 0) { error = "GIF contains no decoded frames."; return false; } return true; } catch (Exception ex) { frames.Clear(); width = height = 0; error = $"GIF decoding failed: {ex.Message}"; return false; } } public static bool TryDecode(byte[] data, out int width, out int height, out byte[] rgba, out string error) { if (data.Length >= 8 && data.AsSpan(0, 8).SequenceEqual(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 })) return PngPixelDecoder.TryDecode(data, out width, out height, out rgba, out error); if (data.Length >= 2 && data[0] == (byte)'B' && data[1] == (byte)'M') return TryDecodeBmp(data, out width, out height, out rgba, out error) || TryDecodeWithStb(data, out width, out height, out rgba, out error); bool jpeg = data.Length >= 2 && data[0] == 0xFF && data[1] == 0xD8; bool gif = data.Length >= 6 && (data.AsSpan(0, 6).SequenceEqual("GIF87a"u8) || data.AsSpan(0, 6).SequenceEqual("GIF89a"u8)); if (!jpeg && !gif) { width = height = 0; rgba = Array.Empty(); error = "Unsupported raster format; expected PNG, BMP, JPEG, or GIF."; return false; } return TryDecodeWithStb(data, out width, out height, out rgba, out error); } private static bool TryDecodeWithStb(byte[] data, out int width, out int height, out byte[] rgba, out string error) { try { var image = ImageResult.FromMemory(data, ColorComponents.RedGreenBlueAlpha); if (image.Width is < 1 or > 4096 || image.Height is < 1 or > 4096 || image.Data.Length != checked(image.Width * image.Height * 4)) { width = height = 0; rgba = Array.Empty(); error = "Decoded raster dimensions are outside supported bounds."; return false; } width = image.Width; height = image.Height; rgba = image.Data; error = ""; return true; } catch (Exception ex) { width = height = 0; rgba = Array.Empty(); error = $"Raster decoding failed: {ex.Message}"; return false; } } private static bool TryDecodeBmp(byte[] data, out int width, out int height, out byte[] rgba, out string error) { width = height = 0; rgba = Array.Empty(); error = ""; if (data.Length < 54) { error = "BMP header is truncated."; return false; } uint pixelOffset = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(10, 4)); uint dibSize = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(14, 4)); if (dibSize < 40 || 14L + dibSize > data.Length) { error = "Unsupported or truncated BMP DIB header."; return false; } int signedWidth = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(18, 4)); int signedHeight = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(22, 4)); ushort planes = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(26, 2)); ushort bits = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(28, 2)); uint compression = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(30, 4)); if (signedWidth is < 1 or > 4096 || signedHeight is 0 or < -4096 or > 4096 || signedHeight == int.MinValue) { error = "BMP dimensions are outside supported bounds."; return false; } if (planes != 1 || compression != 0 || bits is not (8 or 24 or 32)) { error = $"Unsupported BMP layout: planes={planes}, depth={bits}, compression={compression}."; return false; } width = signedWidth; height = Math.Abs(signedHeight); int bytesPerPixel = bits / 8; int stride; try { stride = checked((width * bytesPerPixel + 3) & ~3); } catch (OverflowException) { error = "BMP row size exceeds supported bounds."; return false; } if ((long)pixelOffset + (long)stride * height > data.Length) { error = "BMP pixel data is truncated."; width = height = 0; return false; } int paletteStart = checked(14 + (int)dibSize); int paletteCount = bits == 8 ? (int)BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(46, 4)) : 0; if (bits == 8 && paletteCount == 0) paletteCount = 256; if (paletteCount > 256 || bits == 8 && (paletteStart + (long)paletteCount * 4 > pixelOffset || pixelOffset > data.Length)) { error = "BMP color table is invalid or truncated."; width = height = 0; return false; } rgba = new byte[checked(width * height * 4)]; for (int y = 0; y < height; y++) { int sourceY = signedHeight > 0 ? height - 1 - y : y; int row = checked((int)pixelOffset + sourceY * stride); for (int x = 0; x < width; x++) { int dst = (y * width + x) * 4; if (bits == 8) { int entry = paletteStart + data[row + x] * 4; if (entry + 4 > pixelOffset) { error = "BMP pixel references an invalid palette entry."; rgba = Array.Empty(); return false; } rgba[dst] = data[entry + 2]; rgba[dst + 1] = data[entry + 1]; rgba[dst + 2] = data[entry]; rgba[dst + 3] = 255; } else { int src = row + x * bytesPerPixel; rgba[dst] = data[src + 2]; rgba[dst + 1] = data[src + 1]; rgba[dst + 2] = data[src]; rgba[dst + 3] = bits == 24 || compression == 0 ? (byte)255 : data[src + 3]; } } } return true; } }