402 lines
15 KiB
JavaScript
402 lines
15 KiB
JavaScript
import HeaderTypes from './header-types.js';
|
|
import maskColor from './mask-color.js';
|
|
import { BmpCompression } from './types.js';
|
|
export default class BmpDecoder {
|
|
// Header
|
|
flag;
|
|
fileSize;
|
|
reserved1;
|
|
reserved2;
|
|
offset;
|
|
headerSize;
|
|
width;
|
|
height;
|
|
planes;
|
|
bitPP;
|
|
compression;
|
|
rawSize;
|
|
hr;
|
|
vr;
|
|
colors;
|
|
importantColors;
|
|
palette;
|
|
data;
|
|
maskRed;
|
|
maskGreen;
|
|
maskBlue;
|
|
maskAlpha;
|
|
toRGBA;
|
|
pos;
|
|
bottomUp;
|
|
buffer;
|
|
locRed;
|
|
locGreen;
|
|
locBlue;
|
|
locAlpha;
|
|
shiftRed;
|
|
shiftGreen;
|
|
shiftBlue;
|
|
shiftAlpha;
|
|
constructor(buffer, { toRGBA } = { toRGBA: false }) {
|
|
this.buffer = buffer;
|
|
this.toRGBA = !!toRGBA;
|
|
this.pos = 0;
|
|
this.bottomUp = true;
|
|
this.flag = this.buffer.toString('utf-8', 0, (this.pos += 2));
|
|
if (this.flag !== 'BM') {
|
|
throw new Error('Invalid BMP File');
|
|
}
|
|
this.locRed = this.toRGBA ? 0 : 3;
|
|
this.locGreen = this.toRGBA ? 1 : 2;
|
|
this.locBlue = this.toRGBA ? 2 : 1;
|
|
this.locAlpha = this.toRGBA ? 3 : 0;
|
|
this.parseHeader();
|
|
this.parseRGBA();
|
|
}
|
|
parseHeader() {
|
|
this.fileSize = this.readUInt32LE();
|
|
this.reserved1 = this.buffer.readUInt16LE(this.pos);
|
|
this.pos += 2;
|
|
this.reserved2 = this.buffer.readUInt16LE(this.pos);
|
|
this.pos += 2;
|
|
this.offset = this.readUInt32LE();
|
|
// End of BITMAP_FILE_HEADER
|
|
this.headerSize = this.readUInt32LE();
|
|
if (!(this.headerSize in HeaderTypes)) {
|
|
throw new Error(`Unsupported BMP header size ${this.headerSize}`);
|
|
}
|
|
this.width = this.readUInt32LE();
|
|
this.height = this.readUInt32LE();
|
|
// negative value are possible here => implies bottom down
|
|
this.height =
|
|
this.height > 0x7fffffff ? this.height - 0x100000000 : this.height;
|
|
this.planes = this.buffer.readUInt16LE(this.pos);
|
|
this.pos += 2;
|
|
this.bitPP = this.buffer.readUInt16LE(this.pos);
|
|
this.pos += 2;
|
|
this.compression = this.readUInt32LE();
|
|
this.rawSize = this.readUInt32LE();
|
|
this.hr = this.readUInt32LE();
|
|
this.vr = this.readUInt32LE();
|
|
this.colors = this.readUInt32LE();
|
|
this.importantColors = this.readUInt32LE();
|
|
// De facto defaults
|
|
if (this.bitPP === 32) {
|
|
this.maskAlpha = 0;
|
|
this.maskRed = 0x00ff0000;
|
|
this.maskGreen = 0x0000ff00;
|
|
this.maskBlue = 0x000000ff;
|
|
}
|
|
else if (this.bitPP === 16) {
|
|
this.maskAlpha = 0;
|
|
this.maskRed = 0x7c00;
|
|
this.maskGreen = 0x03e0;
|
|
this.maskBlue = 0x001f;
|
|
}
|
|
// End of BITMAP_INFO_HEADER
|
|
if (this.headerSize > HeaderTypes.BITMAP_INFO_HEADER ||
|
|
this.compression === BmpCompression.BI_BIT_FIELDS ||
|
|
this.compression === BmpCompression.BI_ALPHA_BIT_FIELDS) {
|
|
this.maskRed = this.readUInt32LE();
|
|
this.maskGreen = this.readUInt32LE();
|
|
this.maskBlue = this.readUInt32LE();
|
|
}
|
|
// End of BITMAP_V2_INFO_HEADER
|
|
if (this.headerSize > HeaderTypes.BITMAP_V2_INFO_HEADER ||
|
|
this.compression === BmpCompression.BI_ALPHA_BIT_FIELDS) {
|
|
this.maskAlpha = this.readUInt32LE();
|
|
}
|
|
// End of BITMAP_V3_INFO_HEADER
|
|
if (this.headerSize > HeaderTypes.BITMAP_V3_INFO_HEADER) {
|
|
this.pos +=
|
|
HeaderTypes.BITMAP_V4_HEADER - HeaderTypes.BITMAP_V3_INFO_HEADER;
|
|
}
|
|
// End of BITMAP_V4_HEADER
|
|
if (this.headerSize > HeaderTypes.BITMAP_V4_HEADER) {
|
|
this.pos += HeaderTypes.BITMAP_V5_HEADER - HeaderTypes.BITMAP_V4_HEADER;
|
|
}
|
|
// End of BITMAP_V5_HEADER
|
|
if (this.bitPP <= 8 || this.colors > 0) {
|
|
const len = this.colors === 0 ? 1 << this.bitPP : this.colors;
|
|
this.palette = new Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
const blue = this.buffer.readUInt8(this.pos++);
|
|
const green = this.buffer.readUInt8(this.pos++);
|
|
const red = this.buffer.readUInt8(this.pos++);
|
|
const quad = this.buffer.readUInt8(this.pos++);
|
|
this.palette[i] = {
|
|
red,
|
|
green,
|
|
blue,
|
|
quad,
|
|
};
|
|
}
|
|
}
|
|
// End of color table
|
|
// Can the height ever be negative?
|
|
if (this.height < 0) {
|
|
this.height *= -1;
|
|
this.bottomUp = false;
|
|
}
|
|
const coloShift = maskColor(this.maskRed, this.maskGreen, this.maskBlue, this.maskAlpha);
|
|
this.shiftRed = coloShift.shiftRed;
|
|
this.shiftGreen = coloShift.shiftGreen;
|
|
this.shiftBlue = coloShift.shiftBlue;
|
|
this.shiftAlpha = coloShift.shiftAlpha;
|
|
}
|
|
parseRGBA() {
|
|
this.data = Buffer.alloc(this.width * this.height * 4);
|
|
switch (this.bitPP) {
|
|
case 1:
|
|
this.bit1();
|
|
break;
|
|
case 4:
|
|
this.bit4();
|
|
break;
|
|
case 8:
|
|
this.bit8();
|
|
break;
|
|
case 16:
|
|
this.bit16();
|
|
break;
|
|
case 24:
|
|
this.bit24();
|
|
break;
|
|
default:
|
|
this.bit32();
|
|
}
|
|
}
|
|
bit1() {
|
|
const xLen = Math.ceil(this.width / 8);
|
|
const mode = xLen % 4;
|
|
const padding = mode !== 0 ? 4 - mode : 0;
|
|
let lastLine;
|
|
this.scanImage(padding, xLen, (x, line) => {
|
|
if (line !== lastLine) {
|
|
lastLine = line;
|
|
}
|
|
const b = this.buffer.readUInt8(this.pos++);
|
|
const location = line * this.width * 4 + x * 8 * 4;
|
|
for (let i = 0; i < 8; i++) {
|
|
if (x * 8 + i < this.width) {
|
|
const rgb = this.palette[(b >> (7 - i)) & 0x1];
|
|
this.data[location + i * this.locAlpha] = 0;
|
|
this.data[location + i * 4 + this.locBlue] = rgb.blue;
|
|
this.data[location + i * 4 + this.locGreen] = rgb.green;
|
|
this.data[location + i * 4 + this.locRed] = rgb.red;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
bit4() {
|
|
if (this.compression === BmpCompression.BI_RLE4) {
|
|
this.data.fill(0);
|
|
let lowNibble = false; //for all count of pixel
|
|
let lines = this.bottomUp ? this.height - 1 : 0;
|
|
let location = 0;
|
|
while (location < this.data.length) {
|
|
const a = this.buffer.readUInt8(this.pos++);
|
|
const b = this.buffer.readUInt8(this.pos++);
|
|
//absolute mode
|
|
if (a === 0) {
|
|
if (b === 0) {
|
|
//line end
|
|
lines += this.bottomUp ? -1 : 1;
|
|
location = lines * this.width * 4;
|
|
lowNibble = false;
|
|
continue;
|
|
}
|
|
if (b === 1) {
|
|
// image end
|
|
break;
|
|
}
|
|
if (b === 2) {
|
|
// offset x, y
|
|
const x = this.buffer.readUInt8(this.pos++);
|
|
const y = this.buffer.readUInt8(this.pos++);
|
|
lines += this.bottomUp ? -y : y;
|
|
location += y * this.width * 4 + x * 4;
|
|
}
|
|
else {
|
|
let c = this.buffer.readUInt8(this.pos++);
|
|
for (let i = 0; i < b; i++) {
|
|
location = this.setPixelData(location, lowNibble ? c & 0x0f : (c & 0xf0) >> 4);
|
|
if (i & 1 && i + 1 < b) {
|
|
c = this.buffer.readUInt8(this.pos++);
|
|
}
|
|
lowNibble = !lowNibble;
|
|
}
|
|
if ((((b + 1) >> 1) & 1) === 1) {
|
|
this.pos++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
//encoded mode
|
|
for (let i = 0; i < a; i++) {
|
|
location = this.setPixelData(location, lowNibble ? b & 0x0f : (b & 0xf0) >> 4);
|
|
lowNibble = !lowNibble;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const xLen = Math.ceil(this.width / 2);
|
|
const mode = xLen % 4;
|
|
const padding = mode !== 0 ? 4 - mode : 0;
|
|
this.scanImage(padding, xLen, (x, line) => {
|
|
const b = this.buffer.readUInt8(this.pos++);
|
|
const location = line * this.width * 4 + x * 2 * 4;
|
|
const first4 = b >> 4;
|
|
let rgb = this.palette[first4];
|
|
this.data[location] = 0;
|
|
this.data[location + 1] = rgb.blue;
|
|
this.data[location + 2] = rgb.green;
|
|
this.data[location + 3] = rgb.red;
|
|
if (x * 2 + 1 >= this.width) {
|
|
// throw new Error('Something');
|
|
return false;
|
|
}
|
|
const last4 = b & 0x0f;
|
|
rgb = this.palette[last4];
|
|
this.data[location + 4] = 0;
|
|
this.data[location + 4 + 1] = rgb.blue;
|
|
this.data[location + 4 + 2] = rgb.green;
|
|
this.data[location + 4 + 3] = rgb.red;
|
|
});
|
|
}
|
|
}
|
|
bit8() {
|
|
if (this.compression === BmpCompression.BI_RLE8) {
|
|
this.data.fill(0);
|
|
let lines = this.bottomUp ? this.height - 1 : 0;
|
|
let location = 0;
|
|
while (location < this.data.length) {
|
|
const a = this.buffer.readUInt8(this.pos++);
|
|
const b = this.buffer.readUInt8(this.pos++);
|
|
//absolute mode
|
|
if (a === 0) {
|
|
if (b === 0) {
|
|
//line end
|
|
lines += this.bottomUp ? -1 : 1;
|
|
location = lines * this.width * 4;
|
|
continue;
|
|
}
|
|
if (b === 1) {
|
|
//image end
|
|
break;
|
|
}
|
|
if (b === 2) {
|
|
//offset x,y
|
|
const x = this.buffer.readUInt8(this.pos++);
|
|
const y = this.buffer.readUInt8(this.pos++);
|
|
lines += this.bottomUp ? -y : y;
|
|
location += y * this.width * 4 + x * 4;
|
|
}
|
|
else {
|
|
for (let i = 0; i < b; i++) {
|
|
const c = this.buffer.readUInt8(this.pos++);
|
|
location = this.setPixelData(location, c);
|
|
}
|
|
// @ts-ignore
|
|
const shouldIncrement = b & (1 === 1);
|
|
if (shouldIncrement) {
|
|
this.pos++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
//encoded mode
|
|
for (let i = 0; i < a; i++) {
|
|
location = this.setPixelData(location, b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const mode = this.width % 4;
|
|
const padding = mode !== 0 ? 4 - mode : 0;
|
|
this.scanImage(padding, this.width, (x, line) => {
|
|
const b = this.buffer.readUInt8(this.pos++);
|
|
const location = line * this.width * 4 + x * 4;
|
|
if (b < this.palette.length) {
|
|
const rgb = this.palette[b];
|
|
this.data[location] = 0;
|
|
this.data[location + 1] = rgb.blue;
|
|
this.data[location + 2] = rgb.green;
|
|
this.data[location + 3] = rgb.red;
|
|
}
|
|
else {
|
|
this.data[location] = 0;
|
|
this.data[location + 1] = 0xff;
|
|
this.data[location + 2] = 0xff;
|
|
this.data[location + 3] = 0xff;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
bit16() {
|
|
const padding = (this.width % 2) * 2;
|
|
this.scanImage(padding, this.width, (x, line) => {
|
|
const loc = line * this.width * 4 + x * 4;
|
|
const px = this.buffer.readUInt16LE(this.pos);
|
|
this.pos += 2;
|
|
this.data[loc + this.locRed] = this.shiftRed(px);
|
|
this.data[loc + this.locGreen] = this.shiftGreen(px);
|
|
this.data[loc + this.locBlue] = this.shiftBlue(px);
|
|
this.data[loc + this.locAlpha] = this.shiftAlpha(px);
|
|
});
|
|
}
|
|
bit24() {
|
|
const padding = this.width % 4;
|
|
this.scanImage(padding, this.width, (x, line) => {
|
|
const loc = line * this.width * 4 + x * 4;
|
|
const blue = this.buffer.readUInt8(this.pos++);
|
|
const green = this.buffer.readUInt8(this.pos++);
|
|
const red = this.buffer.readUInt8(this.pos++);
|
|
this.data[loc + this.locRed] = red;
|
|
this.data[loc + this.locGreen] = green;
|
|
this.data[loc + this.locBlue] = blue;
|
|
this.data[loc + this.locAlpha] = 0;
|
|
});
|
|
}
|
|
bit32() {
|
|
this.scanImage(0, this.width, (x, line) => {
|
|
const loc = line * this.width * 4 + x * 4;
|
|
const px = this.readUInt32LE();
|
|
this.data[loc + this.locRed] = this.shiftRed(px);
|
|
this.data[loc + this.locGreen] = this.shiftGreen(px);
|
|
this.data[loc + this.locBlue] = this.shiftBlue(px);
|
|
this.data[loc + this.locAlpha] = this.shiftAlpha(px);
|
|
});
|
|
}
|
|
scanImage(padding = 0, width = this.width, processPixel) {
|
|
for (let y = this.height - 1; y >= 0; y--) {
|
|
const line = this.bottomUp ? y : this.height - 1 - y;
|
|
for (let x = 0; x < width; x++) {
|
|
const result = processPixel.call(this, x, line);
|
|
if (result === false) {
|
|
return;
|
|
}
|
|
}
|
|
this.pos += padding;
|
|
}
|
|
}
|
|
readUInt32LE() {
|
|
const value = this.buffer.readUInt32LE(this.pos);
|
|
this.pos += 4;
|
|
return value;
|
|
}
|
|
setPixelData(location, rgbIndex) {
|
|
const { blue, green, red } = this.palette[rgbIndex];
|
|
this.data[location + this.locAlpha] = 0;
|
|
this.data[location + 1 + this.locBlue] = blue;
|
|
this.data[location + 2 + this.locGreen] = green;
|
|
this.data[location + 3 + this.locRed] = red;
|
|
return location + 4;
|
|
}
|
|
}
|
|
//# sourceMappingURL=decoder.js.map
|