From 3ab222e43b5ec5e0b7581dfb836c9902af1667cc Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Thu, 6 Oct 2016 21:16:10 -0400 Subject: [PATCH 1/6] Support canvas.getBuffer('raw') This should help interface with custom image libraries like LodePNG or WebP. --- Readme.md | 16 +++++++- src/Canvas.cc | 30 ++++++++++++--- src/Canvas.h | 2 + test/canvas.test.js | 90 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index 61dd99565..e9e2fff7c 100644 --- a/Readme.md +++ b/Readme.md @@ -140,10 +140,22 @@ var stream = canvas.jpegStream({ ### Canvas#toBuffer() -A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing all of the PNG data. +A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing image data. ```javascript -canvas.toBuffer(); +// PNG Buffer, default settings +var buf = canvas.toBuffer(); + +// PNG Buffer, zlib compression level 3 (from 0-9), faster but bigger +var buf2 = canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE); + +// ARGB32 Buffer, native-endian +var buf3 = canvas.toBuffer('raw'); +var stride = canvas.stride; +// In memory, this is `canvas.height * canvas.stride` bytes long. +// The top row of pixels, in ARGB order, left-to-right, is: +var topPixelsARGBLeftToRight = buf3.slice(0, canvas.width * 4); +var row3 = buf3.slice(2 * canvas.stride, 2 * canvas.stride + canvas.width * 4); ``` ### Canvas#toBuffer() async diff --git a/src/Canvas.cc b/src/Canvas.cc index 777c1681b..e74966336 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -45,6 +45,7 @@ Canvas::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) { Nan::SetPrototypeMethod(ctor, "streamJPEGSync", StreamJPEGSync); #endif Nan::SetAccessor(proto, Nan::New("type").ToLocalChecked(), GetType); + Nan::SetAccessor(proto, Nan::New("stride").ToLocalChecked(), GetStride); Nan::SetAccessor(proto, Nan::New("width").ToLocalChecked(), GetWidth, SetWidth); Nan::SetAccessor(proto, Nan::New("height").ToLocalChecked(), GetHeight, SetHeight); @@ -91,6 +92,14 @@ NAN_GETTER(Canvas::GetType) { info.GetReturnValue().Set(Nan::New(canvas->isPDF() ? "pdf" : canvas->isSVG() ? "svg" : "image").ToLocalChecked()); } +/* + * Get stride. + */ +NAN_GETTER(Canvas::GetStride) { + Canvas *canvas = Nan::ObjectWrap::Unwrap(info.This()); + info.GetReturnValue().Set(Nan::New(canvas->stride())); +} + /* * Get width. */ @@ -248,6 +257,17 @@ NAN_METHOD(Canvas::ToBuffer) { return; } + if (info.Length() == 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { + // Return raw ARGB data -- just a memcpy() + cairo_surface_t *surface = canvas->surface(); + cairo_surface_flush(surface); + const unsigned char *data = cairo_image_surface_get_data(surface); + printf("%x %x %x %x %x\n", data[0], data[1], data[2], data[3], data[4]); + Local buf = Nan::CopyBuffer(reinterpret_cast(data), canvas->nBytes()).ToLocalChecked(); + info.GetReturnValue().Set(buf); + return; + } + if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) { if (!info[1]->IsUndefined()) { bool good = true; @@ -571,7 +591,7 @@ Canvas::Canvas(int w, int h, canvas_type_t t): Nan::ObjectWrap() { } else { _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); assert(_surface); - Nan::AdjustExternalMemory(4 * w * h); + Nan::AdjustExternalMemory(nBytes()); } } @@ -589,8 +609,9 @@ Canvas::~Canvas() { cairo_surface_destroy(_surface); break; case CANVAS_TYPE_IMAGE: + int oldNBytes = nBytes(); cairo_surface_destroy(_surface); - Nan::AdjustExternalMemory(-4 * width * height); + Nan::AdjustExternalMemory(-oldNBytes); break; } } @@ -626,11 +647,10 @@ Canvas::resurface(Local canvas) { break; case CANVAS_TYPE_IMAGE: // Re-surface - int old_width = cairo_image_surface_get_width(_surface); - int old_height = cairo_image_surface_get_height(_surface); + size_t oldNBytes = nBytes(); cairo_surface_destroy(_surface); _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - Nan::AdjustExternalMemory(4 * (width * height - old_width * old_height)); + Nan::AdjustExternalMemory(nBytes() - oldNBytes); // Reset context context = canvas->Get(Nan::New("context").ToLocalChecked()); diff --git a/src/Canvas.h b/src/Canvas.h index dcf31e861..d0ea25150 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -57,6 +57,7 @@ class Canvas: public Nan::ObjectWrap { static NAN_METHOD(New); static NAN_METHOD(ToBuffer); static NAN_GETTER(GetType); + static NAN_GETTER(GetStride); static NAN_GETTER(GetWidth); static NAN_GETTER(GetHeight); static NAN_SETTER(SetWidth); @@ -85,6 +86,7 @@ class Canvas: public Nan::ObjectWrap { inline void *closure(){ return _closure; } inline uint8_t *data(){ return cairo_image_surface_get_data(_surface); } inline int stride(){ return cairo_image_surface_get_stride(_surface); } + inline int nBytes(){ return height * stride(); } Canvas(int width, int height, canvas_type_t type); void resurface(Local canvas); diff --git a/test/canvas.test.js b/test/canvas.test.js index 3574ab2c1..68254a799 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -5,7 +5,8 @@ var Canvas = require('../') , assert = require('assert') , parseFont = Canvas.Context2d.parseFont - , fs = require('fs'); + , fs = require('fs') + , os = require('os'); console.log(); console.log(' canvas: %s', Canvas.version); @@ -246,6 +247,12 @@ describe('Canvas', function () { assert.equal(50, canvas.height); }); + it('Canvas#stride', function() { + var canvas = new Canvas(24, 10); + assert.ok(canvas.stride >= 24, 'canvas.stride is too short'); + assert.ok(canvas.stride < 1024, 'canvas.stride seems too long'); + }); + it('Canvas#getContext("invalid")', function () { assert.equal(null, new Canvas(200, 300).getContext('invalid')); }); @@ -377,6 +384,87 @@ describe('Canvas', function () { }); }); + describe('#toBuffer("raw")', function() { + var canvas = new Canvas(10, 10) + , ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, 10, 10); + + ctx.fillStyle = 'rgba(200, 200, 200, 0.505)'; + ctx.fillRect(0, 0, 5, 5); + + ctx.fillStyle = 'red'; + ctx.fillRect(5, 0, 5, 5); + + ctx.fillStyle = '#00ff00'; + ctx.fillRect(0, 5, 5, 5); + + ctx.fillStyle = 'black'; + ctx.fillRect(5, 5, 4, 5); + + /** Output: + * *****RRRRR + * *****RRRRR + * *****RRRRR + * *****RRRRR + * *****RRRRR + * GGGGGBBBB- + * GGGGGBBBB- + * GGGGGBBBB- + * GGGGGBBBB- + * GGGGGBBBB- + */ + + var buf = canvas.toBuffer('raw'); + var stride = canvas.stride; + + // Buffer doesn't have readUInt32(): it only has readUInt32LE() and + // readUInt32BE(). + if (os.endianness() === 'LE') buf.swap32(); + + function assertPixel(u32, x, y, message) { + var expected = '0x' + u32.toString(16); + var actual = '0x' + buf.readUInt32BE(y * stride + x * 4).toString(16); + assert.equal(actual, expected, message); + } + + it('should have the correct size', function() { + assert.equal(buf.length, stride * 10); + }); + + it('does not premultiply alpha', function() { + assertPixel(0x80646464, 0, 0, 'first semitransparent pixel'); + assertPixel(0x80646464, 4, 4, 'last semitransparent pixel'); + }); + + it('draws red', function() { + assertPixel(0xffff0000, 5, 0, 'first red pixel'); + assertPixel(0xffff0000, 9, 4, 'last red pixel'); + }); + + it('draws green', function() { + assertPixel(0xff00ff00, 0, 5, 'first green pixel'); + assertPixel(0xff00ff00, 4, 9, 'last green pixel'); + }); + + it('draws black', function() { + assertPixel(0xff000000, 5, 5, 'first black pixel'); + assertPixel(0xff000000, 8, 9, 'last black pixel'); + }); + + it('leaves undrawn pixels black, transparent', function() { + assertPixel(0x0, 9, 5, 'first undrawn pixel'); + assertPixel(0x0, 9, 9, 'last undrawn pixel'); + }); + + it('is immutable', function() { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, 10, 10); + canvas.toBuffer('raw'); // (side-effect: flushes canvas) + assertPixel(0xffff0000, 5, 0, 'first red pixel'); + }); + }); + describe('#toDataURL()', function () { var canvas = new Canvas(200, 200) , ctx = canvas.getContext('2d'); From fe88c8505c2e417faaebaed41264e3563217ef52 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Thu, 6 Oct 2016 21:55:38 -0400 Subject: [PATCH 2/6] Avoid buf.swap32() for older node versions --- test/canvas.test.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/canvas.test.js b/test/canvas.test.js index 68254a799..d29a9bbdb 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -418,13 +418,21 @@ describe('Canvas', function () { var buf = canvas.toBuffer('raw'); var stride = canvas.stride; - // Buffer doesn't have readUInt32(): it only has readUInt32LE() and - // readUInt32BE(). - if (os.endianness() === 'LE') buf.swap32(); - function assertPixel(u32, x, y, message) { var expected = '0x' + u32.toString(16); - var actual = '0x' + buf.readUInt32BE(y * stride + x * 4).toString(16); + + // Buffer doesn't have readUInt32(): it only has readUInt32LE() and + // readUInt32BE(). + var px = buf.readUInt32BE(y * stride + x * 4); + if (os.endianness() === 'LE') { + px = (((px & 0xff) << 24) + | ((px & 0xff00) << 8) + | ((px & 0xff0000) >> 8) + | ((px & 0xff000000) >> 24)) + >>> 0; // -1 => 0xffffffff + } + var actual = '0x' + px.toString(16); + assert.equal(actual, expected, message); } From bb506415fffd495a0ec7107cff620bf5be2fb8c8 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Thu, 6 Oct 2016 22:50:06 -0400 Subject: [PATCH 3/6] Avoid os.endianness() --- test/canvas.test.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/canvas.test.js b/test/canvas.test.js index d29a9bbdb..a510cf4e0 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -418,13 +418,21 @@ describe('Canvas', function () { var buf = canvas.toBuffer('raw'); var stride = canvas.stride; + var isBE = (function() { + var b = new ArrayBuffer(4); + var u32 = new Uint32Array(b); + var u8 = new Uint8Array(b); + u32[0] = 1; + return u8[0] ? 'LE' : 'BE'; + }()); + function assertPixel(u32, x, y, message) { var expected = '0x' + u32.toString(16); // Buffer doesn't have readUInt32(): it only has readUInt32LE() and // readUInt32BE(). var px = buf.readUInt32BE(y * stride + x * 4); - if (os.endianness() === 'LE') { + if (isBE) { px = (((px & 0xff) << 24) | ((px & 0xff00) << 8) | ((px & 0xff0000) >> 8) From 239d3c302de2951b1801f1a1aa707c6ec1da9f82 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Sat, 8 Oct 2016 17:00:30 -0400 Subject: [PATCH 4/6] Nix accidental printf() --- src/Canvas.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index e74966336..2e2ffa0a9 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -262,7 +262,6 @@ NAN_METHOD(Canvas::ToBuffer) { cairo_surface_t *surface = canvas->surface(); cairo_surface_flush(surface); const unsigned char *data = cairo_image_surface_get_data(surface); - printf("%x %x %x %x %x\n", data[0], data[1], data[2], data[3], data[4]); Local buf = Nan::CopyBuffer(reinterpret_cast(data), canvas->nBytes()).ToLocalChecked(); info.GetReturnValue().Set(buf); return; From f7cab453a46fc3c68ea47ae7dd7ef9d7e8f748de Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Sun, 16 Oct 2016 14:14:16 -0400 Subject: [PATCH 5/6] Make test a bit nicer --- test/canvas.test.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/canvas.test.js b/test/canvas.test.js index a510cf4e0..1dbd2040c 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -418,7 +418,8 @@ describe('Canvas', function () { var buf = canvas.toBuffer('raw'); var stride = canvas.stride; - var isBE = (function() { + // emulate os.endianness() (until node v0.8 support is dropped) + var endianness = (function() { var b = new ArrayBuffer(4); var u32 = new Uint32Array(b); var u8 = new Uint8Array(b); @@ -431,14 +432,7 @@ describe('Canvas', function () { // Buffer doesn't have readUInt32(): it only has readUInt32LE() and // readUInt32BE(). - var px = buf.readUInt32BE(y * stride + x * 4); - if (isBE) { - px = (((px & 0xff) << 24) - | ((px & 0xff00) << 8) - | ((px & 0xff0000) >> 8) - | ((px & 0xff000000) >> 24)) - >>> 0; // -1 => 0xffffffff - } + var px = buf['readUInt32' + endianness](y * stride + x * 4); var actual = '0x' + px.toString(16); assert.equal(actual, expected, message); From 94ae2a822dd1a4b924c64150c3d71b56abbf641e Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Sun, 16 Oct 2016 14:14:46 -0400 Subject: [PATCH 6/6] Allow `canvas.toBuffer(raw, undefined, undefined)` --- src/Canvas.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Canvas.cc b/src/Canvas.cc index 2e2ffa0a9..ce62053b3 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -257,7 +257,7 @@ NAN_METHOD(Canvas::ToBuffer) { return; } - if (info.Length() == 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { + if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { // Return raw ARGB data -- just a memcpy() cairo_surface_t *surface = canvas->surface(); cairo_surface_flush(surface);