Skip to content

Commit

Permalink
Merge pull request #819 from adamhooper/toBuffer-raw
Browse files Browse the repository at this point in the history
Support canvas.getBuffer('raw')
  • Loading branch information
LinusU committed Oct 16, 2016
2 parents b5a5d0c + 94ae2a8 commit f677469
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 8 deletions.
16 changes: 14 additions & 2 deletions Readme.md
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions src/Canvas.cc
Expand Up @@ -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);

Expand Down Expand Up @@ -91,6 +92,14 @@ NAN_GETTER(Canvas::GetType) {
info.GetReturnValue().Set(Nan::New<String>(canvas->isPDF() ? "pdf" : canvas->isSVG() ? "svg" : "image").ToLocalChecked());
}

/*
* Get stride.
*/
NAN_GETTER(Canvas::GetStride) {
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.This());
info.GetReturnValue().Set(Nan::New<Number>(canvas->stride()));
}

/*
* Get width.
*/
Expand Down Expand Up @@ -248,6 +257,16 @@ NAN_METHOD(Canvas::ToBuffer) {
return;
}

if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New<String>("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);
Local<Object> buf = Nan::CopyBuffer(reinterpret_cast<const char*>(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;
Expand Down Expand Up @@ -571,7 +590,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());
}
}

Expand All @@ -589,8 +608,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;
}
}
Expand Down Expand Up @@ -626,11 +646,10 @@ Canvas::resurface(Local<Object> 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<String>("context").ToLocalChecked());
Expand Down
2 changes: 2 additions & 0 deletions src/Canvas.h
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Object> canvas);

Expand Down
100 changes: 99 additions & 1 deletion test/canvas.test.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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'));
});
Expand Down Expand Up @@ -377,6 +384,97 @@ 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;

// 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);
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['readUInt32' + endianness](y * stride + x * 4);
var actual = '0x' + px.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');
Expand Down

0 comments on commit f677469

Please sign in to comment.