Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support canvas.getBuffer('raw') #819

Merged
merged 6 commits into from Oct 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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