@huh-david/bmp-js 0.4.0 → 0.5.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/README.md CHANGED
@@ -2,12 +2,19 @@
2
2
 
3
3
  A pure TypeScript BMP encoder/decoder for Node.js.
4
4
 
5
+ ## Maintenance
6
+
7
+ This fork is actively maintained and tracks unresolved upstream `shaozilee/bmp-js` issues and PRs.
8
+
9
+ - Repository: https://github.com/Huh-David/bmp-js
10
+ - Latest release: https://github.com/Huh-David/bmp-js/releases/tag/v0.4.0
11
+
5
12
  ## Features
6
13
 
7
14
  - Decoding for BMP bit depths: 1, 4, 8, 15, 16, 24, 32
8
15
  - Decoding support for RLE-4 and RLE-8 compressed BMPs
9
16
  - Robust DIB handling for CORE/INFO/V4/V5 headers
10
- - Encoding output as 24-bit BMP with configurable orientation
17
+ - Encoding output bit depths: 1, 4, 8, 16, 24, 32
11
18
  - Dual package output: ESM + CommonJS
12
19
  - First-class TypeScript types
13
20
 
@@ -50,7 +57,9 @@ const encoded = encode(
50
57
  },
51
58
  {
52
59
  orientation: "bottom-up", // default: "top-down"
53
- bitPP: 24, // only 24 is currently supported
60
+ bitPP: 32, // supported: 1, 4, 8, 16, 24, 32
61
+ // palette is required for 4/8-bit and optional for 1-bit
62
+ // palette: [{ red: 0, green: 0, blue: 0, quad: 0 }, ...],
54
63
  },
55
64
  );
56
65
  ```
@@ -67,9 +76,20 @@ const encoded = bmp.encode(decoded);
67
76
  fs.writeFileSync("./roundtrip.bmp", encoded.data);
68
77
  ```
69
78
 
79
+ ### Decode options
80
+
81
+ ```ts
82
+ import { decode } from "@huh-david/bmp-js";
83
+
84
+ const decoded = decode(inputBytes, {
85
+ toRGBA: true, // return RGBA instead of default ABGR
86
+ });
87
+ ```
88
+
70
89
  ## Data layout
71
90
 
72
- Decoded pixel data is a byte buffer in `ABGR` order.
91
+ Decoded pixel data is a byte buffer in `ABGR` order by default.
92
+ If `toRGBA: true` is provided to `decode`, output is returned in `RGBA`.
73
93
 
74
94
  - `A`: alpha
75
95
  - `B`: blue
package/dist/index.cjs CHANGED
@@ -81,13 +81,15 @@ var BmpDecoder = class {
81
81
  this.bytes = toUint8Array(input);
82
82
  this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
83
83
  this.options = {
84
- treat16BitAs15BitAlpha: options.treat16BitAs15BitAlpha ?? false
84
+ treat16BitAs15BitAlpha: options.treat16BitAs15BitAlpha ?? false,
85
+ toRGBA: options.toRGBA ?? false
85
86
  };
86
87
  this.parseFileHeader();
87
88
  this.parseDibHeader();
88
89
  this.parsePalette();
89
90
  this.pos = this.offset;
90
91
  this.parseRGBA();
92
+ this.transformToRgbaIfNeeded();
91
93
  }
92
94
  ensureReadable(offset, size, context) {
93
95
  if (offset < 0 || size < 0 || offset + size > this.bytes.length) {
@@ -269,6 +271,21 @@ var BmpDecoder = class {
269
271
  throw new Error(`Unsupported BMP bit depth: ${this.bitPP}`);
270
272
  }
271
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
+ }
272
289
  getPaletteColor(index) {
273
290
  const color = this.palette?.[index];
274
291
  if (color) {
@@ -294,7 +311,7 @@ var BmpDecoder = class {
294
311
  const packed = this.readUInt8(rowStart + Math.floor(x / 8));
295
312
  const bit = packed >> 7 - x % 8 & 1;
296
313
  const rgb = this.getPaletteColor(bit);
297
- this.setPixel(destY, x, 0, rgb.blue, rgb.green, rgb.red);
314
+ this.setPixel(destY, x, 255, rgb.blue, rgb.green, rgb.red);
298
315
  }
299
316
  }
300
317
  }
@@ -313,7 +330,7 @@ var BmpDecoder = class {
313
330
  const packed = this.readUInt8(rowStart + Math.floor(x / 2));
314
331
  const idx = x % 2 === 0 ? (packed & 240) >> 4 : packed & 15;
315
332
  const rgb = this.getPaletteColor(idx);
316
- this.setPixel(destY, x, 0, rgb.blue, rgb.green, rgb.red);
333
+ this.setPixel(destY, x, 255, rgb.blue, rgb.green, rgb.red);
317
334
  }
318
335
  }
319
336
  }
@@ -331,7 +348,7 @@ var BmpDecoder = class {
331
348
  for (let x = 0; x < this.width; x += 1) {
332
349
  const idx = this.readUInt8(rowStart + x);
333
350
  const rgb = this.getPaletteColor(idx);
334
- this.setPixel(destY, x, 0, rgb.blue, rgb.green, rgb.red);
351
+ this.setPixel(destY, x, 255, rgb.blue, rgb.green, rgb.red);
335
352
  }
336
353
  }
337
354
  }
@@ -387,7 +404,7 @@ var BmpDecoder = class {
387
404
  const blue = this.scaleMasked(value, this.maskBlue);
388
405
  const green = this.scaleMasked(value, this.maskGreen);
389
406
  const red = this.scaleMasked(value, this.maskRed);
390
- const alpha = this.maskAlpha !== 0 ? this.scaleMasked(value, this.maskAlpha) : 0;
407
+ const alpha = this.maskAlpha !== 0 ? this.scaleMasked(value, this.maskAlpha) : 255;
391
408
  this.setPixel(destY, x, alpha, blue, green, red);
392
409
  }
393
410
  }
@@ -403,7 +420,7 @@ var BmpDecoder = class {
403
420
  const blue = this.readUInt8(base);
404
421
  const green = this.readUInt8(base + 1);
405
422
  const red = this.readUInt8(base + 2);
406
- this.setPixel(destY, x, 0, blue, green, red);
423
+ this.setPixel(destY, x, 255, blue, green, red);
407
424
  }
408
425
  }
409
426
  }
@@ -420,7 +437,7 @@ var BmpDecoder = class {
420
437
  const red = this.scaleMasked(pixel, this.maskRed || 16711680);
421
438
  const green = this.scaleMasked(pixel, this.maskGreen || 65280);
422
439
  const blue = this.scaleMasked(pixel, this.maskBlue || 255);
423
- const alpha = this.maskAlpha === 0 ? 0 : this.scaleMasked(pixel, this.maskAlpha);
440
+ const alpha = this.maskAlpha === 0 ? 255 : this.scaleMasked(pixel, this.maskAlpha);
424
441
  this.setPixel(destY, x, alpha, blue, green, red);
425
442
  } else {
426
443
  const blue = this.readUInt8(base);
@@ -458,7 +475,7 @@ var BmpDecoder = class {
458
475
  const idx = this.readUInt8();
459
476
  const rgb2 = this.getPaletteColor(idx);
460
477
  if (x < this.width && y >= 0 && y < this.height) {
461
- this.setPixel(y, x, 0, rgb2.blue, rgb2.green, rgb2.red);
478
+ this.setPixel(y, x, 255, rgb2.blue, rgb2.green, rgb2.red);
462
479
  }
463
480
  x += 1;
464
481
  }
@@ -470,7 +487,7 @@ var BmpDecoder = class {
470
487
  const rgb = this.getPaletteColor(value);
471
488
  for (let i = 0; i < count; i += 1) {
472
489
  if (x < this.width && y >= 0 && y < this.height) {
473
- this.setPixel(y, x, 0, rgb.blue, rgb.green, rgb.red);
490
+ this.setPixel(y, x, 255, rgb.blue, rgb.green, rgb.red);
474
491
  }
475
492
  x += 1;
476
493
  }
@@ -503,7 +520,7 @@ var BmpDecoder = class {
503
520
  const nibble = i % 2 === 0 ? (current & 240) >> 4 : current & 15;
504
521
  const rgb = this.getPaletteColor(nibble);
505
522
  if (x < this.width && y >= 0 && y < this.height) {
506
- this.setPixel(y, x, 0, rgb.blue, rgb.green, rgb.red);
523
+ this.setPixel(y, x, 255, rgb.blue, rgb.green, rgb.red);
507
524
  }
508
525
  x += 1;
509
526
  if (i % 2 === 1 && i + 1 < value) {
@@ -519,7 +536,7 @@ var BmpDecoder = class {
519
536
  const nibble = i % 2 === 0 ? (value & 240) >> 4 : value & 15;
520
537
  const rgb = this.getPaletteColor(nibble);
521
538
  if (x < this.width && y >= 0 && y < this.height) {
522
- this.setPixel(y, x, 0, rgb.blue, rgb.green, rgb.red);
539
+ this.setPixel(y, x, 255, rgb.blue, rgb.green, rgb.red);
523
540
  }
524
541
  x += 1;
525
542
  }
@@ -536,22 +553,23 @@ function decode(bmpData, options) {
536
553
  // src/encoder.ts
537
554
  var FILE_HEADER_SIZE2 = 14;
538
555
  var INFO_HEADER_SIZE = 40;
539
- var RGB_TRIPLE_SIZE = 3;
540
556
  var BYTES_PER_PIXEL_ABGR = 4;
541
- function rowStride24(width) {
542
- const raw = width * RGB_TRIPLE_SIZE;
543
- return raw + 3 & ~3;
557
+ var SUPPORTED_BIT_DEPTHS = [1, 4, 8, 16, 24, 32];
558
+ function isSupportedBitDepth(value) {
559
+ return SUPPORTED_BIT_DEPTHS.includes(value);
544
560
  }
545
561
  function normalizeEncodeOptions(qualityOrOptions) {
546
562
  if (typeof qualityOrOptions === "number" || typeof qualityOrOptions === "undefined") {
547
563
  return {
548
564
  orientation: "top-down",
549
- bitPP: 24
565
+ bitPP: 24,
566
+ palette: []
550
567
  };
551
568
  }
552
569
  return {
553
570
  orientation: qualityOrOptions.orientation ?? "top-down",
554
- bitPP: qualityOrOptions.bitPP ?? 24
571
+ bitPP: qualityOrOptions.bitPP ?? 24,
572
+ palette: qualityOrOptions.palette ?? []
555
573
  };
556
574
  }
557
575
  var BmpEncoder = class {
@@ -559,16 +577,19 @@ var BmpEncoder = class {
559
577
  width;
560
578
  height;
561
579
  options;
580
+ palette;
581
+ exactPaletteIndex = /* @__PURE__ */ new Map();
562
582
  constructor(imgData, options) {
563
583
  this.pixelData = imgData.data;
564
584
  this.width = imgData.width;
565
585
  this.height = imgData.height;
566
586
  this.options = options;
587
+ this.palette = this.normalizePalette(options);
567
588
  assertInteger("width", this.width);
568
589
  assertInteger("height", this.height);
569
- if (this.options.bitPP !== 24) {
590
+ if (!isSupportedBitDepth(this.options.bitPP)) {
570
591
  throw new Error(
571
- `Unsupported encode bit depth: ${this.options.bitPP}. Only 24-bit output is supported.`
592
+ `Unsupported encode bit depth: ${this.options.bitPP}. Supported: 1, 4, 8, 16, 24, 32.`
572
593
  );
573
594
  }
574
595
  const minLength = this.width * this.height * BYTES_PER_PIXEL_ABGR;
@@ -577,11 +598,204 @@ var BmpEncoder = class {
577
598
  `Image data is too short: expected at least ${minLength} bytes for ${this.width}x${this.height} ABGR data.`
578
599
  );
579
600
  }
601
+ for (let i = 0; i < this.palette.length; i += 1) {
602
+ const color = this.palette[i];
603
+ const key = this.paletteKey(color.quad, color.blue, color.green, color.red);
604
+ if (!this.exactPaletteIndex.has(key)) {
605
+ this.exactPaletteIndex.set(key, i);
606
+ }
607
+ }
608
+ }
609
+ normalizePalette(options) {
610
+ if (options.bitPP === 1) {
611
+ const palette = options.palette.length ? options.palette : [
612
+ { red: 255, green: 255, blue: 255, quad: 0 },
613
+ { red: 0, green: 0, blue: 0, quad: 0 }
614
+ ];
615
+ this.validatePalette(options.bitPP, palette);
616
+ return palette;
617
+ }
618
+ if (options.bitPP === 4 || options.bitPP === 8) {
619
+ if (options.palette.length === 0) {
620
+ throw new Error(`Encoding ${options.bitPP}-bit BMP requires a non-empty palette.`);
621
+ }
622
+ this.validatePalette(options.bitPP, options.palette);
623
+ return options.palette;
624
+ }
625
+ return [];
626
+ }
627
+ validatePalette(bitPP, palette) {
628
+ const maxSize = 1 << bitPP;
629
+ if (palette.length === 0 || palette.length > maxSize) {
630
+ throw new Error(
631
+ `Palette size ${palette.length} is invalid for ${bitPP}-bit BMP. Expected 1..${maxSize}.`
632
+ );
633
+ }
634
+ for (const color of palette) {
635
+ this.validateChannel("palette.red", color.red);
636
+ this.validateChannel("palette.green", color.green);
637
+ this.validateChannel("palette.blue", color.blue);
638
+ this.validateChannel("palette.quad", color.quad);
639
+ }
640
+ }
641
+ validateChannel(name, value) {
642
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
643
+ throw new Error(`${name} must be an integer between 0 and 255.`);
644
+ }
645
+ }
646
+ rowStride() {
647
+ return Math.floor((this.options.bitPP * this.width + 31) / 32) * 4;
648
+ }
649
+ sourceY(fileRow) {
650
+ return this.options.orientation === "top-down" ? fileRow : this.height - 1 - fileRow;
651
+ }
652
+ sourceOffset(x, y) {
653
+ return (y * this.width + x) * BYTES_PER_PIXEL_ABGR;
654
+ }
655
+ paletteKey(alpha, blue, green, red) {
656
+ return ((alpha & 255) << 24 | (blue & 255) << 16 | (green & 255) << 8 | red & 255) >>> 0;
657
+ }
658
+ findPaletteIndex(a, b, g, r) {
659
+ const exact = this.exactPaletteIndex.get(this.paletteKey(a, b, g, r));
660
+ if (exact !== void 0) {
661
+ return exact;
662
+ }
663
+ let bestIndex = 0;
664
+ let bestDistance = Number.POSITIVE_INFINITY;
665
+ for (let i = 0; i < this.palette.length; i += 1) {
666
+ const color = this.palette[i];
667
+ const dr = color.red - r;
668
+ const dg = color.green - g;
669
+ const db = color.blue - b;
670
+ const da = color.quad - a;
671
+ const distance = dr * dr + dg * dg + db * db + da * da;
672
+ if (distance < bestDistance) {
673
+ bestDistance = distance;
674
+ bestIndex = i;
675
+ }
676
+ }
677
+ return bestIndex;
678
+ }
679
+ writePalette(output, paletteOffset) {
680
+ for (let i = 0; i < this.palette.length; i += 1) {
681
+ const color = this.palette[i];
682
+ const base = paletteOffset + i * 4;
683
+ output[base] = color.blue;
684
+ output[base + 1] = color.green;
685
+ output[base + 2] = color.red;
686
+ output[base + 3] = color.quad;
687
+ }
688
+ }
689
+ encode1Bit(output, pixelOffset, stride) {
690
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
691
+ const srcY = this.sourceY(fileRow);
692
+ const rowStart = pixelOffset + fileRow * stride;
693
+ for (let x = 0; x < this.width; x += 8) {
694
+ let packed = 0;
695
+ for (let bit = 0; bit < 8; bit += 1) {
696
+ const px = x + bit;
697
+ if (px >= this.width) {
698
+ break;
699
+ }
700
+ const source = this.sourceOffset(px, srcY);
701
+ const a = this.pixelData[source] ?? 255;
702
+ const b = this.pixelData[source + 1] ?? 0;
703
+ const g = this.pixelData[source + 2] ?? 0;
704
+ const r = this.pixelData[source + 3] ?? 0;
705
+ const idx = this.findPaletteIndex(a, b, g, r) & 1;
706
+ packed |= idx << 7 - bit;
707
+ }
708
+ output[rowStart + Math.floor(x / 8)] = packed;
709
+ }
710
+ }
711
+ }
712
+ encode4Bit(output, pixelOffset, stride) {
713
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
714
+ const srcY = this.sourceY(fileRow);
715
+ const rowStart = pixelOffset + fileRow * stride;
716
+ for (let x = 0; x < this.width; x += 2) {
717
+ const sourceA = this.sourceOffset(x, srcY);
718
+ const idxA = this.findPaletteIndex(
719
+ this.pixelData[sourceA] ?? 255,
720
+ this.pixelData[sourceA + 1] ?? 0,
721
+ this.pixelData[sourceA + 2] ?? 0,
722
+ this.pixelData[sourceA + 3] ?? 0
723
+ );
724
+ let idxB = 0;
725
+ if (x + 1 < this.width) {
726
+ const sourceB = this.sourceOffset(x + 1, srcY);
727
+ idxB = this.findPaletteIndex(
728
+ this.pixelData[sourceB] ?? 255,
729
+ this.pixelData[sourceB + 1] ?? 0,
730
+ this.pixelData[sourceB + 2] ?? 0,
731
+ this.pixelData[sourceB + 3] ?? 0
732
+ );
733
+ }
734
+ output[rowStart + Math.floor(x / 2)] = (idxA & 15) << 4 | idxB & 15;
735
+ }
736
+ }
737
+ }
738
+ encode8Bit(output, pixelOffset, stride) {
739
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
740
+ const srcY = this.sourceY(fileRow);
741
+ const rowStart = pixelOffset + fileRow * stride;
742
+ for (let x = 0; x < this.width; x += 1) {
743
+ const source = this.sourceOffset(x, srcY);
744
+ output[rowStart + x] = this.findPaletteIndex(
745
+ this.pixelData[source] ?? 255,
746
+ this.pixelData[source + 1] ?? 0,
747
+ this.pixelData[source + 2] ?? 0,
748
+ this.pixelData[source + 3] ?? 0
749
+ );
750
+ }
751
+ }
752
+ }
753
+ encode16Bit(output, view, pixelOffset, stride) {
754
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
755
+ const srcY = this.sourceY(fileRow);
756
+ const rowStart = pixelOffset + fileRow * stride;
757
+ for (let x = 0; x < this.width; x += 1) {
758
+ const source = this.sourceOffset(x, srcY);
759
+ const b = this.pixelData[source + 1] ?? 0;
760
+ const g = this.pixelData[source + 2] ?? 0;
761
+ const r = this.pixelData[source + 3] ?? 0;
762
+ const value = (r >> 3 & 31) << 10 | (g >> 3 & 31) << 5 | b >> 3 & 31;
763
+ view.setUint16(rowStart + x * 2, value, true);
764
+ }
765
+ }
766
+ }
767
+ encode24Bit(output, pixelOffset, stride) {
768
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
769
+ const srcY = this.sourceY(fileRow);
770
+ const rowStart = pixelOffset + fileRow * stride;
771
+ for (let x = 0; x < this.width; x += 1) {
772
+ const source = this.sourceOffset(x, srcY);
773
+ const target = rowStart + x * 3;
774
+ output[target] = this.pixelData[source + 1] ?? 0;
775
+ output[target + 1] = this.pixelData[source + 2] ?? 0;
776
+ output[target + 2] = this.pixelData[source + 3] ?? 0;
777
+ }
778
+ }
779
+ }
780
+ encode32Bit(output, pixelOffset, stride) {
781
+ for (let fileRow = 0; fileRow < this.height; fileRow += 1) {
782
+ const srcY = this.sourceY(fileRow);
783
+ const rowStart = pixelOffset + fileRow * stride;
784
+ for (let x = 0; x < this.width; x += 1) {
785
+ const source = this.sourceOffset(x, srcY);
786
+ const target = rowStart + x * 4;
787
+ output[target] = this.pixelData[source + 1] ?? 0;
788
+ output[target + 1] = this.pixelData[source + 2] ?? 0;
789
+ output[target + 2] = this.pixelData[source + 3] ?? 0;
790
+ output[target + 3] = this.pixelData[source] ?? 255;
791
+ }
792
+ }
580
793
  }
581
794
  encode() {
582
- const stride = rowStride24(this.width);
795
+ const stride = this.rowStride();
583
796
  const imageSize = stride * this.height;
584
- const offset = FILE_HEADER_SIZE2 + INFO_HEADER_SIZE;
797
+ const paletteSize = this.palette.length * 4;
798
+ const offset = FILE_HEADER_SIZE2 + INFO_HEADER_SIZE + paletteSize;
585
799
  const totalSize = offset + imageSize;
586
800
  const output = new Uint8Array(totalSize);
587
801
  const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
@@ -595,23 +809,35 @@ var BmpEncoder = class {
595
809
  const signedHeight = this.options.orientation === "top-down" ? -this.height : this.height;
596
810
  view.setInt32(22, signedHeight, true);
597
811
  view.setUint16(26, 1, true);
598
- view.setUint16(28, 24, true);
812
+ view.setUint16(28, this.options.bitPP, true);
599
813
  view.setUint32(30, 0, true);
600
814
  view.setUint32(34, imageSize, true);
601
815
  view.setUint32(38, 0, true);
602
816
  view.setUint32(42, 0, true);
603
- view.setUint32(46, 0, true);
817
+ view.setUint32(46, this.palette.length, true);
604
818
  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;
608
- for (let x = 0; x < this.width; x += 1) {
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;
614
- }
819
+ if (this.palette.length > 0) {
820
+ this.writePalette(output, FILE_HEADER_SIZE2 + INFO_HEADER_SIZE);
821
+ }
822
+ switch (this.options.bitPP) {
823
+ case 1:
824
+ this.encode1Bit(output, offset, stride);
825
+ break;
826
+ case 4:
827
+ this.encode4Bit(output, offset, stride);
828
+ break;
829
+ case 8:
830
+ this.encode8Bit(output, offset, stride);
831
+ break;
832
+ case 16:
833
+ this.encode16Bit(output, view, offset, stride);
834
+ break;
835
+ case 24:
836
+ this.encode24Bit(output, offset, stride);
837
+ break;
838
+ case 32:
839
+ this.encode32Bit(output, offset, stride);
840
+ break;
615
841
  }
616
842
  return output;
617
843
  }