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