diff --git a/CHANGELOG.md b/CHANGELOG.md index 189d88d00..16a7779a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ canvas.createJPEGStream() // new * Provide better, Node.js core-style coded errors for failed sys calls. (For example, provide an error with code 'ENOENT' if setting `img.src` to a path that does not exist.) + * Support reading CMYK, YCCK JPEGs. ### Added * Prebuilds (#992) with different libc versions to the prebuilt binary (#1140) diff --git a/src/Image.cc b/src/Image.cc index 997ff081f..12880642e 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -200,8 +200,8 @@ Image::clearData() { _surface = NULL; } - free(_data); - _data = NULL; + delete[] _data; + _data = nullptr; free(filename); filename = NULL; @@ -356,7 +356,7 @@ Image::readPNG(void *c, uint8_t *data, unsigned int len) { Image::Image() { filename = NULL; - _data = NULL; + _data = nullptr; _data_len = 0; _surface = NULL; width = height = 0; @@ -613,7 +613,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { return CAIRO_STATUS_INVALID_SIZE; } - uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; if (!data) { GIF_CLOSE_FILE(gif); this->errorInfo.set(NULL, "malloc", errno); @@ -719,7 +719,7 @@ Image::loadGIFFromBuffer(uint8_t *buf, unsigned len) { cairo_status_t status = cairo_surface_status(_surface); if (status) { - free(data); + delete[] data; return status; } @@ -777,6 +777,18 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { #endif +void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { + int stride = naturalWidth * 4; + for (int y = 0; y < naturalHeight; ++y) { + jpeg_read_scanlines(args, &src, 1); + uint32_t *row = (uint32_t*)(data + stride * y); + for (int x = 0; x < naturalWidth; ++x) { + int bx = args->output_components * x; + row[x] = decode(src + bx); + } + } +} + /* * Takes an initialised jpeg_decompress_struct and decodes the * data into _surface. @@ -784,10 +796,9 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { cairo_status_t Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { - int stride = naturalWidth * 4; - cairo_status_t status; - - uint8_t *data = (uint8_t *) malloc(naturalWidth * naturalHeight * 4); + cairo_status_t status = CAIRO_STATUS_SUCCESS; + + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); @@ -795,7 +806,7 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_NO_MEMORY; } - uint8_t *src = (uint8_t *) malloc(naturalWidth * args->output_components); + uint8_t *src = new uint8_t[naturalWidth * args->output_components]; if (!src) { free(data); jpeg_abort_decompress(args); @@ -804,46 +815,56 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { return CAIRO_STATUS_NO_MEMORY; } - for (int y = 0; y < naturalHeight; ++y) { - jpeg_read_scanlines(args, &src, 1); - uint32_t *row = (uint32_t *)(data + stride * y); - for (int x = 0; x < naturalWidth; ++x) { - if (args->jpeg_color_space == 1) { - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[x] << 16 - | src[x] << 8 - | src[x]; - } else { - int bx = 3 * x; - uint32_t *pixel = row + x; - *pixel = 255 << 24 - | src[bx + 0] << 16 - | src[bx + 1] << 8 - | src[bx + 2]; - } - } + // These are the three main cases to handle. libjpeg converts YCCK to CMYK + // and YCbCr to RGB by default. + switch (args->out_color_space) { + case JCS_CMYK: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint16_t k = static_cast(src[3]); + uint8_t r = k * src[0] / 255; + uint8_t g = k * src[1] / 255; + uint8_t b = k * src[2] / 255; + return 255 << 24 | r << 16 | g << 8 | b; + }); + break; + case JCS_RGB: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint8_t r = src[0], g = src[1], b = src[2]; + return 255 << 24 | r << 16 | g << 8 | b; + }); + break; + case JCS_GRAYSCALE: + jpegToARGB(args, data, src, [](uint8_t const* src) { + uint8_t v = src[0]; + return 255 << 24 | v << 16 | v << 8 | v; + }); + break; + default: + this->errorInfo.set("Unsupported JPEG encoding"); + status = CAIRO_STATUS_READ_ERROR; + break; } - _surface = cairo_image_surface_create_for_data( - data - , CAIRO_FORMAT_ARGB32 - , naturalWidth - , naturalHeight - , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); + if (!status) { + _surface = cairo_image_surface_create_for_data( + data + , CAIRO_FORMAT_ARGB32 + , naturalWidth + , naturalHeight + , cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, naturalWidth)); + } jpeg_finish_decompress(args); jpeg_destroy_decompress(args); status = cairo_surface_status(_surface); + delete[] src; + if (status) { - free(data); - free(src); + delete[] data; return status; } - free(src); - _data = data; return CAIRO_STATUS_SUCCESS; @@ -908,7 +929,7 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { // Data alloc // 8 pixels per byte using Alpha Channel format to reduce memory requirement. int buf_size = naturalHeight * cairo_format_stride_for_width(CAIRO_FORMAT_A1, naturalWidth); - uint8_t *data = (uint8_t *) malloc(buf_size); + uint8_t *data = new uint8_t[buf_size]; if (!data) { this->errorInfo.set(NULL, "malloc", errno); return CAIRO_STATUS_NO_MEMORY; @@ -928,7 +949,7 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { cairo_status_t status = cairo_surface_status(_surface); if (status) { - free(data); + delete[] data; return status; } diff --git a/src/Image.h b/src/Image.h index 2dcbd4759..585067c48 100644 --- a/src/Image.h +++ b/src/Image.h @@ -10,6 +10,7 @@ #include "Canvas.h" #include "CanvasError.h" +#include #ifdef HAVE_JPEG #include @@ -34,7 +35,7 @@ #endif #endif - +using JPEGDecodeL = std::function; class Image: public Nan::ObjectWrap { public: @@ -81,6 +82,7 @@ class Image: public Nan::ObjectWrap { #ifdef HAVE_JPEG cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadJPEG(FILE *stream); + void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); #if CAIRO_VERSION_MINOR >= 10 cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); @@ -115,7 +117,7 @@ class Image: public Nan::ObjectWrap { private: cairo_surface_t *_surface; - uint8_t *_data; + uint8_t *_data = nullptr; int _data_len; #ifdef HAVE_RSVG RsvgHandle *_rsvg; diff --git a/test/fixtures/grayscale.jpg b/test/fixtures/grayscale.jpg new file mode 100644 index 000000000..4988e9e55 Binary files /dev/null and b/test/fixtures/grayscale.jpg differ diff --git a/test/fixtures/ycck.jpg b/test/fixtures/ycck.jpg new file mode 100644 index 000000000..0fbcd3b0c Binary files /dev/null and b/test/fixtures/ycck.jpg differ diff --git a/test/public/tests.js b/test/public/tests.js index 9120e293e..bed0dde55 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -1573,9 +1573,7 @@ tests['shadow image'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('star.png') } @@ -1589,9 +1587,7 @@ tests['scaled shadow image'] = function (ctx, done) { ctx.drawImage(img, 10, 10, 80, 80) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('star.png') } @@ -1657,9 +1653,7 @@ tests['drawImage(img,0,0)'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1669,21 +1663,38 @@ tests['drawImage(img) jpeg'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 100, 100) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('face.jpeg') } -tests['drawImage(img) svg'] = function (ctx, done) { +tests['drawImage(img) YCCK JPEG (#425)'] = function (ctx, done) { + // This also provides coverage for CMYK JPEGs var img = new Image() img.onload = function () { ctx.drawImage(img, 0, 0, 100, 100) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) + img.onerror = done + img.src = imageSrc('ycck.jpg') +} + +tests['drawImage(img) grayscale JPEG'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0, 100, 100) + done(null) } + img.onerror = done + img.src = imageSrc('grayscale.jpg') +} + +tests['drawImage(img) svg'] = function (ctx, done) { + var img = new Image() + img.onload = function () { + ctx.drawImage(img, 0, 0, 100, 100) + done(null) + } + img.onerror = done img.src = imageSrc('tree.svg') } @@ -1693,9 +1704,7 @@ tests['drawImage(img) svg with scaling from drawImage'] = function (ctx, done) { ctx.drawImage(img, -800, -800, 1000, 1000) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('tree.svg') } @@ -1706,9 +1715,7 @@ tests['drawImage(img) svg with scaling from ctx'] = function (ctx, done) { ctx.drawImage(img, -8, -8, 10, 10) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('tree.svg') } @@ -1718,9 +1725,7 @@ tests['drawImage(img,x,y)'] = function (ctx, done) { ctx.drawImage(img, 5, 25) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1730,9 +1735,7 @@ tests['drawImage(img,x,y,w,h) scale down'] = function (ctx, done) { ctx.drawImage(img, 25, 25, 10, 10) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1742,9 +1745,7 @@ tests['drawImage(img,x,y,w,h) scale up'] = function (ctx, done) { ctx.drawImage(img, 0, 0, 200, 200) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1754,9 +1755,7 @@ tests['drawImage(img,x,y,w,h) scale vertical'] = function (ctx, done) { ctx.drawImage(img, 0, 0, img.width, 200) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1766,9 +1765,7 @@ tests['drawImage(img,sx,sy,sw,sh,x,y,w,h)'] = function (ctx, done) { ctx.drawImage(img, 13, 13, 45, 45, 25, 25, img.width / 2, img.height / 2) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1780,9 +1777,7 @@ tests['drawImage(img,0,0) globalAlpha'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1797,9 +1792,7 @@ tests['drawImage(img,0,0) clip'] = function (ctx, done) { ctx.drawImage(img, 0, 0) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -1986,9 +1979,7 @@ tests['putImageData() png data'] = function (ctx, done) { done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -2009,9 +2000,7 @@ tests['putImageData() png data 2'] = function (ctx, done) { done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -2033,9 +2022,7 @@ tests['putImageData() png data 3'] = function (ctx, done) { ctx.putImageData(imageData, 50, 50) done(null) } - img.onerror = function () { - done(new Error('Failed to load image')) - } + img.onerror = done img.src = imageSrc('state.png') } @@ -2253,9 +2240,7 @@ tests['image sampling (#1084)'] = function (ctx, done) { if (loaded2) done() } - img1.onerror = function () { - done(new Error('Failed to load image')) - } + img1.onerror = done img2.onload = () => { loaded2 = true @@ -2263,9 +2248,7 @@ tests['image sampling (#1084)'] = function (ctx, done) { if (loaded1) done() } - img2.onerror = function () { - done(new Error('Failed to load image')) - } + img2.onerror = done img1.src = imageSrc('halved-1.jpeg') img2.src = imageSrc('halved-2.jpeg')