using Minint.Core.Services; namespace Minint.Infrastructure.Export; /// /// Writes a 32-bit BGRA BMP (BITMAPV4HEADER) from an ARGB pixel buffer. /// BMP rows are bottom-up, so we flip vertically during write. /// public sealed class BmpExporter : IBmpExporter { private const int BmpFileHeaderSize = 14; private const int BitmapV4HeaderSize = 108; private const int HeadersTotal = BmpFileHeaderSize + BitmapV4HeaderSize; public void Export(Stream stream, uint[] pixels, int width, int height) { ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(pixels); if (pixels.Length != width * height) throw new ArgumentException("Pixel buffer size does not match dimensions."); int rowBytes = width * 4; int imageSize = rowBytes * height; int fileSize = HeadersTotal + imageSize; using var w = new BinaryWriter(stream, System.Text.Encoding.UTF8, leaveOpen: true); // BITMAPFILEHEADER (14 bytes) w.Write((byte)'B'); w.Write((byte)'M'); w.Write(fileSize); w.Write((ushort)0); // reserved1 w.Write((ushort)0); // reserved2 w.Write(HeadersTotal); // bfOffBits — смещение до пикселей от начала файла // --- BITMAPV4HEADER (108 байт, см. WinGDI BITMAPV4HEADER) --- // Поля ниже идут подряд, little-endian. Расширяет BITMAPINFOHEADER полями // масок каналов (как при BI_BITFIELDS), типом цветового пространства и гаммой. // bV4Size (4) — размер этой структуры в байтах; обязательно 108 для V4. w.Write(BitmapV4HeaderSize); // bV4Width (4) — ширина растра в пикселях. w.Write(width); // bV4Height (4) — высота. Значение > 0: строки снизу вверх (первый байт строки — нижняя линия изображения). w.Write(height); // bV4Planes (2) — число цветовых плоскостей, для BMP всегда 1. w.Write((ushort)1); // bV4BitCount (2) — бит на пиксель; 32 = BGRA по байтам в строке. w.Write((ushort)32); // bV4Compression (4) — BI_BITFIELDS (3): цвета кодируются масками bV4*Mask, не таблицей. w.Write(3); // bV4SizeImage (4) — размер сырого bitmap в байтах (здесь width*height*4). w.Write(imageSize); // bV4XPelsPerMeter, bV4YPelsPerMeter (по 4) — разрешение для метаданных (~2835 ≈ 72 DPI). w.Write(2835); w.Write(2835); // bV4ClrUsed (4) — число используемых индексов палитры; 0 для полного 32 bpp. w.Write(0); // bV4ClrImportant (4) — «важных» цветов; 0 = все. w.Write(0); // Маски извлечения каналов из DWORD пикселя (порядок записи: R, G, B, A). // В памяти строки: B, G, R, A (младший байт — B); DWORD при little-endian совпадает с 0xAARRGGBB. w.Write(0x00FF0000u); // bV4RedMask w.Write(0x0000FF00u); // bV4GreenMask w.Write(0x000000FFu); // bV4BlueMask w.Write(0xFF000000u); // bV4AlphaMask // bV4CSType (4) — LCS_sRGB: 0x73524742 ('sRGB' в ASCII как little-endian DWORD). w.Write(0x73524742); // bV4Endpoints (36) — CIEXYZTRIPLE (координаты для профиля); для sRGB не используем — нули. w.Write(new byte[36]); // bV4GammaRed/Green/Blue (по 4 байта) — гамма; 0 = не задана / по умолчанию. w.Write(new byte[12]); // Pixel data: BMP is bottom-up, our buffer is top-down for (int y = height - 1; y >= 0; y--) { int rowStart = y * width; for (int x = 0; x < width; x++) { uint argb = pixels[rowStart + x]; byte a = (byte)(argb >> 24); byte r = (byte)((argb >> 16) & 0xFF); byte g = (byte)((argb >> 8) & 0xFF); byte b = (byte)(argb & 0xFF); w.Write(b); w.Write(g); w.Write(r); w.Write(a); } } } }