Skip to content

Commit f239678

Browse files
feat: encode and decode resolution fields xPixelsPerMeter and yPixelsPerMeter
Encode / decode the image's horizontal and vertical number of pixels per meter. BREAKING CHANGE: In decoder's output data and encoder's input data, each pixel is now stored as 1 byte per pixel instead of 1 bit per pixel.
1 parent 0607b6b commit f239678

16 files changed

+196
-605
lines changed

src/BMPDecoder.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export default class BMPDecoder {
88
width: number;
99
height: number;
1010
bitDepth: number;
11-
11+
xPixelsPerMeter: number;
12+
yPixelsPerMeter: number;
1213
constructor(bufferData: Buffer) {
1314
this.bufferData = new IOBuffer(bufferData);
1415
const formatCheck = this.bufferData.readBytes(2);
@@ -21,6 +22,8 @@ export default class BMPDecoder {
2122
this.width = this.bufferData.skip(4).readUint32();
2223
this.height = this.bufferData.readUint32();
2324
this.bitDepth = this.bufferData.seek(28).readUint16();
25+
this.xPixelsPerMeter = this.bufferData.seek(38).readInt32();
26+
this.yPixelsPerMeter = this.bufferData.readInt32();
2427
if (this.bitDepth !== 1) {
2528
throw new Error('only bitDepth of 1 is supported');
2629
}
@@ -55,6 +58,8 @@ export default class BMPDecoder {
5558
channels,
5659
components,
5760
data,
61+
yPixelsPerMeter: this.yPixelsPerMeter,
62+
xPixelsPerMeter: this.xPixelsPerMeter,
5863
};
5964
}
6065
}

src/BMPEncoder.ts

Lines changed: 40 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,25 @@ export interface ImageCodec {
2727
* Image number of channels excluding alpha.
2828
*/
2929
components: number;
30+
/**
31+
* Horizontal number of pixels per meter.
32+
*/
33+
xPixelsPerMeter?: number;
34+
/**
35+
* Vertical number of pixels per meter.
36+
*/
37+
yPixelsPerMeter?: number;
3038
}
3139

32-
const tableLeft: number[] = [];
33-
for (let i = 0; i <= 8; i++) {
34-
tableLeft.push(0b11111111 << i);
35-
}
36-
37-
export default class BMPEncoder extends IOBuffer {
40+
export default class BMPEncoder {
3841
width: number;
3942
height: number;
4043
bitDepth: number;
4144
channels: number;
4245
components: number;
46+
data: Uint8Array;
47+
xPixelsPerMeter: number;
48+
yPixelsPerMeter: number;
4349
encoded: IOBuffer = new IOBuffer();
4450
constructor(data: ImageCodec) {
4551
if (data.bitDepth !== 1) {
@@ -48,84 +54,52 @@ export default class BMPEncoder extends IOBuffer {
4854
if (!data.height || !data.width) {
4955
throw new Error('ImageData width and height are required');
5056
}
51-
52-
super(data.data);
53-
57+
this.data = data.data as Uint8Array;
5458
this.width = data.width;
5559
this.height = data.height;
5660
this.bitDepth = data.bitDepth;
5761
this.channels = data.channels;
5862
this.components = data.components;
63+
this.xPixelsPerMeter =
64+
data.xPixelsPerMeter ?? BITMAPV5HEADER.DEFAULT_PIXELS_PER_METER;
65+
this.yPixelsPerMeter =
66+
data.yPixelsPerMeter ?? BITMAPV5HEADER.DEFAULT_PIXELS_PER_METER;
5967
}
6068

6169
encode() {
6270
this.encoded = new IOBuffer();
6371
this.encoded.skip(14);
72+
6473
this.writeBitmapV5Header();
6574
this.writeColorTable();
6675
const offset = this.encoded.offset;
76+
6777
this.writePixelArray();
78+
6879
const imageSize = this.encoded.getWrittenByteLength();
80+
6981
this.encoded.rewind();
7082
this.writeBitmapFileHeader(offset, imageSize);
83+
7184
return this.encoded.toArray();
7285
}
7386

7487
writePixelArray() {
75-
const io = this.encoded;
76-
const rowSize = Math.floor((this.bitDepth * this.width + 31) / 32) * 4;
77-
const dataRowSize = Math.ceil((this.bitDepth * this.width) / 8);
78-
const skipSize = rowSize - dataRowSize;
79-
const bitOverflow = (this.bitDepth * this.width) % 8;
80-
const bitSkip = bitOverflow === 0 ? 0 : 8 - bitOverflow;
81-
const totalBytes = rowSize * this.height;
82-
let byteA, byteB;
83-
let offset = 0; // Current off set in the ioData
84-
let relOffset = 0;
85-
let iOffset = 8;
86-
io.mark();
87-
byteB = this.readUint8();
88-
for (let i = this.height - 1; i >= 0; i--) {
89-
const lastRow = i === 0;
90-
io.reset();
91-
io.skip(i * rowSize);
92-
for (let j = 0; j < dataRowSize; j++) {
93-
const lastCol = j === dataRowSize - 1;
94-
if (relOffset <= bitSkip && lastCol) {
95-
// no need to read new data
88+
this.encoded.setBigEndian();
89+
let byte = 0;
9690

97-
io.writeByte(byteB << relOffset);
98-
if ((bitSkip === 0 || bitSkip === relOffset) && !lastRow) {
99-
byteA = byteB;
100-
byteB = this.readByte();
101-
}
102-
} else if (relOffset === 0) {
103-
byteA = byteB;
104-
byteB = this.readUint8();
105-
106-
io.writeByte(byteA);
107-
} else {
108-
byteA = byteB;
109-
byteB = this.readUint8();
110-
io.writeByte(
111-
((byteA << relOffset) & tableLeft[relOffset]) | (byteB >> iOffset)
112-
);
113-
}
114-
115-
if (lastCol) {
116-
offset += bitOverflow || 0;
117-
io.skip(skipSize);
118-
relOffset = offset % 8;
119-
iOffset = 8 - relOffset;
91+
for (let row = this.height - 1; row >= 0; row--) {
92+
for (let col = 0; col < this.width; col++) {
93+
if (col % 32 === 0 && row * this.width !== row * this.width + col) {
94+
this.encoded.writeUint32(byte);
95+
byte = 0;
12096
}
97+
byte |= this.data[row * this.width + col] << (31 - (col % 32));
12198
}
99+
this.encoded.writeUint32(byte);
100+
byte = 0;
122101
}
123-
if (rowSize > dataRowSize) {
124-
// make sure last written byte is correct
125-
io.reset();
126-
io.skip(totalBytes - 1);
127-
io.writeUint8(0);
128-
}
102+
this.encoded.setLittleEndian();
129103
}
130104

131105
writeColorTable() {
@@ -156,18 +130,18 @@ export default class BMPEncoder extends IOBuffer {
156130
.writeUint16(this.bitDepth) // bV5BitCount
157131
.writeUint32(BITMAPV5HEADER.Compression.BI_RGB) // bV5Compression - No compression
158132
.writeUint32(totalBytes) // bv5SizeImage - size of pixel buffer (can be 0 if uncompressed)
159-
.writeInt32(0) // bV5XPelsPerMeter - resolution
160-
.writeInt32(0) // bV5YPelsPerMeter - resolution
133+
.writeInt32(this.xPixelsPerMeter) // bV5XPelsPerMeter - resolution
134+
.writeInt32(this.yPixelsPerMeter) // bV5YPelsPerMeter - resolution
161135
.writeUint32(2 ** this.bitDepth)
162136
.writeUint32(2 ** this.bitDepth)
163-
.writeUint32(0xff000000) // bV5RedMask
164-
.writeUint32(0x00ff0000) // bV5GreenMask
165-
.writeUint32(0x0000ff00) // bV5BlueMask
166-
.writeUint32(0x000000ff) // bV5AlphaMask
137+
.writeUint32(0x00ff0000) // bV5BlueMask
138+
.writeUint32(0x0000ff00) // bV5GreenMask
139+
.writeUint32(0x000000ff) // bV5RedMask
140+
.writeUint32(0x00000000) // bv5ReservedData
167141
.writeUint32(BITMAPV5HEADER.LogicalColorSpace.LCS_sRGB)
168142
.skip(36) // bV5Endpoints
169143
.skip(12) // bV5GammaRed, Green, Blue
170-
.writeUint32(BITMAPV5HEADER.GamutMappingIntent.LCS_GM_IMAGES)
144+
.writeUint32(BITMAPV5HEADER.GamutMappingIntent.LCS_GM_GRAPHICS)
171145
.skip(12); // ProfileData, ProfileSize, Reserved
172146
}
173147
}

0 commit comments

Comments
 (0)