95 lines
4.5 KiB
C#
95 lines
4.5 KiB
C#
using Minint.Core.Services;
|
||
|
||
namespace Minint.Infrastructure.Export;
|
||
|
||
/// <summary>
|
||
/// Writes a 32-bit BGRA BMP (BITMAPV4HEADER) from an ARGB pixel buffer.
|
||
/// BMP rows are bottom-up, so we flip vertically during write.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|