@3846masa/bmp
Version:
Create a BMP (w/ alpha channel) binary from RGBA raw bytes like ImageData.
103 lines (85 loc) • 3.06 kB
JavaScript
const workerScript = () => {
const BMP_HEADER_BASE64 =
'Qk0AAAAAAAAAAHoAAABsAAAAAAAAAAAAAAABACAAAwAAAAAAAADDDgAAww4AAAAAAAAAAAAA/wAAAAD/AAAAAP8AAAAA/0JHUnM';
const BMP_HEADER = Uint8Array.from(atob(BMP_HEADER_BASE64), (c) => c.charCodeAt(0));
const BMP_HEADER_LENGTH = 122;
const BMP_FILESIZE_OFFSET = 2;
const BMP_WIDTH_OFFSET = 18;
const BMP_HEIGHT_OFFSET = 22;
const BMP_IMAGESIZE_OFFSET = 34;
const BMP_RED_BITFIELDS_OFFSET = 54;
const BMP_GREEN_BITFIELDS_OFFSET = 62;
const IS_WIN = 'navigator' in globalThis && /Trident|Edge/.test(navigator.userAgent);
/**
* @typedef Options
* @type {Object}
* @property {boolean} strict
*/
/**
* @param {ImageData} imageData
* @param {Options} [options]
* @returns {Uint8Array}
*/
const convert = ({ width, height, data }, _options) => {
const options = Object.assign({ strict: false }, _options);
const dataLength = data.byteLength;
const fileSize = BMP_HEADER_LENGTH + dataLength;
const uint8Array = new Uint8Array(fileSize);
const dataView = new DataView(uint8Array.buffer);
const setUint32 = (offset, value) => dataView.setUint32(offset, value, true);
uint8Array.set(BMP_HEADER);
setUint32(BMP_FILESIZE_OFFSET, fileSize);
setUint32(BMP_WIDTH_OFFSET, width);
setUint32(BMP_HEIGHT_OFFSET, -height);
setUint32(BMP_IMAGESIZE_OFFSET, dataLength);
uint8Array.set(data, BMP_HEADER_LENGTH);
if (options.strict || IS_WIN) {
// RGBA -> BGRA
setUint32(BMP_RED_BITFIELDS_OFFSET, 0x00ff0000);
setUint32(BMP_GREEN_BITFIELDS_OFFSET, 0x000000ff);
for (let offset = 0; offset < dataLength; offset += 4) {
uint8Array[BMP_HEADER_LENGTH + offset] = data[offset + 2];
uint8Array[BMP_HEADER_LENGTH + 2 + offset] = data[offset];
}
}
return uint8Array;
};
onmessage = ({ data: [key, width, height, buffer, options] }) => {
try {
const result = convert({ width, height, data: new Uint8Array(buffer) }, options);
postMessage([key, result.buffer], [result.buffer]);
} catch (err) {
postMessage([key, undefined, err]);
}
};
};
const workerUrl = URL.createObjectURL(new Blob([`(${workerScript})()`]));
const worker = new Worker(workerUrl);
const callbackStore = new Map();
worker.onmessage = ({ data: [key, arrayBuffer, errObj] }) => {
const [resolve, reject] = callbackStore.get(key);
callbackStore.delete(key);
if (arrayBuffer) {
resolve(new Uint8Array(arrayBuffer));
} else {
reject(Object.assign(new Error(), errObj));
}
};
/**
* @typedef Options
* @type {Object}
* @property {boolean} strict
*/
/**
* @param {ImageData} imageData
* @param {Options} [options]
* @returns {Promise<Uint8Array>}
*/
const convert = ({ width, height, data }, options) => {
return new Promise((resolve, reject) => {
const key = '' + Date.now() + Math.random();
callbackStore.set(key, [resolve, reject]);
worker.postMessage([key, width, height, data.buffer, options], [data.buffer]);
});
};
export { convert };