bmp-js
bmp-js is a dependency-free CommonJS encoder and decoder for Windows BMP image data in Node.js. It decodes 1, 4, 8, 16, 24, and 32-bit files into metadata plus a four-byte-per-pixel ABGR buffer, with code paths for RLE4, RLE8, and bitfields. Encoding is much narrower: it accepts an ABGR buffer and writes an uncompressed 24-bit BMP, dropping the alpha byte.
A practical compatibility codec when BMP is unavoidable and you can own the ABGR conversion and input limits. It is not a general image library, and its alpha loss, synchronous API, and dormant release line make it a poor default for new upload pipelines.
Use it if
- You need to inspect or transform BMP files in Node without a native image dependency
- You must decode old paletted, RLE4, RLE8, 16-bit, 24-bit, or 32-bit BMP input
- You only need to emit basic uncompressed 24-bit BMP files
- You can adapt the package's ABGR pixel order to the rest of your image pipeline
- You need PNG, JPEG, WebP, GIF, TIFF, resizing, color management, or compositing: the API only decodes BMP and encodes 24-bit BMP
- You need alpha-preserving output: the encoder always writes 24-bit pixels and explicitly skips each input alpha byte
- Your pipeline expects RGBA: the README and source define decoded bytes as alpha, blue, green, red, so direct use produces swapped colors
- You decode untrusted uploads without a wrapper: the source allocates `width * height * 4` from header values and exposes no pixel, memory, or file-size limit
- You require active releases, TypeScript declarations, or native ESM: npm 0.1.0 dates to 2018 and publishes CommonJS with no types or export map
Setup reality
npm install bmp-js adds no runtime dependencies, native compilation, peer packages, credentials, or config. In Node CommonJS, require('bmp-js') exposes `decode(buffer)` and `encode({ data, width, height })`. Both are synchronous and operate on Node Buffers, so reading a large file and decoding it blocks the event loop; use a worker thread or an offline job when latency matters. The largest integration trap is channel order. Decoded and encoder input pixels use ABGR, four bytes per pixel, not RGBA. For a 24-bit source the decoder writes alpha as zero, not 255, while some 32-bit variants carry an alpha byte. Convert explicitly before handing pixels to Canvas, PNG encoders, or browser APIs. Encoding always produces top-down, uncompressed 24-bit BMP data and ignores the input alpha byte. The documented `quality` argument in source defaults to 100 but does not influence output. The README's sample also contains a capitalization typo: Node's method is `fs.writeFileSync`, not `fs.WriteFileSync`. There is no streaming API and no validation layer for untrusted headers. The decoder reads dimensions from the file and allocates width times height times four bytes, so check the BMP signature, minimum header length, dimensions, pixel count, and upload byte limit before calling it. The release remains 0.1.0 from 2018, with no bundled TypeScript types or ESM entry, so add your own declaration or wrapper and lock representative fixture tests if this sits in a production conversion path.
Patterns
Decode a BMP filedecode-file
const fs = require('node:fs');
const bmp = require('bmp-js');
const input = fs.readFileSync('./input.bmp');
const image = bmp.decode(input);
console.log(image.width, image.height, image.bitPP);decode is synchronous and returns ABGR pixel bytes in image.data.
Read one decoded pixelread-pixel
function getPixel(image, x, y) {
if (x < 0 || y < 0 || x >= image.width || y >= image.height) throw new RangeError('pixel');
const offset = (y * image.width + x) * 4;
const [a, b, g, r] = image.data.subarray(offset, offset + 4);
return { r, g, b, a };
}
console.log(getPixel(image, 10, 4));The source buffer order is ABGR. For 24-bit BMP input, the decoder sets alpha to 0.
Convert decoded ABGR bytes to RGBAconvert-to-rgba
function toRgba(image) {
const rgba = Buffer.alloc(image.data.length);
for (let i = 0; i < image.data.length; i += 4) {
rgba[i] = image.data[i + 3];
rgba[i + 1] = image.data[i + 2];
rgba[i + 2] = image.data[i + 1];
rgba[i + 3] = image.bitPP === 24 ? 255 : image.data[i];
}
return rgba;
}Promoting 24-bit alpha to 255 is a policy choice needed because the decoder stores zero there.
Encode ABGR pixels as a BMP fileencode-file
const fs = require('node:fs');
const bmp = require('bmp-js');
const encoded = bmp.encode({ data: abgrPixels, width, height });
fs.writeFileSync('./output.bmp', encoded.data);The encoder writes uncompressed 24-bit BMP and ignores every input alpha byte. Node uses writeFileSync with a lowercase w.
Create a solid-color BMPcreate-solid-image
const bmp = require('bmp-js');
const width = 64;
const height = 64;
const data = Buffer.alloc(width * height * 4);
for (let i = 0; i < data.length; i += 4) {
data[i] = 255; // A, ignored while encoding
data[i + 1] = 40; // B
data[i + 2] = 90; // G
data[i + 3] = 220;// R
}
const output = bmp.encode({ data, width, height }).data;Pixel input is ABGR even though the output file stores only BGR channels.
Edit a pixel in placeedit-pixel
function setPixel(image, x, y, { r, g, b, a = 255 }) {
const i = (y * image.width + x) * 4;
image.data[i] = a;
image.data[i + 1] = b;
image.data[i + 2] = g;
image.data[i + 3] = r;
}
setPixel(image, 0, 0, { r: 255, g: 0, b: 0 });Bounds and channel validation are caller responsibilities; encoding the result discards alpha.
Normalize any supported BMP to 24-bit outputround-trip-bmp
const bmp = require('bmp-js');
const decoded = bmp.decode(sourceBuffer);
const normalized = bmp.encode({
data: decoded.data,
width: decoded.width,
height: decoded.height,
}).data;This is lossy for palettes, compression, bit depth, header metadata, and alpha because encode always emits uncompressed 24-bit data.
Reject oversized dimensions before decodingguard-image-size
function decodeWithLimit(buffer, maxPixels = 20_000_000) {
if (buffer.length < 26 || buffer.toString('ascii', 0, 2) !== 'BM') throw new Error('invalid BMP header');
const width = buffer.readUInt32LE(18);
const signedHeight = buffer.readInt32LE(22);
const height = Math.abs(signedHeight);
if (!width || !height || width > Math.floor(maxPixels / height)) throw new Error('BMP too large');
return require('bmp-js').decode(buffer);
}The decoder allocates from header dimensions and provides no built-in memory or pixel cap; also enforce an upload byte limit.
Inspect decoded BMP header fieldsinspect-format
const image = bmp.decode(buffer);
console.log({
width: image.width,
height: image.height,
bitsPerPixel: image.bitPP,
compression: image.compress,
paletteEntries: image.palette?.length ?? 0,
});Header metadata reflects the input. Re-encoding does not preserve its bit depth, compression, or palette.
Create a BMP data URLmake-data-url
const encoded = bmp.encode({ data: abgrPixels, width, height });
const dataUrl = `data:image/bmp;base64,${encoded.data.toString('base64')}`;Base64 adds payload overhead and the encoder output has no alpha; use object storage or a binary response for large images.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sharp | npm | You need fast production image conversion, resizing, metadata, and many common formats and accept native binaries |
| jimp | npm | You want pure JavaScript image manipulation with a higher-level bitmap API |
| pngjs | npm | Your actual interchange format is PNG and you want a focused pure-JavaScript codec |
| utif | npm | You need a pure-JavaScript decoder for TIFF rather than BMP input |