@huh-david/bmp-js 0.3.0 → 0.4.1

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