@huh-david/bmp-js 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,36 @@
1
+ // src/binary.ts
2
+ function toUint8Array(input) {
3
+ if (input instanceof ArrayBuffer) {
4
+ return new Uint8Array(input);
5
+ }
6
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
7
+ }
8
+ function assertInteger(name, value) {
9
+ if (!Number.isInteger(value) || value <= 0) {
10
+ throw new Error(`${name} must be a positive integer`);
11
+ }
12
+ }
13
+
1
14
  // src/decoder.ts
15
+ var FILE_HEADER_SIZE = 14;
16
+ var INFO_HEADER_MIN = 40;
17
+ var CORE_HEADER_SIZE = 12;
18
+ function rowStride(width, bitPP) {
19
+ return Math.floor((bitPP * width + 31) / 32) * 4;
20
+ }
2
21
  var BmpDecoder = class {
3
22
  pos = 0;
4
- buffer;
5
- isWithAlpha;
23
+ bytes;
24
+ view;
25
+ options;
6
26
  bottomUp = true;
27
+ dibStart = FILE_HEADER_SIZE;
28
+ paletteEntrySize = 4;
29
+ externalMaskOffset = 0;
7
30
  maskRed = 0;
8
31
  maskGreen = 0;
9
32
  maskBlue = 0;
10
- mask0 = 0;
33
+ maskAlpha = 0;
11
34
  fileSize;
12
35
  reserved;
13
36
  offset;
@@ -24,67 +47,172 @@ var BmpDecoder = class {
24
47
  importantColors;
25
48
  palette;
26
49
  data;
27
- constructor(buffer, isWithAlpha = false) {
28
- this.buffer = buffer;
29
- this.isWithAlpha = isWithAlpha;
30
- const flag = this.buffer.toString("utf-8", 0, this.pos += 2);
31
- if (flag !== "BM") {
32
- throw new Error("Invalid BMP File");
33
- }
34
- this.parseHeader();
50
+ constructor(input, options = {}) {
51
+ this.bytes = toUint8Array(input);
52
+ this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
53
+ this.options = {
54
+ treat16BitAs15BitAlpha: options.treat16BitAs15BitAlpha ?? false
55
+ };
56
+ this.parseFileHeader();
57
+ this.parseDibHeader();
58
+ this.parsePalette();
59
+ this.pos = this.offset;
35
60
  this.parseRGBA();
36
61
  }
37
- parseHeader() {
38
- this.fileSize = this.buffer.readUInt32LE(this.pos);
39
- this.pos += 4;
40
- this.reserved = this.buffer.readUInt32LE(this.pos);
41
- this.pos += 4;
42
- this.offset = this.buffer.readUInt32LE(this.pos);
43
- this.pos += 4;
44
- this.headerSize = this.buffer.readUInt32LE(this.pos);
45
- this.pos += 4;
46
- this.width = this.buffer.readUInt32LE(this.pos);
47
- this.pos += 4;
48
- this.height = this.buffer.readInt32LE(this.pos);
49
- this.pos += 4;
50
- this.planes = this.buffer.readUInt16LE(this.pos);
51
- this.pos += 2;
52
- this.bitPP = this.buffer.readUInt16LE(this.pos);
53
- this.pos += 2;
54
- this.compress = this.buffer.readUInt32LE(this.pos);
55
- this.pos += 4;
56
- this.rawSize = this.buffer.readUInt32LE(this.pos);
57
- this.pos += 4;
58
- this.hr = this.buffer.readUInt32LE(this.pos);
59
- this.pos += 4;
60
- this.vr = this.buffer.readUInt32LE(this.pos);
61
- this.pos += 4;
62
- this.colors = this.buffer.readUInt32LE(this.pos);
63
- this.pos += 4;
64
- this.importantColors = this.buffer.readUInt32LE(this.pos);
65
- this.pos += 4;
66
- if (this.bitPP === 16 && this.isWithAlpha) {
67
- this.bitPP = 15;
62
+ ensureReadable(offset, size, context) {
63
+ if (offset < 0 || size < 0 || offset + size > this.bytes.length) {
64
+ throw new Error(`BMP decode out-of-range while reading ${context}`);
68
65
  }
69
- if (this.bitPP < 15) {
70
- const len = this.colors === 0 ? 1 << this.bitPP : this.colors;
71
- this.palette = new Array(len);
72
- for (let i = 0; i < len; i += 1) {
73
- const blue = this.buffer.readUInt8(this.pos++);
74
- const green = this.buffer.readUInt8(this.pos++);
75
- const red = this.buffer.readUInt8(this.pos++);
76
- const quad = this.buffer.readUInt8(this.pos++);
77
- this.palette[i] = { red, green, blue, quad };
78
- }
66
+ }
67
+ readUInt8(offset = this.pos) {
68
+ this.ensureReadable(offset, 1, "uint8");
69
+ if (offset === this.pos) this.pos += 1;
70
+ return this.view.getUint8(offset);
71
+ }
72
+ readUInt16LE(offset = this.pos) {
73
+ this.ensureReadable(offset, 2, "uint16");
74
+ if (offset === this.pos) this.pos += 2;
75
+ return this.view.getUint16(offset, true);
76
+ }
77
+ readInt16LE(offset = this.pos) {
78
+ this.ensureReadable(offset, 2, "int16");
79
+ if (offset === this.pos) this.pos += 2;
80
+ return this.view.getInt16(offset, true);
81
+ }
82
+ readUInt32LE(offset = this.pos) {
83
+ this.ensureReadable(offset, 4, "uint32");
84
+ if (offset === this.pos) this.pos += 4;
85
+ return this.view.getUint32(offset, true);
86
+ }
87
+ readInt32LE(offset = this.pos) {
88
+ this.ensureReadable(offset, 4, "int32");
89
+ if (offset === this.pos) this.pos += 4;
90
+ return this.view.getInt32(offset, true);
91
+ }
92
+ parseFileHeader() {
93
+ this.ensureReadable(0, FILE_HEADER_SIZE, "file header");
94
+ if (this.bytes[0] !== 66 || this.bytes[1] !== 77) {
95
+ throw new Error("Invalid BMP file signature");
79
96
  }
97
+ this.pos = 2;
98
+ this.fileSize = this.readUInt32LE();
99
+ this.reserved = this.readUInt32LE();
100
+ this.offset = this.readUInt32LE();
101
+ if (this.offset < FILE_HEADER_SIZE || this.offset > this.bytes.length) {
102
+ throw new Error(`Invalid pixel data offset: ${this.offset}`);
103
+ }
104
+ }
105
+ parseDibHeader() {
106
+ this.pos = this.dibStart;
107
+ this.headerSize = this.readUInt32LE();
108
+ if (this.headerSize < CORE_HEADER_SIZE) {
109
+ throw new Error(`Unsupported DIB header size: ${this.headerSize}`);
110
+ }
111
+ this.ensureReadable(this.dibStart, this.headerSize, "DIB header");
112
+ if (this.headerSize === CORE_HEADER_SIZE) {
113
+ this.parseCoreHeader();
114
+ return;
115
+ }
116
+ if (this.headerSize < INFO_HEADER_MIN) {
117
+ throw new Error(`Unsupported DIB header size: ${this.headerSize}`);
118
+ }
119
+ this.parseInfoHeader();
120
+ }
121
+ parseCoreHeader() {
122
+ const width = this.readUInt16LE(this.dibStart + 4);
123
+ const height = this.readUInt16LE(this.dibStart + 6);
124
+ this.width = width;
125
+ this.height = height;
126
+ this.planes = this.readUInt16LE(this.dibStart + 8);
127
+ this.bitPP = this.readUInt16LE(this.dibStart + 10);
128
+ this.compress = 0;
129
+ this.rawSize = 0;
130
+ this.hr = 0;
131
+ this.vr = 0;
132
+ this.colors = 0;
133
+ this.importantColors = 0;
134
+ this.bottomUp = true;
135
+ this.paletteEntrySize = 3;
136
+ this.externalMaskOffset = this.dibStart + this.headerSize;
137
+ this.validateDimensions();
138
+ }
139
+ parseInfoHeader() {
140
+ const rawWidth = this.readInt32LE(this.dibStart + 4);
141
+ const rawHeight = this.readInt32LE(this.dibStart + 8);
142
+ this.width = rawWidth;
143
+ this.height = rawHeight;
144
+ this.planes = this.readUInt16LE(this.dibStart + 12);
145
+ this.bitPP = this.readUInt16LE(this.dibStart + 14);
146
+ this.compress = this.readUInt32LE(this.dibStart + 16);
147
+ this.rawSize = this.readUInt32LE(this.dibStart + 20);
148
+ this.hr = this.readUInt32LE(this.dibStart + 24);
149
+ this.vr = this.readUInt32LE(this.dibStart + 28);
150
+ this.colors = this.readUInt32LE(this.dibStart + 32);
151
+ this.importantColors = this.readUInt32LE(this.dibStart + 36);
152
+ this.paletteEntrySize = 4;
153
+ this.externalMaskOffset = this.dibStart + this.headerSize;
80
154
  if (this.height < 0) {
81
155
  this.height *= -1;
82
156
  this.bottomUp = false;
83
157
  }
158
+ if (this.width < 0) {
159
+ this.width *= -1;
160
+ }
161
+ if (this.bitPP === 16 && this.options.treat16BitAs15BitAlpha) {
162
+ this.bitPP = 15;
163
+ }
164
+ this.validateDimensions();
165
+ this.parseBitMasks();
166
+ }
167
+ validateDimensions() {
168
+ if (!Number.isInteger(this.width) || !Number.isInteger(this.height) || this.width <= 0 || this.height <= 0) {
169
+ throw new Error(`Invalid BMP dimensions: ${this.width}x${this.height}`);
170
+ }
171
+ }
172
+ parseBitMasks() {
173
+ if (!(this.bitPP === 16 || this.bitPP === 32) || !(this.compress === 3 || this.compress === 6)) {
174
+ return;
175
+ }
176
+ const inHeaderMaskStart = this.dibStart + 40;
177
+ const hasMasksInHeader = this.headerSize >= 52;
178
+ const maskStart = hasMasksInHeader ? inHeaderMaskStart : this.externalMaskOffset;
179
+ const maskCount = this.compress === 6 || this.headerSize >= 56 ? 4 : 3;
180
+ this.ensureReadable(maskStart, maskCount * 4, "bit masks");
181
+ this.maskRed = this.readUInt32LE(maskStart);
182
+ this.maskGreen = this.readUInt32LE(maskStart + 4);
183
+ this.maskBlue = this.readUInt32LE(maskStart + 8);
184
+ this.maskAlpha = maskCount >= 4 ? this.readUInt32LE(maskStart + 12) : 0;
185
+ if (!hasMasksInHeader) {
186
+ this.externalMaskOffset += maskCount * 4;
187
+ }
188
+ }
189
+ parsePalette() {
190
+ if (this.bitPP >= 16) {
191
+ return;
192
+ }
193
+ const colorCount = this.colors === 0 ? 1 << this.bitPP : this.colors;
194
+ if (colorCount <= 0) {
195
+ return;
196
+ }
197
+ const paletteStart = this.externalMaskOffset;
198
+ const paletteSize = colorCount * this.paletteEntrySize;
199
+ if (paletteStart + paletteSize > this.offset) {
200
+ throw new Error("Palette data overlaps or exceeds pixel data offset");
201
+ }
202
+ this.palette = new Array(colorCount);
203
+ for (let i = 0; i < colorCount; i += 1) {
204
+ const base = paletteStart + i * this.paletteEntrySize;
205
+ const blue = this.readUInt8(base);
206
+ const green = this.readUInt8(base + 1);
207
+ const red = this.readUInt8(base + 2);
208
+ const quad = this.paletteEntrySize === 4 ? this.readUInt8(base + 3) : 0;
209
+ this.palette[i] = { red, green, blue, quad };
210
+ }
84
211
  }
85
212
  parseRGBA() {
86
- const len = this.width * this.height * 4;
87
- this.data = Buffer.alloc(len);
213
+ const pixelCount = this.width * this.height;
214
+ const len = pixelCount * 4;
215
+ this.data = new Uint8Array(len);
88
216
  switch (this.bitPP) {
89
217
  case 1:
90
218
  this.bit1();
@@ -116,312 +244,254 @@ var BmpDecoder = class {
116
244
  if (color) {
117
245
  return color;
118
246
  }
119
- return {
120
- red: 255,
121
- green: 255,
122
- blue: 255,
123
- quad: 0
124
- };
247
+ return { red: 255, green: 255, blue: 255, quad: 0 };
248
+ }
249
+ setPixel(destY, x, alpha, blue, green, red) {
250
+ const base = (destY * this.width + x) * 4;
251
+ this.data[base] = alpha;
252
+ this.data[base + 1] = blue;
253
+ this.data[base + 2] = green;
254
+ this.data[base + 3] = red;
125
255
  }
126
256
  bit1() {
127
- const xLen = Math.ceil(this.width / 8);
128
- const mode = xLen % 4;
129
- for (let y = this.height - 1; y >= 0; y -= 1) {
130
- const line = this.bottomUp ? y : this.height - 1 - y;
131
- for (let x = 0; x < xLen; x += 1) {
132
- const b = this.buffer.readUInt8(this.pos++);
133
- const location = line * this.width * 4 + x * 8 * 4;
134
- for (let i = 0; i < 8; i += 1) {
135
- if (x * 8 + i >= this.width) {
136
- break;
137
- }
138
- const rgb = this.getPaletteColor(b >> 7 - i & 1);
139
- this.data[location + i * 4] = 0;
140
- this.data[location + i * 4 + 1] = rgb.blue;
141
- this.data[location + i * 4 + 2] = rgb.green;
142
- this.data[location + i * 4 + 3] = rgb.red;
143
- }
144
- }
145
- if (mode !== 0) {
146
- this.pos += 4 - mode;
257
+ const stride = rowStride(this.width, 1);
258
+ const bytesPerRow = Math.ceil(this.width / 8);
259
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
260
+ const rowStart = this.offset + srcRow * stride;
261
+ this.ensureReadable(rowStart, bytesPerRow, "1-bit row");
262
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
263
+ for (let x = 0; x < this.width; x += 1) {
264
+ const packed = this.readUInt8(rowStart + Math.floor(x / 8));
265
+ const bit = packed >> 7 - x % 8 & 1;
266
+ const rgb = this.getPaletteColor(bit);
267
+ this.setPixel(destY, x, 0, rgb.blue, rgb.green, rgb.red);
147
268
  }
148
269
  }
149
270
  }
150
271
  bit4() {
151
272
  if (this.compress === 2) {
152
- let setPixelData2 = function(rgbIndex) {
153
- const rgb = this.getPaletteColor(rgbIndex);
154
- this.data[location] = 0;
155
- this.data[location + 1] = rgb.blue;
156
- this.data[location + 2] = rgb.green;
157
- this.data[location + 3] = rgb.red;
158
- location += 4;
159
- };
160
- var setPixelData = setPixelData2;
161
- this.data.fill(255);
162
- let location = 0;
163
- let lines = this.bottomUp ? this.height - 1 : 0;
164
- let lowNibble = false;
165
- while (location < this.data.length) {
166
- const a = this.buffer.readUInt8(this.pos++);
167
- const b = this.buffer.readUInt8(this.pos++);
168
- if (a === 0) {
169
- if (b === 0) {
170
- lines += this.bottomUp ? -1 : 1;
171
- location = lines * this.width * 4;
172
- lowNibble = false;
173
- continue;
174
- }
175
- if (b === 1) {
176
- break;
177
- }
178
- if (b === 2) {
179
- const x = this.buffer.readUInt8(this.pos++);
180
- const y = this.buffer.readUInt8(this.pos++);
181
- lines += this.bottomUp ? -y : y;
182
- location += y * this.width * 4 + x * 4;
183
- continue;
184
- }
185
- let c = this.buffer.readUInt8(this.pos++);
186
- for (let i = 0; i < b; i += 1) {
187
- if (lowNibble) {
188
- setPixelData2.call(this, c & 15);
189
- } else {
190
- setPixelData2.call(this, (c & 240) >> 4);
191
- }
192
- if ((i & 1) === 1 && i + 1 < b) {
193
- c = this.buffer.readUInt8(this.pos++);
194
- }
195
- lowNibble = !lowNibble;
196
- }
197
- if ((b + 1 >> 1 & 1) === 1) {
198
- this.pos += 1;
199
- }
200
- } else {
201
- for (let i = 0; i < a; i += 1) {
202
- if (lowNibble) {
203
- setPixelData2.call(this, b & 15);
204
- } else {
205
- setPixelData2.call(this, (b & 240) >> 4);
206
- }
207
- lowNibble = !lowNibble;
208
- }
209
- }
210
- }
273
+ this.bit4Rle();
211
274
  return;
212
275
  }
213
- const xLen = Math.ceil(this.width / 2);
214
- const mode = xLen % 4;
215
- for (let y = this.height - 1; y >= 0; y -= 1) {
216
- const line = this.bottomUp ? y : this.height - 1 - y;
217
- for (let x = 0; x < xLen; x += 1) {
218
- const b = this.buffer.readUInt8(this.pos++);
219
- const location = line * this.width * 4 + x * 2 * 4;
220
- const before = b >> 4;
221
- const after = b & 15;
222
- let rgb = this.getPaletteColor(before);
223
- this.data[location] = 0;
224
- this.data[location + 1] = rgb.blue;
225
- this.data[location + 2] = rgb.green;
226
- this.data[location + 3] = rgb.red;
227
- if (x * 2 + 1 >= this.width) {
228
- break;
229
- }
230
- rgb = this.getPaletteColor(after);
231
- this.data[location + 4] = 0;
232
- this.data[location + 5] = rgb.blue;
233
- this.data[location + 6] = rgb.green;
234
- this.data[location + 7] = rgb.red;
235
- }
236
- if (mode !== 0) {
237
- this.pos += 4 - mode;
276
+ const stride = rowStride(this.width, 4);
277
+ const bytesPerRow = Math.ceil(this.width / 2);
278
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
279
+ const rowStart = this.offset + srcRow * stride;
280
+ this.ensureReadable(rowStart, bytesPerRow, "4-bit row");
281
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
282
+ for (let x = 0; x < this.width; x += 1) {
283
+ const packed = this.readUInt8(rowStart + Math.floor(x / 2));
284
+ const idx = x % 2 === 0 ? (packed & 240) >> 4 : packed & 15;
285
+ const rgb = this.getPaletteColor(idx);
286
+ this.setPixel(destY, x, 0, rgb.blue, rgb.green, rgb.red);
238
287
  }
239
288
  }
240
289
  }
241
290
  bit8() {
242
291
  if (this.compress === 1) {
243
- let setPixelData2 = function(rgbIndex) {
244
- const rgb = this.getPaletteColor(rgbIndex);
245
- this.data[location] = 0;
246
- this.data[location + 1] = rgb.blue;
247
- this.data[location + 2] = rgb.green;
248
- this.data[location + 3] = rgb.red;
249
- location += 4;
250
- };
251
- var setPixelData = setPixelData2;
252
- this.data.fill(255);
253
- let location = 0;
254
- let lines = this.bottomUp ? this.height - 1 : 0;
255
- while (location < this.data.length) {
256
- const a = this.buffer.readUInt8(this.pos++);
257
- const b = this.buffer.readUInt8(this.pos++);
258
- if (a === 0) {
259
- if (b === 0) {
260
- lines += this.bottomUp ? -1 : 1;
261
- location = lines * this.width * 4;
262
- continue;
263
- }
264
- if (b === 1) {
265
- break;
266
- }
267
- if (b === 2) {
268
- const x = this.buffer.readUInt8(this.pos++);
269
- const y = this.buffer.readUInt8(this.pos++);
270
- lines += this.bottomUp ? -y : y;
271
- location += y * this.width * 4 + x * 4;
272
- continue;
273
- }
274
- for (let i = 0; i < b; i += 1) {
275
- const c = this.buffer.readUInt8(this.pos++);
276
- setPixelData2.call(this, c);
277
- }
278
- if ((b & 1) === 1) {
279
- this.pos += 1;
280
- }
281
- } else {
282
- for (let i = 0; i < a; i += 1) {
283
- setPixelData2.call(this, b);
284
- }
285
- }
286
- }
292
+ this.bit8Rle();
287
293
  return;
288
294
  }
289
- const mode = this.width % 4;
290
- for (let y = this.height - 1; y >= 0; y -= 1) {
291
- const line = this.bottomUp ? y : this.height - 1 - y;
295
+ const stride = rowStride(this.width, 8);
296
+ const bytesPerRow = this.width;
297
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
298
+ const rowStart = this.offset + srcRow * stride;
299
+ this.ensureReadable(rowStart, bytesPerRow, "8-bit row");
300
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
292
301
  for (let x = 0; x < this.width; x += 1) {
293
- const b = this.buffer.readUInt8(this.pos++);
294
- const location = line * this.width * 4 + x * 4;
295
- const rgb = this.getPaletteColor(b);
296
- this.data[location] = 0;
297
- this.data[location + 1] = rgb.blue;
298
- this.data[location + 2] = rgb.green;
299
- this.data[location + 3] = rgb.red;
300
- }
301
- if (mode !== 0) {
302
- this.pos += 4 - mode;
302
+ const idx = this.readUInt8(rowStart + x);
303
+ const rgb = this.getPaletteColor(idx);
304
+ this.setPixel(destY, x, 0, rgb.blue, rgb.green, rgb.red);
303
305
  }
304
306
  }
305
307
  }
306
308
  bit15() {
307
- const difW = this.width % 3;
308
- const m = 31;
309
- for (let y = this.height - 1; y >= 0; y -= 1) {
310
- const line = this.bottomUp ? y : this.height - 1 - y;
309
+ const stride = rowStride(this.width, 16);
310
+ const max = 31;
311
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
312
+ const rowStart = this.offset + srcRow * stride;
313
+ this.ensureReadable(rowStart, this.width * 2, "15-bit row");
314
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
311
315
  for (let x = 0; x < this.width; x += 1) {
312
- const value = this.buffer.readUInt16LE(this.pos);
313
- this.pos += 2;
314
- const blue = (value & m) / m * 255;
315
- const green = (value >> 5 & m) / m * 255;
316
- const red = (value >> 10 & m) / m * 255;
317
- const alpha = value >> 15 !== 0 ? 255 : 0;
318
- const location = line * this.width * 4 + x * 4;
319
- this.data[location] = alpha;
320
- this.data[location + 1] = blue | 0;
321
- this.data[location + 2] = green | 0;
322
- this.data[location + 3] = red | 0;
316
+ const value = this.readUInt16LE(rowStart + x * 2);
317
+ const blue = (value >> 0 & max) / max * 255;
318
+ const green = (value >> 5 & max) / max * 255;
319
+ const red = (value >> 10 & max) / max * 255;
320
+ const alpha = (value & 32768) !== 0 ? 255 : 0;
321
+ this.setPixel(destY, x, alpha, blue | 0, green | 0, red | 0);
323
322
  }
324
- this.pos += difW;
325
323
  }
326
324
  }
325
+ scaleMasked(value, mask) {
326
+ if (mask === 0) return 0;
327
+ let shift = 0;
328
+ let bits = 0;
329
+ let m = mask;
330
+ while ((m & 1) === 0) {
331
+ shift += 1;
332
+ m >>>= 1;
333
+ }
334
+ while ((m & 1) === 1) {
335
+ bits += 1;
336
+ m >>>= 1;
337
+ }
338
+ const component = (value & mask) >>> shift;
339
+ if (bits >= 8) {
340
+ return component >>> bits - 8;
341
+ }
342
+ return component << 8 - bits & 255;
343
+ }
327
344
  bit16() {
328
- const difW = this.width % 2 * 2;
329
- this.maskRed = 31744;
330
- this.maskGreen = 992;
331
- this.maskBlue = 31;
332
- this.mask0 = 0;
333
- if (this.compress === 3) {
334
- this.maskRed = this.buffer.readUInt32LE(this.pos);
335
- this.pos += 4;
336
- this.maskGreen = this.buffer.readUInt32LE(this.pos);
337
- this.pos += 4;
338
- this.maskBlue = this.buffer.readUInt32LE(this.pos);
339
- this.pos += 4;
340
- this.mask0 = this.buffer.readUInt32LE(this.pos);
341
- this.pos += 4;
342
- }
343
- const ns = [0, 0, 0];
344
- for (let i = 0; i < 16; i += 1) {
345
- if ((this.maskRed >> i & 1) !== 0) ns[0] += 1;
346
- if ((this.maskGreen >> i & 1) !== 0) ns[1] += 1;
347
- if ((this.maskBlue >> i & 1) !== 0) ns[2] += 1;
348
- }
349
- ns[1] += ns[0];
350
- ns[2] += ns[1];
351
- ns[0] = 8 - ns[0];
352
- ns[1] -= 8;
353
- ns[2] -= 8;
354
- for (let y = this.height - 1; y >= 0; y -= 1) {
355
- const line = this.bottomUp ? y : this.height - 1 - y;
345
+ if (this.maskRed === 0 && this.maskGreen === 0 && this.maskBlue === 0) {
346
+ this.maskRed = 31744;
347
+ this.maskGreen = 992;
348
+ this.maskBlue = 31;
349
+ }
350
+ const stride = rowStride(this.width, 16);
351
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
352
+ const rowStart = this.offset + srcRow * stride;
353
+ this.ensureReadable(rowStart, this.width * 2, "16-bit row");
354
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
356
355
  for (let x = 0; x < this.width; x += 1) {
357
- const value = this.buffer.readUInt16LE(this.pos);
358
- this.pos += 2;
359
- const blue = (value & this.maskBlue) << ns[0];
360
- const green = (value & this.maskGreen) >> ns[1];
361
- const red = (value & this.maskRed) >> ns[2];
362
- const location = line * this.width * 4 + x * 4;
363
- this.data[location] = 0;
364
- this.data[location + 1] = blue;
365
- this.data[location + 2] = green;
366
- this.data[location + 3] = red;
356
+ const value = this.readUInt16LE(rowStart + x * 2);
357
+ const blue = this.scaleMasked(value, this.maskBlue);
358
+ const green = this.scaleMasked(value, this.maskGreen);
359
+ const red = this.scaleMasked(value, this.maskRed);
360
+ const alpha = this.maskAlpha !== 0 ? this.scaleMasked(value, this.maskAlpha) : 0;
361
+ this.setPixel(destY, x, alpha, blue, green, red);
367
362
  }
368
- this.pos += difW;
369
363
  }
370
364
  }
371
365
  bit24() {
372
- for (let y = this.height - 1; y >= 0; y -= 1) {
373
- const line = this.bottomUp ? y : this.height - 1 - y;
366
+ const stride = rowStride(this.width, 24);
367
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
368
+ const rowStart = this.offset + srcRow * stride;
369
+ this.ensureReadable(rowStart, this.width * 3, "24-bit row");
370
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
374
371
  for (let x = 0; x < this.width; x += 1) {
375
- const blue = this.buffer.readUInt8(this.pos++);
376
- const green = this.buffer.readUInt8(this.pos++);
377
- const red = this.buffer.readUInt8(this.pos++);
378
- const location = line * this.width * 4 + x * 4;
379
- this.data[location] = 0;
380
- this.data[location + 1] = blue;
381
- this.data[location + 2] = green;
382
- this.data[location + 3] = red;
372
+ const base = rowStart + x * 3;
373
+ const blue = this.readUInt8(base);
374
+ const green = this.readUInt8(base + 1);
375
+ const red = this.readUInt8(base + 2);
376
+ this.setPixel(destY, x, 0, blue, green, red);
383
377
  }
384
- this.pos += this.width % 4;
385
378
  }
386
379
  }
387
380
  bit32() {
388
- if (this.compress === 3) {
389
- this.maskRed = this.buffer.readUInt32LE(this.pos);
390
- this.pos += 4;
391
- this.maskGreen = this.buffer.readUInt32LE(this.pos);
392
- this.pos += 4;
393
- this.maskBlue = this.buffer.readUInt32LE(this.pos);
394
- this.pos += 4;
395
- this.mask0 = this.buffer.readUInt32LE(this.pos);
396
- this.pos += 4;
397
- for (let y = this.height - 1; y >= 0; y -= 1) {
398
- const line = this.bottomUp ? y : this.height - 1 - y;
399
- for (let x = 0; x < this.width; x += 1) {
400
- const alpha = this.buffer.readUInt8(this.pos++);
401
- const blue = this.buffer.readUInt8(this.pos++);
402
- const green = this.buffer.readUInt8(this.pos++);
403
- const red = this.buffer.readUInt8(this.pos++);
404
- const location = line * this.width * 4 + x * 4;
405
- this.data[location] = alpha;
406
- this.data[location + 1] = blue;
407
- this.data[location + 2] = green;
408
- this.data[location + 3] = red;
381
+ const stride = rowStride(this.width, 32);
382
+ for (let srcRow = 0; srcRow < this.height; srcRow += 1) {
383
+ const rowStart = this.offset + srcRow * stride;
384
+ this.ensureReadable(rowStart, this.width * 4, "32-bit row");
385
+ const destY = this.bottomUp ? this.height - 1 - srcRow : srcRow;
386
+ for (let x = 0; x < this.width; x += 1) {
387
+ const base = rowStart + x * 4;
388
+ if (this.compress === 3 || this.compress === 6) {
389
+ const pixel = this.readUInt32LE(base);
390
+ const red = this.scaleMasked(pixel, this.maskRed || 16711680);
391
+ const green = this.scaleMasked(pixel, this.maskGreen || 65280);
392
+ const blue = this.scaleMasked(pixel, this.maskBlue || 255);
393
+ const alpha = this.maskAlpha === 0 ? 0 : this.scaleMasked(pixel, this.maskAlpha);
394
+ this.setPixel(destY, x, alpha, blue, green, red);
395
+ } else {
396
+ const blue = this.readUInt8(base);
397
+ const green = this.readUInt8(base + 1);
398
+ const red = this.readUInt8(base + 2);
399
+ const alpha = this.readUInt8(base + 3);
400
+ this.setPixel(destY, x, alpha, blue, green, red);
409
401
  }
410
402
  }
411
- return;
412
403
  }
413
- for (let y = this.height - 1; y >= 0; y -= 1) {
414
- const line = this.bottomUp ? y : this.height - 1 - y;
415
- for (let x = 0; x < this.width; x += 1) {
416
- const blue = this.buffer.readUInt8(this.pos++);
417
- const green = this.buffer.readUInt8(this.pos++);
418
- const red = this.buffer.readUInt8(this.pos++);
419
- const alpha = this.buffer.readUInt8(this.pos++);
420
- const location = line * this.width * 4 + x * 4;
421
- this.data[location] = alpha;
422
- this.data[location + 1] = blue;
423
- this.data[location + 2] = green;
424
- this.data[location + 3] = red;
404
+ }
405
+ bit8Rle() {
406
+ this.data.fill(255);
407
+ this.pos = this.offset;
408
+ let x = 0;
409
+ let y = this.bottomUp ? this.height - 1 : 0;
410
+ while (this.pos < this.bytes.length) {
411
+ const count = this.readUInt8();
412
+ const value = this.readUInt8();
413
+ if (count === 0) {
414
+ if (value === 0) {
415
+ x = 0;
416
+ y += this.bottomUp ? -1 : 1;
417
+ continue;
418
+ }
419
+ if (value === 1) {
420
+ break;
421
+ }
422
+ if (value === 2) {
423
+ x += this.readUInt8();
424
+ y += this.bottomUp ? -this.readUInt8() : this.readUInt8();
425
+ continue;
426
+ }
427
+ for (let i = 0; i < value; i += 1) {
428
+ const idx = this.readUInt8();
429
+ const rgb2 = this.getPaletteColor(idx);
430
+ if (x < this.width && y >= 0 && y < this.height) {
431
+ this.setPixel(y, x, 0, rgb2.blue, rgb2.green, rgb2.red);
432
+ }
433
+ x += 1;
434
+ }
435
+ if ((value & 1) === 1) {
436
+ this.pos += 1;
437
+ }
438
+ continue;
439
+ }
440
+ const rgb = this.getPaletteColor(value);
441
+ for (let i = 0; i < count; i += 1) {
442
+ if (x < this.width && y >= 0 && y < this.height) {
443
+ this.setPixel(y, x, 0, rgb.blue, rgb.green, rgb.red);
444
+ }
445
+ x += 1;
446
+ }
447
+ }
448
+ }
449
+ bit4Rle() {
450
+ this.data.fill(255);
451
+ this.pos = this.offset;
452
+ let x = 0;
453
+ let y = this.bottomUp ? this.height - 1 : 0;
454
+ while (this.pos < this.bytes.length) {
455
+ const count = this.readUInt8();
456
+ const value = this.readUInt8();
457
+ if (count === 0) {
458
+ if (value === 0) {
459
+ x = 0;
460
+ y += this.bottomUp ? -1 : 1;
461
+ continue;
462
+ }
463
+ if (value === 1) {
464
+ break;
465
+ }
466
+ if (value === 2) {
467
+ x += this.readUInt8();
468
+ y += this.bottomUp ? -this.readUInt8() : this.readUInt8();
469
+ continue;
470
+ }
471
+ let current = this.readUInt8();
472
+ for (let i = 0; i < value; i += 1) {
473
+ const nibble = i % 2 === 0 ? (current & 240) >> 4 : current & 15;
474
+ const rgb = this.getPaletteColor(nibble);
475
+ if (x < this.width && y >= 0 && y < this.height) {
476
+ this.setPixel(y, x, 0, rgb.blue, rgb.green, rgb.red);
477
+ }
478
+ x += 1;
479
+ if (i % 2 === 1 && i + 1 < value) {
480
+ current = this.readUInt8();
481
+ }
482
+ }
483
+ if ((value + 1 >> 1 & 1) === 1) {
484
+ this.pos += 1;
485
+ }
486
+ continue;
487
+ }
488
+ for (let i = 0; i < count; i += 1) {
489
+ const nibble = i % 2 === 0 ? (value & 240) >> 4 : value & 15;
490
+ const rgb = this.getPaletteColor(nibble);
491
+ if (x < this.width && y >= 0 && y < this.height) {
492
+ this.setPixel(y, x, 0, rgb.blue, rgb.green, rgb.red);
493
+ }
494
+ x += 1;
425
495
  }
426
496
  }
427
497
  }
@@ -429,92 +499,96 @@ var BmpDecoder = class {
429
499
  return this.data;
430
500
  }
431
501
  };
432
- function decode(bmpData) {
433
- return new BmpDecoder(bmpData);
502
+ function decode(bmpData, options) {
503
+ return new BmpDecoder(bmpData, options);
434
504
  }
435
505
 
436
506
  // src/encoder.ts
507
+ var FILE_HEADER_SIZE2 = 14;
508
+ var INFO_HEADER_SIZE = 40;
509
+ var RGB_TRIPLE_SIZE = 3;
510
+ var BYTES_PER_PIXEL_ABGR = 4;
511
+ function rowStride24(width) {
512
+ const raw = width * RGB_TRIPLE_SIZE;
513
+ return raw + 3 & ~3;
514
+ }
515
+ function normalizeEncodeOptions(qualityOrOptions) {
516
+ if (typeof qualityOrOptions === "number" || typeof qualityOrOptions === "undefined") {
517
+ return {
518
+ orientation: "top-down",
519
+ bitPP: 24
520
+ };
521
+ }
522
+ return {
523
+ orientation: qualityOrOptions.orientation ?? "top-down",
524
+ bitPP: qualityOrOptions.bitPP ?? 24
525
+ };
526
+ }
437
527
  var BmpEncoder = class {
438
- buffer;
528
+ pixelData;
439
529
  width;
440
530
  height;
441
- extraBytes;
442
- rgbSize;
443
- headerInfoSize;
444
- flag = "BM";
445
- reserved = 0;
446
- offset = 54;
447
- fileSize;
448
- planes = 1;
449
- bitPP = 24;
450
- compress = 0;
451
- hr = 0;
452
- vr = 0;
453
- colors = 0;
454
- importantColors = 0;
455
- pos = 0;
456
- constructor(imgData) {
457
- this.buffer = imgData.data;
531
+ options;
532
+ constructor(imgData, options) {
533
+ this.pixelData = imgData.data;
458
534
  this.width = imgData.width;
459
535
  this.height = imgData.height;
460
- this.extraBytes = this.width % 4;
461
- this.rgbSize = this.height * (3 * this.width + this.extraBytes);
462
- this.headerInfoSize = 40;
463
- this.fileSize = this.rgbSize + this.offset;
536
+ this.options = options;
537
+ assertInteger("width", this.width);
538
+ assertInteger("height", this.height);
539
+ if (this.options.bitPP !== 24) {
540
+ throw new Error(
541
+ `Unsupported encode bit depth: ${this.options.bitPP}. Only 24-bit output is supported.`
542
+ );
543
+ }
544
+ const minLength = this.width * this.height * BYTES_PER_PIXEL_ABGR;
545
+ if (this.pixelData.length < minLength) {
546
+ throw new Error(
547
+ `Image data is too short: expected at least ${minLength} bytes for ${this.width}x${this.height} ABGR data.`
548
+ );
549
+ }
464
550
  }
465
551
  encode() {
466
- const tempBuffer = Buffer.alloc(this.offset + this.rgbSize);
467
- tempBuffer.write(this.flag, this.pos, 2);
468
- this.pos += 2;
469
- tempBuffer.writeUInt32LE(this.fileSize, this.pos);
470
- this.pos += 4;
471
- tempBuffer.writeUInt32LE(this.reserved, this.pos);
472
- this.pos += 4;
473
- tempBuffer.writeUInt32LE(this.offset, this.pos);
474
- this.pos += 4;
475
- tempBuffer.writeUInt32LE(this.headerInfoSize, this.pos);
476
- this.pos += 4;
477
- tempBuffer.writeUInt32LE(this.width, this.pos);
478
- this.pos += 4;
479
- tempBuffer.writeInt32LE(-this.height, this.pos);
480
- this.pos += 4;
481
- tempBuffer.writeUInt16LE(this.planes, this.pos);
482
- this.pos += 2;
483
- tempBuffer.writeUInt16LE(this.bitPP, this.pos);
484
- this.pos += 2;
485
- tempBuffer.writeUInt32LE(this.compress, this.pos);
486
- this.pos += 4;
487
- tempBuffer.writeUInt32LE(this.rgbSize, this.pos);
488
- this.pos += 4;
489
- tempBuffer.writeUInt32LE(this.hr, this.pos);
490
- this.pos += 4;
491
- tempBuffer.writeUInt32LE(this.vr, this.pos);
492
- this.pos += 4;
493
- tempBuffer.writeUInt32LE(this.colors, this.pos);
494
- this.pos += 4;
495
- tempBuffer.writeUInt32LE(this.importantColors, this.pos);
496
- this.pos += 4;
497
- let i = 0;
498
- const rowBytes = 3 * this.width + this.extraBytes;
499
- for (let y = 0; y < this.height; y += 1) {
552
+ const stride = rowStride24(this.width);
553
+ const imageSize = stride * this.height;
554
+ const offset = FILE_HEADER_SIZE2 + INFO_HEADER_SIZE;
555
+ const totalSize = offset + imageSize;
556
+ const output = new Uint8Array(totalSize);
557
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
558
+ output[0] = 66;
559
+ output[1] = 77;
560
+ view.setUint32(2, totalSize, true);
561
+ view.setUint32(6, 0, true);
562
+ view.setUint32(10, offset, true);
563
+ view.setUint32(14, INFO_HEADER_SIZE, true);
564
+ view.setInt32(18, this.width, true);
565
+ const signedHeight = this.options.orientation === "top-down" ? -this.height : this.height;
566
+ view.setInt32(22, signedHeight, true);
567
+ view.setUint16(26, 1, true);
568
+ view.setUint16(28, 24, true);
569
+ view.setUint32(30, 0, true);
570
+ view.setUint32(34, imageSize, true);
571
+ view.setUint32(38, 0, true);
572
+ view.setUint32(42, 0, true);
573
+ view.setUint32(46, 0, true);
574
+ view.setUint32(50, 0, true);
575
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
576
+ const srcY = this.options.orientation === "top-down" ? fileRow : this.height - 1 - fileRow;
577
+ const rowStart = offset + fileRow * stride;
500
578
  for (let x = 0; x < this.width; x += 1) {
501
- const p = this.pos + y * rowBytes + x * 3;
502
- i += 1;
503
- tempBuffer[p] = this.buffer.readUInt8(i++);
504
- tempBuffer[p + 1] = this.buffer.readUInt8(i++);
505
- tempBuffer[p + 2] = this.buffer.readUInt8(i++);
506
- }
507
- if (this.extraBytes > 0) {
508
- const fillOffset = this.pos + y * rowBytes + this.width * 3;
509
- tempBuffer.fill(0, fillOffset, fillOffset + this.extraBytes);
579
+ const source = (srcY * this.width + x) * BYTES_PER_PIXEL_ABGR;
580
+ const target = rowStart + x * RGB_TRIPLE_SIZE;
581
+ output[target] = this.pixelData[source + 1] ?? 0;
582
+ output[target + 1] = this.pixelData[source + 2] ?? 0;
583
+ output[target + 2] = this.pixelData[source + 3] ?? 0;
510
584
  }
511
585
  }
512
- return tempBuffer;
586
+ return output;
513
587
  }
514
588
  };
515
- function encode(imgData, quality = 100) {
516
- void quality;
517
- const encoder = new BmpEncoder(imgData);
589
+ function encode(imgData, qualityOrOptions) {
590
+ const options = normalizeEncodeOptions(qualityOrOptions);
591
+ const encoder = new BmpEncoder(imgData, options);
518
592
  const data = encoder.encode();
519
593
  return {
520
594
  data,