Skip to content

Commit cd601b7

Browse files
committed
feat: support BMP image compression #181
1 parent 67b80f7 commit cd601b7

File tree

3 files changed

+194
-0
lines changed

3 files changed

+194
-0
lines changed

lib/canvastobmp.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// https://github.com/marcosvega91/canvas-to-bmp/blob/77aaf2221647a6533b1926cb637c7cd2bc432d9b/src/canvastobmp.js
2+
3+
/**
4+
* Static helper object that can convert a CORS-compliant canvas element
5+
* to a 32-bits BMP file (buffer, Blob and data-URI).
6+
*
7+
* @type {{toArrayBuffer: Function, toBlob: Function, toDataURL: Function}}
8+
* @namespace
9+
*/
10+
const CanvasToBMP = {
11+
12+
/**
13+
* Convert a canvas element to ArrayBuffer containing a BMP file
14+
* with support for 32-bit format (alpha). The call is asynchronous
15+
* so a callback must be provided.
16+
*
17+
* Note that CORS requirement must be fulfilled.
18+
*
19+
* @param {HTMLCanvasElement} canvas - the canvas element to convert
20+
* @param {function} callback - called when conversion is done. Argument is ArrayBuffer
21+
* @static
22+
*/
23+
toArrayBuffer(canvas, callback) {
24+
const w = canvas.width;
25+
const h = canvas.height;
26+
const w4 = w << 2;
27+
const idata = canvas.getContext('2d').getImageData(0, 0, w, h);
28+
const data32 = new Uint32Array(idata.data.buffer);
29+
30+
const stride = ((32 * w + 31) / 32) << 2;
31+
const pixelArraySize = stride * h;
32+
const fileLength = 122 + pixelArraySize;
33+
34+
const file = new ArrayBuffer(fileLength);
35+
const view = new DataView(file);
36+
const blockSize = 1 << 20;
37+
let block = blockSize;
38+
let y = 0; let x; let v; let a; let pos = 0; let p; let
39+
s = 0;
40+
41+
// Header
42+
set16(0x4d42); // BM
43+
set32(fileLength); // total length
44+
seek(4); // skip unused fields
45+
set32(0x7a); // offset to pixels
46+
47+
// DIB header
48+
set32(0x6c); // header size (108)
49+
set32(w);
50+
set32(-h >>> 0); // negative = top-to-bottom
51+
set16(1); // 1 plane
52+
set16(32); // 32-bits (RGBA)
53+
set32(3); // no compression (BI_BITFIELDS, 3)
54+
set32(pixelArraySize); // bitmap size incl. padding (stride x height)
55+
set32(2835); // pixels/meter h (~72 DPI x 39.3701 inch/m)
56+
set32(2835); // pixels/meter v
57+
seek(8); // skip color/important colors
58+
set32(0xff0000); // red channel mask
59+
set32(0xff00); // green channel mask
60+
set32(0xff); // blue channel mask
61+
set32(0xff000000); // alpha channel mask
62+
set32(0x57696e20); // " win" color space
63+
64+
(function convert() {
65+
// bitmap data, change order of ABGR to BGRA (msb-order)
66+
while (y < h && block > 0) {
67+
p = 0x7a + y * stride; // offset + stride x height
68+
x = 0;
69+
70+
while (x < w4) {
71+
block--;
72+
v = data32[s++]; // get ABGR
73+
a = v >>> 24; // alpha
74+
view.setUint32(p + x, (v << 8) | a); // set BGRA (msb order)
75+
x += 4;
76+
}
77+
y++;
78+
}
79+
80+
if (s < data32.length) {
81+
block = blockSize;
82+
setTimeout(convert, CanvasToBMP._dly);
83+
} else callback(file);
84+
}());
85+
86+
// helper method to move current buffer position
87+
function set16(data) {
88+
view.setUint16(pos, data, true);
89+
pos += 2;
90+
}
91+
92+
function set32(data) {
93+
view.setUint32(pos, data, true);
94+
pos += 4;
95+
}
96+
97+
function seek(delta) { pos += delta; }
98+
},
99+
100+
/**
101+
* Converts a canvas to BMP file, returns a Blob representing the
102+
* file. This can be used with URL.createObjectURL(). The call is
103+
* asynchronous so a callback must be provided.
104+
*
105+
* Note that CORS requirement must be fulfilled.
106+
*
107+
* @param {HTMLCanvasElement} canvas - the canvas element to convert
108+
* @param {function} callback - called when conversion is done. Argument is a Blob
109+
* @static
110+
*/
111+
toBlob(canvas, callback) {
112+
this.toArrayBuffer(canvas, (file) => {
113+
callback(new Blob([file], { type: 'image/bmp' }));
114+
});
115+
},
116+
117+
// /**
118+
// * Converts a canvas to BMP file, returns an ObjectURL (for Blob)
119+
// * representing the file. The call is asynchronous so a callback
120+
// * must be provided.
121+
// *
122+
// * **Important**: To avoid memory-leakage you must revoke the returned
123+
// * ObjectURL when no longer needed:
124+
// *
125+
// * var _URL = self.URL || self.webkitURL || self;
126+
// * _URL.revokeObjectURL(url);
127+
// *
128+
// * Note that CORS requirement must be fulfilled.
129+
// *
130+
// * @param {HTMLCanvasElement} canvas - the canvas element to convert
131+
// * @param {function} callback - called when conversion is done. Argument is a Blob
132+
// * @static
133+
// */
134+
// toObjectURL(canvas, callback) {
135+
// this.toBlob(canvas, (blob) => {
136+
// const url = self.URL || self.webkitURL || self;
137+
// callback(url.createObjectURL(blob));
138+
// });
139+
// },
140+
141+
// /**
142+
// * Converts the canvas to a data-URI representing a BMP file. The
143+
// * call is asynchronous so a callback must be provided.
144+
// *
145+
// * Note that CORS requirement must be fulfilled.
146+
// *
147+
// * @param {HTMLCanvasElement} canvas - the canvas element to convert
148+
// * @param {function} callback - called when conversion is done. Argument is an data-URI (string)
149+
// * @static
150+
// */
151+
// toDataURL(canvas, callback) {
152+
// this.toArrayBuffer(canvas, (file) => {
153+
// const buffer = new Uint8Array(file);
154+
// const blockSize = 1 << 20;
155+
// let block = blockSize;
156+
// let bs = ''; let base64 = ''; let i = 0; let
157+
// l = buffer.length;
158+
159+
// // This is a necessary step before we can use btoa. We can
160+
// // replace this later with a direct byte-buffer to Base-64 routine.
161+
// // Will do for now, impacts only with very large bitmaps (in which
162+
// // case toBlob should be used).
163+
// (function prepBase64() {
164+
// while (i < l && block-- > 0) bs += String.fromCharCode(buffer[i++]);
165+
166+
// if (i < l) {
167+
// block = blockSize;
168+
// setTimeout(prepBase64, CanvasToBMP._dly);
169+
// } else {
170+
// // convert string to Base-64
171+
// i = 0;
172+
// l = bs.length;
173+
// block = 180000; // must be divisible by 3
174+
175+
// (function toBase64() {
176+
// base64 += btoa(bs.substr(i, block));
177+
// i += block;
178+
// (i < l)
179+
// ? setTimeout(toBase64, CanvasToBMP._dly)
180+
// : callback(`data:image/bmp;base64,${base64}`);
181+
// }());
182+
// }
183+
// }());
184+
// });
185+
// },
186+
_dly: 9, // delay for async operations
187+
};
188+
export default CanvasToBMP;

lib/image-compression.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default async function compress(file, options, previousProgress = 0) {
7878
const origExceedMaxSize = tempFile.size > maxSizeByte;
7979
const sizeBecomeLarger = tempFile.size > file.size;
8080
if (process.env.BUILD === 'development') {
81+
console.log('outputFileType', outputFileType);
8182
console.log('original file size', file.size);
8283
console.log('current file size', tempFile.size);
8384
}

lib/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import UPNG from './UPNG';
2+
import CanvasToBMP from './canvastobmp';
23
import MAX_CANVAS_SIZE from './config/max-canvas-size';
34
import BROWSER_NAME from './config/browser-name';
45

@@ -264,6 +265,10 @@ export async function canvasToFile(canvas, fileType, fileName, fileLastModified,
264265
file = new Blob([png], { type: fileType });
265266
file.name = fileName;
266267
file.lastModified = fileLastModified;
268+
} else if (fileType === 'image/bmp') {
269+
file = await new Promise((resolve) => CanvasToBMP.toBlob(canvas, resolve));
270+
file.name = fileName;
271+
file.lastModified = fileLastModified;
267272
} else if (typeof OffscreenCanvas === 'function' && canvas instanceof OffscreenCanvas) { // checked on Win Chrome 83, MacOS Chrome 83
268273
file = await canvas.convertToBlob({ type: fileType, quality });
269274
file.name = fileName;

0 commit comments

Comments
 (0)