From 3ab222e43b5ec5e0b7581dfb836c9902af1667cc Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Thu, 6 Oct 2016 21:16:10 -0400 Subject: [PATCH] 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 61dd995..e9e2fff 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 777c168..e749663 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 dcf31e8..d0ea251 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 3574ab2..68254a7 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');