@raven-js/cortex
Version:
Zero-dependency machine learning, AI, and data processing library for modern JavaScript
164 lines (141 loc) • 6.14 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file BMP encoder - Pure function to encode RGBA pixels to BMP buffer.
*
* Encodes RGBA pixel data to Windows BMP format. Supports both 24-bit (BGR)
* and 32-bit (BGRA) uncompressed BMP files. Pixels are stored bottom-up
* with 4-byte row alignment as per BMP specification.
*/
/**
* Write little-endian 32-bit integer to buffer.
*
* @param {Uint8Array} buffer - Buffer to write to
* @param {number} offset - Byte offset
* @param {number} value - 32-bit integer value
*/
function writeUint32LE(buffer, offset, value) {
buffer[offset] = value & 0xff;
buffer[offset + 1] = (value >> 8) & 0xff;
buffer[offset + 2] = (value >> 16) & 0xff;
buffer[offset + 3] = (value >> 24) & 0xff;
}
/**
* Write little-endian 16-bit integer to buffer.
*
* @param {Uint8Array} buffer - Buffer to write to
* @param {number} offset - Byte offset
* @param {number} value - 16-bit integer value
*/
function writeUint16LE(buffer, offset, value) {
buffer[offset] = value & 0xff;
buffer[offset + 1] = (value >> 8) & 0xff;
}
/**
* Encode RGBA pixel data to BMP buffer.
*
* @param {Uint8Array} pixels - RGBA pixel data (4 bytes per pixel)
* @param {number} width - Image width in pixels
* @param {number} height - Image height in pixels
* @param {Object} [options] - BMP encoding options
* @param {boolean} [options.hasAlpha=false] - Whether to preserve alpha channel (32-bit BMP)
* @param {number} [options.xResolution=0] - Horizontal resolution in pixels per meter
* @param {number} [options.yResolution=0] - Vertical resolution in pixels per meter
* @returns {Uint8Array} BMP encoded buffer
* @throws {Error} If BMP encoding fails
*
* @example
* const pixels = new Uint8Array(width * height * 4); // RGBA data
* const bmpBuffer = encodeBMP(pixels, width, height, { hasAlpha: true });
* writeFileSync('output.bmp', bmpBuffer);
*/
export function encodeBMP(pixels, width, height, options = {}) {
const { hasAlpha = false, xResolution = 0, yResolution = 0 } = options;
// Validate input parameters
if (!pixels || pixels.length === 0) {
throw new Error("BMP encoding failed: No pixel data provided for encoding");
}
if (!Number.isInteger(width) || width <= 0) {
throw new Error(`BMP encoding failed: Invalid width: ${width} (must be positive integer)`);
}
if (!Number.isInteger(height) || height <= 0) {
throw new Error(`BMP encoding failed: Invalid height: ${height} (must be positive integer)`);
}
const expectedPixelCount = width * height * 4; // RGBA = 4 bytes per pixel
if (pixels.length !== expectedPixelCount) {
throw new Error(
`BMP encoding failed: Pixel data length mismatch: expected ${expectedPixelCount} bytes for ${width}×${height} RGBA, got ${pixels.length}`
);
}
try {
// Determine BMP format
const bitCount = hasAlpha ? 32 : 24; // 32-bit for BGRA, 24-bit for BGR
const bytesPerPixel = bitCount / 8;
// Calculate row size (must be 4-byte aligned)
const rowSize = Math.floor((width * bitCount + 31) / 32) * 4;
const pixelDataSize = rowSize * height;
// Calculate file size
const fileHeaderSize = 14; // BITMAPFILEHEADER
const infoHeaderSize = 40; // BITMAPINFOHEADER
const dataOffset = fileHeaderSize + infoHeaderSize;
const totalSize = dataOffset + pixelDataSize;
// Create output buffer
const buffer = new Uint8Array(totalSize);
// Step 1: Write BMP file header (BITMAPFILEHEADER)
buffer[0] = 0x42; // 'B'
buffer[1] = 0x4d; // 'M'
writeUint32LE(buffer, 2, totalSize); // bfSize
writeUint16LE(buffer, 6, 0); // bfReserved1
writeUint16LE(buffer, 8, 0); // bfReserved2
writeUint32LE(buffer, 10, dataOffset); // bfOffBits
// Step 2: Write BMP info header (BITMAPINFOHEADER)
const infoOffset = fileHeaderSize;
writeUint32LE(buffer, infoOffset, infoHeaderSize); // biSize
writeUint32LE(buffer, infoOffset + 4, width); // biWidth
writeUint32LE(buffer, infoOffset + 8, height); // biHeight (positive = bottom-up)
writeUint16LE(buffer, infoOffset + 12, 1); // biPlanes
writeUint16LE(buffer, infoOffset + 14, bitCount); // biBitCount
writeUint32LE(buffer, infoOffset + 16, 0); // biCompression (uncompressed)
writeUint32LE(buffer, infoOffset + 20, pixelDataSize); // biSizeImage
writeUint32LE(buffer, infoOffset + 24, xResolution); // biXPelsPerMeter
writeUint32LE(buffer, infoOffset + 28, yResolution); // biYPelsPerMeter
writeUint32LE(buffer, infoOffset + 32, 0); // biClrUsed
writeUint32LE(buffer, infoOffset + 36, 0); // biClrImportant
// Step 3: Convert and write pixel data (bottom-up order)
let pixelIndex = 0;
for (let row = height - 1; row >= 0; row--) {
// Bottom-up
const rowOffset = dataOffset + row * rowSize;
for (let col = 0; col < width; col++) {
const pixelOffset = rowOffset + col * bytesPerPixel;
// Read RGBA from input
const r = pixels[pixelIndex++];
const g = pixels[pixelIndex++];
const b = pixels[pixelIndex++];
const a = pixels[pixelIndex++];
// Write as BGR/BGRA to output
buffer[pixelOffset] = b; // B
buffer[pixelOffset + 1] = g; // G
buffer[pixelOffset + 2] = r; // R
if (bitCount === 32) {
buffer[pixelOffset + 3] = a; // A
}
}
// Pad row to 4-byte boundary (already handled by rowSize calculation)
// The remaining bytes in the row are already zero-initialized
}
console.log(`✓ Successfully encoded BMP: ${width}×${height}`);
console.log(` - Bit depth: ${bitCount}, Format: ${bitCount === 32 ? "BGRA" : "BGR"}`);
console.log(` - Row size: ${rowSize} bytes (4-byte aligned)`);
console.log(` - Pixel data size: ${pixelDataSize} bytes`);
console.log(` - Total file size: ${totalSize} bytes`);
return buffer;
} catch (error) {
throw new Error(`BMP encoding failed: ${error.message}`);
}
}