From 04fefa429121a8abac4125ea689b90cd713d4288 Mon Sep 17 00:00:00 2001 From: Philippe Plantier Date: Wed, 26 Oct 2022 11:47:40 +0200 Subject: [PATCH] getImageData fixes when rectangle is outside of canvas fix a crash in getImageData if the rectangle is outside the canvas return transparent black pixels when getting image data outside the canvas remove dead code, add comments Fixes #2024 Fixes #1849 --- CHANGELOG.md | 2 + src/CanvasRenderingContext2d.cc | 50 ++-- test/canvas.test.js | 478 ++++++++++++++++++++++++++++++++ test/public/tests.js | 22 ++ 4 files changed, 533 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d8a039d..ed1b43c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ project adheres to [Semantic Versioning](http://semver.org/). * Support for accessibility and links in PDFs ### Fixed +* Fix a crash in `getImageData` when the rectangle is entirely outside the canvas. ([#2024](https://github.com/Automattic/node-canvas/issues/2024)) +* Fix `getImageData` cropping the resulting `ImageData` when the given rectangle is partly outside the canvas. ([#1849](https://github.com/Automattic/node-canvas/issues/1849)) 3.0.1 ================== diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index f8f217ad9..dfbcc17a0 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -1011,21 +1011,26 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { sh = -sh; } - if (sx + sw > width) sw = width - sx; - if (sy + sh > height) sh = height - sy; - - // WebKit/moz functionality. node-canvas used to return in either case. - if (sw <= 0) sw = 1; - if (sh <= 0) sh = 1; - - // Non-compliant. "Pixels outside the canvas must be returned as transparent - // black." This instead clips the returned array to the canvas area. + // Width and height to actually copy + int cw = sw; + int ch = sh; + // Offsets in the destination image + int ox = 0; + int oy = 0; + + // Clamp the copy width and height if the copy would go outside the image + if (sx + sw > width) cw = width - sx; + if (sy + sh > height) ch = height - sy; + + // Clamp the copy origin if the copy would go outside the image if (sx < 0) { - sw += sx; + ox = -sx; + cw += sx; sx = 0; } if (sy < 0) { - sh += sy; + oy = -sy; + ch += sy; sy = 0; } @@ -1047,13 +1052,16 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { uint8_t *dst = (uint8_t *)buffer.Data(); + if (!(cw > 0 && ch > 0)) goto return_empty; + switch (canvas->backend()->getFormat()) { case CAIRO_FORMAT_ARGB32: { + dst += oy * dstStride + ox * 4; // Rearrange alpha (argb -> rgba), undo alpha pre-multiplication, // and store in big-endian format - for (int y = 0; y < sh; ++y) { + for (int y = 0; y < ch; ++y) { uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); - for (int x = 0; x < sw; ++x) { + for (int x = 0; x < cw; ++x) { int bx = x * 4; uint32_t *pixel = row + x + sx; uint8_t a = *pixel >> 24; @@ -1082,10 +1090,11 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { break; } case CAIRO_FORMAT_RGB24: { + dst += oy * dstStride + ox * 4; // Rearrange alpha (argb -> rgba) and store in big-endian format - for (int y = 0; y < sh; ++y) { + for (int y = 0; y < ch; ++y) { uint32_t *row = (uint32_t *)(src + srcStride * (y + sy)); - for (int x = 0; x < sw; ++x) { + for (int x = 0; x < cw; ++x) { int bx = x * 4; uint32_t *pixel = row + x + sx; uint8_t r = *pixel >> 16; @@ -1102,9 +1111,10 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { break; } case CAIRO_FORMAT_A8: { - for (int y = 0; y < sh; ++y) { + dst += oy * dstStride + ox; + for (int y = 0; y < ch; ++y) { uint8_t *row = (uint8_t *)(src + srcStride * (y + sy)); - memcpy(dst, row + sx, dstStride); + memcpy(dst, row + sx, cw); dst += dstStride; } break; @@ -1116,9 +1126,10 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { break; } case CAIRO_FORMAT_RGB16_565: { - for (int y = 0; y < sh; ++y) { + dst += oy * dstStride + ox * 2; + for (int y = 0; y < ch; ++y) { uint16_t *row = (uint16_t *)(src + srcStride * (y + sy)); - memcpy(dst, row + sx, dstStride); + memcpy(dst, row + sx, cw * 2); dst += dstStride; } break; @@ -1138,6 +1149,7 @@ Context2d::GetImageData(const Napi::CallbackInfo& info) { } } +return_empty: Napi::Number swHandle = Napi::Number::New(env, sw); Napi::Number shHandle = Napi::Number::New(env, sh); Napi::Function ctor = env.GetInstanceData()->ImageDataCtor.Value(); diff --git a/test/canvas.test.js b/test/canvas.test.js index ecadbc7ec..48abb57b8 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -1251,6 +1251,430 @@ describe('Canvas', function () { it('works, slice, RGB30') + describe('slice partially outside the canvas', function () { + describe('left', function () { + if('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(-1, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + }) + }) + + describe('right', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(255, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(255, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal((255 & 0b11111), imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(2, 0, 2, 1) + assert.equal(2, imageData.width) + assert.equal(1, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(191, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('left and right', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(20, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(0, imageData.data[10]) + assert.equal(255, imageData.data[11]) + + assert.equal(0, imageData.data[12]) + assert.equal(0, imageData.data[13]) + assert.equal(255, imageData.data[14]) + assert.equal(255, imageData.data[15]) + + assert.equal(0, imageData.data[16]) + assert.equal(0, imageData.data[17]) + assert.equal(0, imageData.data[18]) + assert.equal(0, imageData.data[19]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(20, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(0, imageData.data[8]) + assert.equal(255, imageData.data[9]) + assert.equal(0, imageData.data[10]) + assert.equal(255, imageData.data[11]) + + assert.equal(0, imageData.data[12]) + assert.equal(0, imageData.data[13]) + assert.equal(255, imageData.data[14]) + assert.equal(255, imageData.data[15]) + + assert.equal(0, imageData.data[16]) + assert.equal(0, imageData.data[17]) + assert.equal(0, imageData.data[18]) + assert.equal(0, imageData.data[19]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(5, imageData.data.length) + + assert.equal(0, imageData.data[0]) + + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + assert.equal((255 & 0b111111) << 5, imageData.data[2]) + assert.equal((255 & 0b11111), imageData.data[3]) + + assert.equal(0, imageData.data[4]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(-1, 0, 5, 1) + assert.equal(5, imageData.width) + assert.equal(1, imageData.height) + assert.equal(5, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + assert.equal(127, imageData.data[2]) + assert.equal(191, imageData.data[3]) + assert.equal(0, imageData.data[4]) + }) + }) + + describe('top', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, -1, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + }) + }) + + describe('bottom', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(255, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(255, imageData.data[3]) + + assert.equal(0, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal((255 & 0b11111) << 11, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, 5, 1, 2) + assert.equal(1, imageData.width) + assert.equal(2, imageData.height) + assert.equal(2, imageData.data.length) + + assert.equal(63, imageData.data[0]) + assert.equal(0, imageData.data[1]) + }) + }) + + describe('top to bottom', function () { + it('works, RGBA32', function () { + const ctx = createTestCanvas() + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(32, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(255, imageData.data[24]) + assert.equal(0, imageData.data[25]) + assert.equal(0, imageData.data[26]) + assert.equal(255, imageData.data[27]) + + assert.equal(0, imageData.data[28]) + assert.equal(0, imageData.data[29]) + assert.equal(0, imageData.data[30]) + assert.equal(0, imageData.data[31]) + }) + + it('works, RGB24', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB24' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(32, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + + assert.equal(255, imageData.data[4]) + assert.equal(0, imageData.data[5]) + assert.equal(0, imageData.data[6]) + assert.equal(255, imageData.data[7]) + + assert.equal(255, imageData.data[24]) + assert.equal(0, imageData.data[25]) + assert.equal(0, imageData.data[26]) + assert.equal(255, imageData.data[27]) + + assert.equal(0, imageData.data[28]) + assert.equal(0, imageData.data[29]) + assert.equal(0, imageData.data[30]) + assert.equal(0, imageData.data[31]) + }) + + it('works, RGB16_565', function () { + const ctx = createTestCanvas(false, { pixelFormat: 'RGB16_565' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal((255 & 0b11111) << 11, imageData.data[1]) + assert.equal((255 & 0b11111) << 11, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + + it('works, A8', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + const imageData = ctx.getImageData(0, -1, 1, 8) + assert.equal(1, imageData.width) + assert.equal(8, imageData.height) + assert.equal(8, imageData.data.length) + + assert.equal(0, imageData.data[0]) + assert.equal(63, imageData.data[1]) + assert.equal(63, imageData.data[6]) + assert.equal(0, imageData.data[7]) + }) + }) + }) + it('works, assignment', function () { const ctx = createTestCanvas() const data = ctx.getImageData(0, 0, 5, 5).data @@ -1274,6 +1698,60 @@ describe('Canvas', function () { ctx.getImageData(0, 0, 3, 6) }) }) + + describe('does not throw if rectangle is outside the canvas (#2024)', function () { + it('on the left', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(-11, 0, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the right', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(98, 0, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the top', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(0, -12, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + + it('on the bottom', function () { + const ctx = createTestCanvas(true, { pixelFormat: 'A8' }) + + const imageData = ctx.getImageData(0, 98, 2, 2); + assert.equal(2, imageData.width) + assert.equal(2, imageData.height) + assert.equal(4, imageData.data.length) + assert.equal(0, imageData.data[0]) + assert.equal(0, imageData.data[1]) + assert.equal(0, imageData.data[2]) + assert.equal(0, imageData.data[3]) + }) + }) }) it('Context2d#createPattern(Canvas)', function () { diff --git a/test/public/tests.js b/test/public/tests.js index c904cde7e..89d5ba67e 100644 --- a/test/public/tests.js +++ b/test/public/tests.js @@ -2378,6 +2378,28 @@ tests['putImageData() 10'] = function (ctx) { ctx.putImageData(data, 20, 120) } +tests['putImageData() 11'] = function (ctx) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(-25, -25, 50, 50) + ctx.putImageData(data, 10, 10) +} + +tests['putImageData() 12'] = function (ctx) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' + Math.floor(255 - 42.5 * j) + ',0)' + ctx.fillRect(j * 25, i * 25, 25, 25) + } + } + const data = ctx.getImageData(175, 175, 50, 50) + ctx.putImageData(data, 10, 10) +} + tests['putImageData() alpha'] = function (ctx) { ctx.fillStyle = 'rgba(255,0,0,0.5)' ctx.fillRect(0, 0, 50, 100)