diff --git a/CHANGELOG.md b/CHANGELOG.md index f0dd17b2e..bf024b4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ================== ### Added * Warn when building with old, unsupported versions of cairo or libjpeg. +* BMP support 2.0.0 ================== diff --git a/Readme.md b/Readme.md index ed55420ca..6f779535f 100644 --- a/Readme.md +++ b/Readme.md @@ -524,6 +524,8 @@ Examples line in the `examples` directory. Most produce a png image of the same ## License +### node-canvas + (The MIT License) Copyright (c) 2010 LearnBoost, and contributors <dev@learnboost.com> @@ -546,3 +548,7 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### BMP parser + +See [license](src/bmp/LICENSE.md) diff --git a/binding.gyp b/binding.gyp index c0f39f6d2..e770b86c7 100644 --- a/binding.gyp +++ b/binding.gyp @@ -63,6 +63,7 @@ 'src/backend/ImageBackend.cc', 'src/backend/PdfBackend.cc', 'src/backend/SvgBackend.cc', + 'src/bmp/BMPParser.cc', 'src/Backends.cc', 'src/Canvas.cc', 'src/CanvasGradient.cc', diff --git a/src/Image.cc b/src/Image.cc index 0da8eaf0d..a6f69d95a 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -12,6 +12,7 @@ #include "Util.h" #include "Canvas.h" #include "Image.h" +#include "bmp/BMPParser.h" #ifdef HAVE_GIF typedef struct { @@ -313,6 +314,9 @@ Image::loadFromBuffer(uint8_t *buf, unsigned len) { #endif } + if (isBMP(buf, len)) + return loadBMPFromBuffer(buf, len); + this->errorInfo.set("Unsupported image type"); return CAIRO_STATUS_READ_ERROR; } @@ -489,6 +493,9 @@ Image::loadSurface() { #endif } + if (isBMP(buf, 2)) + return loadBMP(stream); + fclose(stream); this->errorInfo.set("Unsupported image type"); @@ -1223,6 +1230,77 @@ Image::loadSVG(FILE *stream) { #endif /* HAVE_RSVG */ +/* + * Load BMP from buffer. + */ + +cairo_status_t Image::loadBMPFromBuffer(uint8_t *buf, unsigned len){ + BMPParser::Parser parser; + + // Reversed ARGB32 with pre-multiplied alpha + uint8_t pixFmt[5] = {2, 1, 0, 3, 1}; + parser.parse(buf, len, pixFmt); + + if (parser.getStatus() != BMPParser::Status::OK) { + errorInfo.reset(); + errorInfo.message = parser.getErrMsg(); + return CAIRO_STATUS_READ_ERROR; + } + + width = naturalWidth = parser.getWidth(); + height = naturalHeight = parser.getHeight(); + uint8_t *data = parser.getImgd(); + + _surface = cairo_image_surface_create_for_data( + data, + CAIRO_FORMAT_ARGB32, + width, + height, + cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width) + ); + + // No need to delete the data + cairo_status_t status = cairo_surface_status(_surface); + if (status) return status; + + _data = data; + parser.clearImgd(); + + return CAIRO_STATUS_SUCCESS; +} + +/* + * Load BMP. + */ + +cairo_status_t Image::loadBMP(FILE *stream){ + struct stat s; + int fd = fileno(stream); + + // Stat + if (fstat(fd, &s) < 0) { + fclose(stream); + return CAIRO_STATUS_READ_ERROR; + } + + uint8_t *buf = new uint8_t[s.st_size]; + + if (!buf) { + fclose(stream); + errorInfo.set(NULL, "malloc", errno); + return CAIRO_STATUS_NO_MEMORY; + } + + size_t read = fread(buf, s.st_size, 1, stream); + fclose(stream); + + cairo_status_t result = CAIRO_STATUS_READ_ERROR; + if (read == 1) result = loadBMPFromBuffer(buf, s.st_size); + delete[] buf; + + return result; +} + /* * Return UNKNOWN, SVG, GIF, JPEG, or PNG based on the filename. */ @@ -1286,3 +1364,18 @@ Image::isSVG(uint8_t *data, unsigned len) { } return false; } + +/* + * Check for valid BMP signatures + */ + +int Image::isBMP(uint8_t *data, unsigned len) { + if(len < 2) return false; + string sig = string(1, (char)data[0]) + (char)data[1]; + return sig == "BM" || + sig == "BA" || + sig == "CI" || + sig == "CP" || + sig == "IC" || + sig == "PT"; +} diff --git a/src/Image.h b/src/Image.h index a1f1ff1bc..8d00b5659 100644 --- a/src/Image.h +++ b/src/Image.h @@ -62,6 +62,7 @@ class Image: public Nan::ObjectWrap { static int isJPEG(uint8_t *data); static int isGIF(uint8_t *data); static int isSVG(uint8_t *data, unsigned len); + static int isBMP(uint8_t *data, unsigned len); static cairo_status_t readPNG(void *closure, unsigned char *data, unsigned len); inline int isComplete(){ return COMPLETE == state; } cairo_surface_t *surface(); @@ -87,6 +88,8 @@ class Image: public Nan::ObjectWrap { cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); #endif + cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); + cairo_status_t loadBMP(FILE *stream); CanvasError errorInfo; void loaded(); cairo_status_t load(); diff --git a/src/bmp/BMPParser.cc b/src/bmp/BMPParser.cc new file mode 100644 index 000000000..d0ae1be1d --- /dev/null +++ b/src/bmp/BMPParser.cc @@ -0,0 +1,383 @@ +#include + +#include "BMPParser.h" + +using namespace std; +using namespace BMPParser; + +#define MAX_IMG_SIZE 10000 + +#define E(cond, msg) if(cond) return setErr(msg) +#define EU(cond, msg) if(cond) return setErrUnsupported(msg) +#define EX(cond, msg) if(cond) return setErrUnknown(msg) + +#define I1() get() +#define U1() get() +#define I2() get() +#define U2() get() +#define I4() get() +#define U4() get() + +#define I1UC() get() +#define U1UC() get() +#define I2UC() get() +#define U2UC() get() +#define I4UC() get() +#define U4UC() get() + +#define CALC_MASK(col) \ + col##Mask = U4(); \ + if(col##Mask == 0xffu) col##Mask = 0; \ + else if(col##Mask == 0xff00u) col##Mask = 8; \ + else if(col##Mask == 0xff0000u) col##Mask = 16; \ + else if(col##Mask == 0xff000000u) col##Mask = 24; \ + else EU(1, #col " mask"); + +#define CHECK_OVERRUN(size, type) \ + if(ptr + (size) - data > len){ \ + setErr("unexpected end of file"); \ + return type(); \ + } + +Parser::~Parser(){ + data = nullptr; + ptr = nullptr; + + if(imgd){ + delete[] imgd; + imgd = nullptr; + } +} + +void Parser::parse(uint8_t *buf, int bufSize, uint8_t *format){ + assert(status == Status::EMPTY); + + data = ptr = buf; + len = bufSize; + + // Start parsing file header + setOp("file header"); + + // File header signature + string fhSig = getStr(2); + string temp = "file header signature"; + EU(fhSig == "BA", temp + " \"BA\""); + EU(fhSig == "CI", temp + " \"CI\""); + EU(fhSig == "CP", temp + " \"CP\""); + EU(fhSig == "IC", temp + " \"IC\""); + EU(fhSig == "PT", temp + " \"PT\""); + EX(fhSig != "BM", temp); // BM + + // Length of the file should be equal to `len` + E(U4() != static_cast(len), "inconsistent file size"); + + // Skip unused values + skip(4); + + // Offset where the pixel array (bitmap data) can be found + auto imgdOffset = U4(); + + // Start parsing DIB header + setOp("DIB header"); + + // Prepare some variables in case they are needed + uint32_t compr = 0; + uint32_t redMask = 0, greenMask = 0, blueMask = 0, alphaMask = 0; + + /** + * Type of the DIB (device-independent bitmap) header + * is determined by its size. Most BMP files use BITMAPINFOHEADER. + */ + auto dibSize = U4(); + temp = "DIB header"; + EU(dibSize == 64, temp + " \"OS22XBITMAPHEADER\""); + EU(dibSize == 16, temp + " \"OS22XBITMAPHEADER\""); + EU(dibSize == 52, temp + " \"BITMAPV2INFOHEADER\""); + EU(dibSize == 56, temp + " \"BITMAPV3INFOHEADER\""); + EU(dibSize == 124, temp + " \"BITMAPV5HEADER\""); + + // BITMAPCOREHEADER, BITMAPINFOHEADER, BITMAPV4HEADER + auto isDibValid = dibSize == 12 || dibSize == 40 || dibSize == 108; + EX(!isDibValid, temp); + + // Image width + w = dibSize == 12 ? U2() : I4(); + E(!w, "image width is 0"); + E(w < 0, "negative image width"); + E(w > MAX_IMG_SIZE, "too large image width"); + + // Image height (specification allows negative values) + h = dibSize == 12 ? U2() : I4(); + E(!h, "image height is 0"); + E(h > MAX_IMG_SIZE, "too large image height"); + + bool isHeightNegative = h < 0; + if(isHeightNegative) h = -h; + + // Number of color planes (must be 1) + E(U2() != 1, "number of color planes must be 1"); + + // Bits per pixel (color depth) + auto bpp = U2(); + auto isBppValid = bpp == 1 || bpp == 24 || bpp == 32; + EU(!isBppValid, "color depth"); + + // Calculate image data size and padding + uint32_t expectedImgdSize = (((w * bpp + 31) >> 5) << 2) * h; + uint32_t rowPadding = (-w * bpp & 31) >> 3; + uint32_t imgdSize = 0; + + if(dibSize == 40 || dibSize == 108){ + // Compression type + compr = U4(); + temp = "compression type"; + EU(compr == 1, temp + " \"BI_RLE8\""); + EU(compr == 2, temp + " \"BI_RLE4\""); + EU(compr == 4, temp + " \"BI_JPEG\""); + EU(compr == 5, temp + " \"BI_PNG\""); + EU(compr == 6, temp + " \"BI_ALPHABITFIELDS\""); + EU(compr == 11, temp + " \"BI_CMYK\""); + EU(compr == 12, temp + " \"BI_CMYKRLE8\""); + EU(compr == 13, temp + " \"BI_CMYKRLE4\""); + + // BI_RGB and BI_BITFIELDS + auto isComprValid = compr == 0 || compr == 3; + EX(!isComprValid, temp); + + // Also ensure that BI_BITFIELDS appears only with BITMAPV4HEADER and 32-bit colors + if(compr == 3){ + E(dibSize != 108, "compression BI_BITFIELDS can be used only with BITMAPV4HEADER"); + E(bpp != 32, "compression BI_BITFIELDS can be used only with 32-bit color depth"); + } + + // Size of the image data + imgdSize = U4(); + + // Horizontal and vertical resolution (ignored) + skip(8); + + // Number of colors in the palette or 0 if no palette is present + auto palColNum = U4(); + EU(palColNum, "non-empty color palette"); + + // Number of important colors used or 0 if all colors are important + auto impCols = U4(); + EU(impCols, "non-zero important colors"); + + // BITMAPV4HEADER has additional properties + if(dibSize == 108){ + // If BI_BITFIELDS are used, calculate masks, otherwise ignore them + if(compr == 3){ + // Convert each mask to bit offset for faster shifting + CALC_MASK(red); + CALC_MASK(green); + CALC_MASK(blue); + CALC_MASK(alpha); + }else{ + skip(16); + } + + // Encure that the color space is LCS_WINDOWS_COLOR_SPACE + string colSpace = getStr(4, 1); + EU(colSpace != "Win ", "color space \"" + colSpace + "\""); + + // The rest 48 bytes are ignored for LCS_WINDOWS_COLOR_SPACE + skip(48); + } + } + + /** + * Skip to the image data. There may be other chunks between, + * but they are optional. + */ + E(ptr - data > imgdOffset, "image data overlaps with another structure"); + ptr = data + imgdOffset; + + // Start parsing image data + setOp("image data"); + + if(!imgdSize){ + // Value 0 is allowed only for BI_RGB compression type + E(compr != 0, "missing image data size"); + imgdSize = expectedImgdSize; + }else{ + E(imgdSize != expectedImgdSize, "inconsistent image data size"); + } + + // Ensure that all image data is present + E(ptr - data + imgdSize > len, "not enough image data"); + + // Direction of reading rows + int yStart = h - 1; + int yEnd = -1; + int dy = isHeightNegative ? 1 : -1; + + // In case of negative height, read rows backward + if(isHeightNegative){ + yStart = 0; + yEnd = h; + } + + // Allocate output image data array + int buffLen = w * h << 2; + imgd = new (nothrow) uint8_t[buffLen]; + E(!imgd, "unable to allocate memory"); + + // Prepare color valus + uint8_t color[4] = {0}; + uint8_t &red = color[0]; + uint8_t &green = color[1]; + uint8_t &blue = color[2]; + uint8_t &alpha = color[3]; + + // Check if pre-multiplied alpha is used + bool premul = format ? format[4] : 0; + + // Main loop + for(int y = yStart; y != yEnd; y += dy){ + // Use in-byte offset for bpp < 8 + uint8_t colOffset = 0; + uint8_t cval = 0; + + for(int x = 0; x != w; x++){ + // Index in the output image data + int i = (x + y * w) << 2; + + switch(compr){ + case 0: // BI_RGB + switch(bpp){ + case 1: + if(colOffset) ptr--; + cval = (U1UC() >> (7 - colOffset)) & 1; + red = green = blue = cval ? 255 : 0; + alpha = 255; + colOffset = (colOffset + 1) & 7; + break; + + case 24: + blue = U1UC(); + green = U1UC(); + red = U1UC(); + alpha = 255; + break; + + case 32: + blue = U1UC(); + green = U1UC(); + red = U1UC(); + alpha = U1UC(); + break; + } + break; + + case 3: // BI_BITFIELDS + auto col = U4UC(); + red = col >> redMask; + green = col >> greenMask; + blue = col >> blueMask; + alpha = col >> alphaMask; + break; + } + + /** + * Pixel format: + * red, + * green, + * blue, + * alpha, + * is alpha pre-multiplied + * Default is [0, 1, 2, 3, 0] + */ + + if(premul && alpha != 255){ + double a = alpha / 255.; + red = static_cast(red * a + .5); + green = static_cast(green * a + .5); + blue = static_cast(blue * a + .5); + } + + if(format){ + imgd[i] = color[format[0]]; + imgd[i + 1] = color[format[1]]; + imgd[i + 2] = color[format[2]]; + imgd[i + 3] = color[format[3]]; + }else{ + imgd[i] = red; + imgd[i + 1] = green; + imgd[i + 2] = blue; + imgd[i + 3] = alpha; + } + } + + // Skip unused bytes in the current row + skip(rowPadding); + } + + if(status == Status::ERROR) + return; + + E(ptr - data != len, "extra data found at the end of file"); + status = Status::OK; +}; + +void Parser::clearImgd(){ imgd = nullptr; } +int32_t Parser::getWidth() const{ return w; } +int32_t Parser::getHeight() const{ return h; } +uint8_t *Parser::getImgd() const{ return imgd; } +Status Parser::getStatus() const{ return status; } + +string Parser::getErrMsg() const{ + return "Error while processing " + getOp() + " - " + err; +} + +template T Parser::get(){ + if(check) + CHECK_OVERRUN(sizeof(T), T); + T val = *(T*)ptr; + ptr += sizeof(T); + return val; +} + +string Parser::getStr(int size, bool reverse){ + CHECK_OVERRUN(size, string); + string val = ""; + + while(size--){ + if(reverse) val = string(1, static_cast(*ptr++)) + val; + else val += static_cast(*ptr++); + } + + return val; +} + +void Parser::skip(int size){ + CHECK_OVERRUN(size, void); + ptr += size; +} + +void Parser::setOp(string val){ + if(status != Status::EMPTY) return; + op = val; +} + +string Parser::getOp() const{ + return op; +} + +void Parser::setErrUnsupported(string msg){ + setErr("unsupported " + msg); +} + +void Parser::setErrUnknown(string msg){ + setErr("unknown " + msg); +} + +void Parser::setErr(string msg){ + if(status != Status::EMPTY) return; + err = msg; + status = Status::ERROR; +} + +string Parser::getErr() const{ + return err; +} diff --git a/src/bmp/BMPParser.h b/src/bmp/BMPParser.h new file mode 100644 index 000000000..86d1465ed --- /dev/null +++ b/src/bmp/BMPParser.h @@ -0,0 +1,60 @@ +#ifndef __NODE_BMP_PARSER_H__ +#define __NODE_BMP_PARSER_H__ + +#ifdef ERROR +#define ERROR_ ERROR +#undef ERROR +#endif + +#include + +namespace BMPParser{ + enum Status{ + EMPTY, + OK, + ERROR, + }; + + class Parser{ + public: + Parser()=default; + ~Parser(); + void parse(uint8_t *buf, int bufSize, uint8_t *format=nullptr); + void clearImgd(); + int32_t getWidth() const; + int32_t getHeight() const; + uint8_t *getImgd() const; + Status getStatus() const; + std::string getErrMsg() const; + + private: + Status status = Status::EMPTY; + uint8_t *data = nullptr; + uint8_t *ptr = nullptr; + int len = 0; + int32_t w = 0; + int32_t h = 0; + uint8_t *imgd = nullptr; + std::string err = ""; + std::string op = ""; + + template T get(); + std::string getStr(int len, bool reverse=false); + void skip(int len); + + void setOp(std::string val); + std::string getOp() const; + + void setErrUnsupported(std::string msg); + void setErrUnknown(std::string msg); + void setErr(std::string msg); + std::string getErr() const; + }; +} + +#ifdef ERROR_ +#define ERROR ERROR_ +#undef ERROR_ +#endif + +#endif diff --git a/src/bmp/LICENSE.md b/src/bmp/LICENSE.md new file mode 100644 index 000000000..ea89a5dfe --- /dev/null +++ b/src/bmp/LICENSE.md @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/test/fixtures/bmp/1-bit.bmp b/test/fixtures/bmp/1-bit.bmp new file mode 100644 index 000000000..04c5f892e Binary files /dev/null and b/test/fixtures/bmp/1-bit.bmp differ diff --git a/test/fixtures/bmp/24-bit.bmp b/test/fixtures/bmp/24-bit.bmp new file mode 100644 index 000000000..4f1f51086 Binary files /dev/null and b/test/fixtures/bmp/24-bit.bmp differ diff --git a/test/fixtures/bmp/32-bit.bmp b/test/fixtures/bmp/32-bit.bmp new file mode 100644 index 000000000..edc32b449 Binary files /dev/null and b/test/fixtures/bmp/32-bit.bmp differ diff --git a/test/fixtures/bmp/bomb.bmp b/test/fixtures/bmp/bomb.bmp new file mode 100644 index 000000000..60703159a Binary files /dev/null and b/test/fixtures/bmp/bomb.bmp differ diff --git a/test/fixtures/bmp/min.bmp b/test/fixtures/bmp/min.bmp new file mode 100644 index 000000000..688af7180 Binary files /dev/null and b/test/fixtures/bmp/min.bmp differ diff --git a/test/fixtures/bmp/negative-height.bmp b/test/fixtures/bmp/negative-height.bmp new file mode 100644 index 000000000..5a0ab3ae3 Binary files /dev/null and b/test/fixtures/bmp/negative-height.bmp differ diff --git a/test/image.test.js b/test/image.test.js index 1162f3cac..8e79cae2f 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -6,18 +6,20 @@ * Module dependencies. */ -const loadImage = require('../').loadImage +const {createCanvas, loadImage} = require('../'); const Image = require('../').Image const assert = require('assert') const assertRejects = require('assert-rejects') const fs = require('fs') +const path = require('path') const png_checkers = `${__dirname}/fixtures/checkers.png` const png_clock = `${__dirname}/fixtures/clock.png` const jpg_chrome = `${__dirname}/fixtures/chrome.jpg` const jpg_face = `${__dirname}/fixtures/face.jpeg` const svg_tree = `${__dirname}/fixtures/tree.svg` +const bmp_dir = `${__dirname}/fixtures/bmp` describe('Image', function () { it('Prototype and ctor are well-shaped, don\'t hit asserts on accessors (GH-803)', function () { @@ -300,4 +302,139 @@ describe('Image', function () { assert.ok(!keys.includes('getSource')); assert.ok(!keys.includes('setSource')); }); + + describe('supports BMP', function () { + it('parses 1-bit image', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 111); + assert.strictEqual(img.height, 72); + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, '1-bit.bmp'); + }); + + it('parses 24-bit image', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 2); + assert.strictEqual(img.height, 2); + + testImgd(img, [ + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + 255, 255, 255, 255, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, '24-bit.bmp'); + }); + + it('parses 32-bit image', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 4); + assert.strictEqual(img.height, 2); + + testImgd(img, [ + 0, 0, 255, 255, + 0, 255, 0, 255, + 255, 0, 0, 255, + 255, 255, 255, 255, + 0, 0, 255, 127, + 0, 255, 0, 127, + 255, 0, 0, 127, + 255, 255, 255, 127, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = fs.readFileSync(path.join(bmp_dir, '32-bit.bmp')); // Also tests loading from buffer + }); + + it('parses minimal BMP', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 1); + assert.strictEqual(img.height, 1); + + testImgd(img, [ + 255, 0, 0, 255, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, 'min.bmp'); + }); + + it('properly handles negative height', function (done) { + let img = new Image(); + + img.onload = () => { + assert.strictEqual(img.width, 1); + assert.strictEqual(img.height, 2); + + testImgd(img, [ + 255, 0, 0, 255, + 0, 255, 0, 255, + ]); + + done(); + }; + + img.onerror = err => { throw err; }; + img.src = path.join(bmp_dir, 'negative-height.bmp'); + }); + + it('catches BMP errors', function (done) { + let img = new Image(); + + img.onload = () => { + throw new Error('Invalid image should not be loaded properly'); + }; + + img.onerror = err => { + let msg = 'Error while processing file header - unexpected end of file'; + assert.strictEqual(err.message, msg); + done(); + }; + + img.src = Buffer.from('BM'); + }); + + it('BMP bomb', function (done) { + let img = new Image(); + + img.onload = () => { + throw new Error('Invalid image should not be loaded properly'); + }; + + img.onerror = err => { + done(); + }; + + img.src = path.join(bmp_dir, 'bomb.bmp'); + }); + + function testImgd(img, data){ + let ctx = createCanvas(img.width, img.height).getContext('2d'); + ctx.drawImage(img, 0, 0); + var actualData = ctx.getImageData(0, 0, img.width, img.height).data; + assert.strictEqual(String(actualData), String(data)); + } + }); })