Skip to content

Commit 3345bce

Browse files
feat: add support for Greyscale, RGB and RGBA images
added support for these color models: - 8-bit grayscale - 24 RGB - 32 RGBA added `compression` and `colorMasks` fields to ImageCodec data BREAKING CHANGE: renamed `bitDepth` to `bitsPerPixel`
1 parent f239678 commit 3345bce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1250
-429
lines changed

README.md

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,89 @@
1010
Maintained by <a href="https://www.zakodium.com">Zakodium</a>
1111
</p>
1212

13-
1413
[![NPM version][npm-image]][npm-url]
1514
[![Test coverage][codecov-image]][codecov-url]
1615
[![npm download][download-image]][download-url]
1716

1817
</h3>
1918

19+
A library for encoding and decoding bmp image file format.
20+
References:
2021

21-
A library for encoding bmp image file format.
22+
- [Wikipedia BMP format page](https://en.wikipedia.org/wiki/BMP_file_format)
23+
- [Microsoft BMPV5 format page](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header)
2224

2325
# Supported features
2426

25-
For now there is only support for 1-bit image encoding.
27+
This library only supports V5 headers.
28+
29+
- binary (1-bit per pixel)
30+
- greyscale (8-bits per pixel)
31+
- RGB (24-bits per pixel)
32+
- RGBA (32-bits per pixel)
2633

2734
# Usage
2835

36+
## Encoding
37+
2938
```js
30-
const bmp = require('fast-bmp');
39+
import { encode } from 'fast-bmp';
3140

3241
// 0 0 0 0 0
3342
// 0 1 1 1 0
3443
// 0 1 0 1 0
3544
// 0 1 1 1 0
3645
// 0 0 0 0 0
46+
const data = new Uint8Array([
47+
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
48+
]);
3749
const imageData = {
3850
width: 5,
3951
height: 5,
40-
data: new Uint8Array([0b00000011, 0b10010100, 0b11100000, 0b00000000]),
41-
bitDepth: 1,
52+
data,
53+
bitsPerPixel: 1,
4254
components: 1,
4355
channels: 1,
4456
};
4557
// Encode returns a Uint8Array.
46-
const encoded = bmp.encode(imageData);
58+
const encoded = encode(imageData);
4759
fs.writeFileSync('image.bmp', encoded);
4860
```
4961

62+
## Decoding
63+
64+
```ts
65+
import { decode } from 'fast-bmp';
66+
67+
// 0 0 0 0 0
68+
// 0 1 1 1 0
69+
// 0 1 0 1 0
70+
// 0 1 1 1 0
71+
// 0 0 0 0 0
72+
const buffer = fs.writeFileSync('image.bmp');
73+
const imageData = decode(buffer);
74+
/* Returns object:
75+
{
76+
width: 5,
77+
height: 5,
78+
data: new Uint8Array([
79+
0, 0, 0, 0, 0,
80+
0, 1, 1, 1, 0,
81+
0, 1, 0, 1, 0,
82+
0, 1, 1, 1, 0,
83+
0, 0, 0, 0, 0,
84+
]),
85+
bitsPerPixel: 1,
86+
components: 1,
87+
channels: 1,
88+
colorMasks: [0x00ff0000, 0x0000ff00, 0x000000ff],
89+
compression: 0,
90+
xPixelsPerMeter: 2835,
91+
yPixelsPerMeter: 2835,
92+
}
93+
*/
94+
```
95+
5096
[npm-image]: https://img.shields.io/npm/v/fast-bmp.svg?style=flat-square
5197
[npm-url]: https://www.npmjs.com/package/fast-bmp
5298
[codecov-image]: https://img.shields.io/codecov/c/github/image-js/fast-bmp.svg?style=flat-square

src/BMPDecoder.ts

Lines changed: 115 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ export default class BMPDecoder {
77
pixelDataOffset: number;
88
width: number;
99
height: number;
10-
bitDepth: number;
10+
bitsPerPixel: number;
1111
xPixelsPerMeter: number;
1212
yPixelsPerMeter: number;
13+
compression: number;
14+
colorMasks: number[];
1315
constructor(bufferData: Buffer) {
1416
this.bufferData = new IOBuffer(bufferData);
1517
const formatCheck = this.bufferData.readBytes(2);
@@ -21,45 +23,138 @@ export default class BMPDecoder {
2123
this.pixelDataOffset = this.bufferData.skip(8).readUint32();
2224
this.width = this.bufferData.skip(4).readUint32();
2325
this.height = this.bufferData.readUint32();
24-
this.bitDepth = this.bufferData.seek(28).readUint16();
26+
this.bitsPerPixel = this.bufferData.seek(28).readUint16();
27+
if (
28+
this.bitsPerPixel !== 1 &&
29+
this.bitsPerPixel !== 8 &&
30+
this.bitsPerPixel !== 24 &&
31+
this.bitsPerPixel !== 32
32+
) {
33+
throw new Error(
34+
`Invalid number of bits per pixel. Supported number of bits per pixel: 1, 8, 24, 32. Received: ${this.bitsPerPixel}`
35+
);
36+
}
37+
this.compression = this.bufferData.readUint32();
38+
if (this.compression !== 0 && this.compression !== 3) {
39+
throw new Error(
40+
`Only BI_RGB and BI_BITFIELDS compression methods are allowed. `
41+
);
42+
}
43+
44+
this.colorMasks = [
45+
this.bufferData.seek(54).readUint32(),
46+
this.bufferData.readUint32(),
47+
this.bufferData.readUint32(),
48+
];
49+
50+
if (
51+
this.bitsPerPixel === 32 &&
52+
(this.colorMasks[0] !== 0x00ff0000 ||
53+
this.colorMasks[1] !== 0x0000ff00 ||
54+
this.colorMasks[2] !== 0x000000ff)
55+
) {
56+
throw new Error(
57+
`Unsupported color masks detected in 32-bit BMP image. Only standard RGBA (${(0x00ff0000).toString(
58+
16
59+
)}, ${(0x0000ff00).toString(16)}, ${(0x000000ff).toString(
60+
16
61+
)}) masks are supported. Received: ${this.colorMasks[0].toString(
62+
16
63+
)},${this.colorMasks[1].toString(16)},${this.colorMasks[2].toString(
64+
16
65+
)}.`
66+
);
67+
}
68+
this.bufferData.skip(1); // skipping image size.
2569
this.xPixelsPerMeter = this.bufferData.seek(38).readInt32();
2670
this.yPixelsPerMeter = this.bufferData.readInt32();
27-
if (this.bitDepth !== 1) {
28-
throw new Error('only bitDepth of 1 is supported');
29-
}
71+
this.bufferData.skip(1);
3072
}
3173

3274
decode(): ImageCodec {
3375
this.bufferData.seek(this.pixelDataOffset);
34-
const data = new Uint8Array(this.height * this.width * this.bitDepth);
3576
this.bufferData.setBigEndian();
77+
const channels = Math.ceil(this.bitsPerPixel / 8);
78+
const components = channels % 2 === 0 ? channels - 1 : channels;
79+
const data: Uint8Array = this.decodePixelData(channels, components);
80+
return {
81+
width: this.width,
82+
height: this.height,
83+
bitsPerPixel: this.bitsPerPixel,
84+
compression: this.compression,
85+
colorMasks: this.colorMasks,
86+
channels,
87+
components,
88+
data,
89+
yPixelsPerMeter: this.yPixelsPerMeter,
90+
xPixelsPerMeter: this.xPixelsPerMeter,
91+
};
92+
}
3693

37-
let currentNumber = 0;
94+
decodePixelData(channels: number, components: number): Uint8Array {
95+
const data = new Uint8Array(this.height * this.width * channels);
96+
if (this.bitsPerPixel === 1) {
97+
this.decodeBitDepth1Pixels(data);
98+
} else if (channels === components) {
99+
this.decodeStandardPixels(data, channels);
100+
} else {
101+
this.decodePixelsWithAlpha(data, channels, components);
102+
}
103+
return data;
104+
}
38105

106+
private decodeBitDepth1Pixels(data: Uint8Array) {
107+
let currentNumber = 0;
39108
for (let row = 0; row < this.height; row++) {
40109
for (let col = 0; col < this.width; col++) {
41110
const bitIndex = col % 32;
42111
if (bitIndex === 0) {
43112
currentNumber = this.bufferData.readUint32();
44113
}
45-
46114
if (currentNumber & (1 << (31 - bitIndex))) {
47115
data[(this.height - row - 1) * this.width + col] = 1;
48116
}
49117
}
50118
}
119+
}
51120

52-
const channels = Math.ceil(this.bitDepth / 8);
53-
const components = channels % 2 === 0 ? channels - 1 : channels;
54-
return {
55-
width: this.width,
56-
height: this.height,
57-
bitDepth: this.bitDepth,
58-
channels,
59-
components,
60-
data,
61-
yPixelsPerMeter: this.yPixelsPerMeter,
62-
xPixelsPerMeter: this.xPixelsPerMeter,
63-
};
121+
private decodeStandardPixels(data: Uint8Array, channels: number) {
122+
const padding = this.calculatePadding(channels);
123+
for (let row = 0; row < this.height; row++) {
124+
const rowOffset = (this.height - row - 1) * this.width;
125+
for (let col = 0; col < this.width; col++) {
126+
for (let channel = channels - 1; channel >= 0; channel--) {
127+
data[(rowOffset + col) * channels + channel] =
128+
this.bufferData.readByte();
129+
}
130+
}
131+
this.bufferData.skip(padding);
132+
}
133+
}
134+
135+
private decodePixelsWithAlpha(
136+
data: Uint8Array,
137+
channels: number,
138+
components: number
139+
) {
140+
for (let row = 0; row < this.height; row++) {
141+
const rowOffset = (this.height - row - 1) * this.width;
142+
143+
for (let col = 0; col < this.width; col++) {
144+
const pixelBaseIndex = (rowOffset + col) * channels;
145+
// Decode color components
146+
for (let component = components - 1; component >= 0; component--) {
147+
data[pixelBaseIndex + component] = this.bufferData.readByte();
148+
}
149+
// Decode alpha channel
150+
data[pixelBaseIndex + components] = this.bufferData.readByte();
151+
}
152+
}
153+
}
154+
155+
private calculatePadding(channels: number): number {
156+
return (this.width * channels) % 4 === 0
157+
? 0
158+
: 4 - ((this.width * channels) % 4);
64159
}
65160
}

0 commit comments

Comments
 (0)