Skip to content

Commit

Permalink
Make toBuffer more versatile
Browse files Browse the repository at this point in the history
Now works for image/jpeg.

PNG filter and ZLIB compression options are now named instead of positional.
  • Loading branch information
zbjornson committed Apr 22, 2018
1 parent 4e04eff commit 101083e
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 193 deletions.
115 changes: 80 additions & 35 deletions Readme.md
Expand Up @@ -80,9 +80,84 @@ loadImage('examples/images/lime-cat.jpg').then((image) => {
})
```

## Non-Standard API
## Non-Standard APIs

node-canvas extends the canvas API to provide interfacing with node, for example streaming PNG data, converting to a `Buffer` instance, etc. Among the interfacing API, in some cases the drawing API has been extended for SSJS image manipulation / creation usage, however keep in mind these additions may fail to render properly within browsers.
node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as closely as possible.
(See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status)
for the current API compliance.) All non-standard APIs are documented below.

### Canvas#toBuffer()

Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the
image contained in the canvas.

> `canvas.toBuffer((err: Error|null, result: Buffer) => void[, mimeType[, configArgument]]) => void`
> `canvas.toBuffer([mimeType[, configArgument]]) => Buffer`
* **callback** If provided, the buffer will be provided in the callback instead
of being returned by the function. Invoked with an error as the first argument
if encoding failed, or the resulting buffer as the second argument if it
succeeded. Not supported for mimeType `raw` or for PDF or SVG canvases (there
is no async work to do in those cases). *Note*: Currently the callback
function is invoked synchronously for `image/jpeg`.
* **mimeType** A string indicating the image format. Valid options are `image/png`,
`image/jpeg` (if node-canvas was built with JPEG support) and `raw` (unencoded
ARGB32 data in native-endian byte order, top-to-bottom). Defaults to
`image/png`. If the canvas is a PDF or SVG canvas, this argument is ignored
and a PDF or SVG is returend always.
* **configArgument**
* For `image/jpeg` an object specifying the quality (0 to 1), if progressive
compression should be used and/or if chroma subsampling should be used:
`{quality: 0.75, progressive: false, chromaSubsampling: true}`. All
properties are optional.
* For `image/png`, an object specifying the ZLIB compression level (between 0
and 9), the compression filter(s), the palette (indexed PNGs only) and/or
the background palette index (indexed PNGs only):
`{compressionLevel: 6, filter: Canvas.PNG_ALL_FILTERS, palette: undefined, backgroundIndex: 0}`.
All properties are optional.

**Return value**

If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html).
If a callback is provided, none.

**Examples**

```javascript
// Default: buf contains a PNG-encoded image
const buf = canvas.toBuffer()

// PNG-encoded, zlib compression level 3 for faster compression but bigger files, no filtering
const buf2 = canvas.toBuffer('image/png', {compressionLevel: 3, filter: Canvas.PNG_FILTER_NONE})

// JPEG-encoded, 50% quality
const buf3 = canvas.toBuffer('image/jpeg', {quality: 0.5})

// Asynchronous PNG
canvas.toBuffer((err, buf) => {
if (err) throw err; // encoding failed
// buf is PNG-encoded image
})

canvas.toBuffer((err, buf) => {
if (err) throw err; // encoding failed
// buf is JPEG-encoded image at 95% quality
// Note that this callback is currently called synchronously.
}, 'image/jpeg', {quality: 0.95})

// ARGB32 pixel values, native-endian
const buf4 = canvas.toBuffer('raw')
const {stride, width} = canvas
// In memory, this is `canvas.height * canvas.stride` bytes long.
// The top row of pixels, in ARGB order, left-to-right, is:
const topPixelsARGBLeftToRight = buf4.slice(0, width * 4)
// And the third row is:
const row3 = buf4.slice(2 * stride, 2 * stride + width * 4)

// SVG and PDF canvases ignore the mimeType argument
const myCanvas = createCanvas(w, h, 'pdf')
myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas
```

### Image#src=Buffer

Expand Down Expand Up @@ -157,14 +232,14 @@ canvas.pngStream({
})
```

### Canvas#jpegStream() and Canvas#syncJPEGStream()
### Canvas#jpegStream()

You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with
some optional parameters; functionality is otherwise identical to
`pngStream()`. See `examples/crop.js` for an example.

_Note: At the moment, `jpegStream()` is the same as `syncJPEGStream()`, both
are synchronous_
_Note: At the moment, `jpegStream()` is synchronous under the hood. That is, it
runs in the main thread, not in the libuv threadpool._

```javascript
var stream = canvas.jpegStream({
Expand All @@ -175,36 +250,6 @@ var stream = canvas.jpegStream({
});
```

### Canvas#toBuffer()

A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing image data.

```javascript
// 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

Optionally we may pass a callback function to `Canvas#toBuffer()`, and this process will be performed asynchronously, and will `callback(err, buf)`.

```javascript
canvas.toBuffer(function(err, buf){

});
```

### Canvas#toDataURL() sync and async

The following syntax patterns are supported:
Expand Down
201 changes: 140 additions & 61 deletions src/Canvas.cc
Expand Up @@ -224,18 +224,90 @@ Canvas::ToBufferAsyncAfter(uv_work_t *req) {
free(closure);
}

inline static uint32_t parsePngCompressionLevel(Local<Value> arg) {
// See quote below from spec section 4.12.5.5.
if (arg->IsObject()) {
Local<Object> obj = arg->ToObject();
Local<Value> cLevelStr = Nan::New("compressionLevel").ToLocalChecked();
if (obj->Has(cLevelStr)) {
uint32_t compression_level = obj->Get(cLevelStr)->Uint32Value();
if (compression_level >= 0 && compression_level <= 9) return compression_level;
}
}
return 6;
}

inline static uint32_t parsePngFilter(Local<Value> arg) {
if (arg->IsObject()) {
Local<Object> obj = arg->ToObject();
Local<Value> cLevelStr = Nan::New("filter").ToLocalChecked();
if (obj->Has(cLevelStr)) {
return obj->Get(cLevelStr)->Uint32Value();
}
}
return PNG_ALL_FILTERS;
}

inline static uint32_t parseJpegQuality(Local<Value> arg) {
// "If Type(quality) is not Number, or if quality is outside that range, the
// user agent must use its default quality value, as if the quality argument
// had not been given." - 4.12.5.5
if (arg->IsObject()) {
Local<Object> obj = arg->ToObject();
Local<Value> qualStr = Nan::New("quality").ToLocalChecked();
if (obj->Has(qualStr)) {
double quality = obj->Get(qualStr)->NumberValue();
if (quality >= 0.0 && quality <= 1.0) return static_cast<uint32_t>(100.0 * quality);
}
}
return 75; // spec doesn't say what the default should be
}

inline static uint32_t parseJpegChromaSampFactor(Local<Value> arg) {
if (arg->IsObject()) {
Local<Object> obj = arg->ToObject();
Local<Value> chromaStr = Nan::New("chromaSubsampling").ToLocalChecked();
if (obj->Has(chromaStr)) {
Local<Value> chroma = obj->Get(chromaStr);
if (chroma->IsBoolean()) {
bool subsample = chroma->BooleanValue();
return subsample ? 2 : 1;
} else if (chroma->IsNumber()) {
return chroma->Uint32Value();
}
}
}
return 2;
}

/*
* Convert PNG data to a node::Buffer, async when a
* callback function is passed.
* Converts/encodes data to a Buffer. Async when a callback function is passed.
* PDF/SVG canvases:
() => Buffer
* ARGB data:
("raw") => Buffer
((err: null|Error, buffer) => any, "raw") // ? There's no async work to do here.
* PNG-encoded
() => Buffer
(undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer
((err: null|Error, buffer) => any)
((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number})
* JPEG-encoded
("image/jpeg") => Buffer
("image/jpeg", quality) => Buffer
((err: null|Error, buffer) => any, "image/jpeg")
((err: null|Error, buffer) => any, "image/jpeg", quality)
*/

NAN_METHOD(Canvas::ToBuffer) {
cairo_status_t status;
uint32_t compression_level = 6;
uint32_t filter = PNG_ALL_FILTERS;
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.This());

// TODO: async / move this out
// Vector canvases, sync only
const string name = canvas->backend()->getName();
if (name == "pdf" || name == "svg") {
cairo_surface_finish(canvas->surface());
Expand All @@ -246,8 +318,8 @@ NAN_METHOD(Canvas::ToBuffer) {
return;
}

if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New<String>("raw").ToLocalChecked())) {
// Return raw ARGB data -- just a memcpy()
// Raw ARGB data -- just a memcpy()
if (info[0]->StrictEquals(Nan::New<String>("raw").ToLocalChecked())) {
cairo_surface_t *surface = canvas->surface();
cairo_surface_flush(surface);
const unsigned char *data = cairo_image_surface_get_data(surface);
Expand All @@ -256,46 +328,44 @@ NAN_METHOD(Canvas::ToBuffer) {
return;
}

if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) {
if (!info[1]->IsUndefined()) {
bool good = true;
if (info[1]->IsNumber()) {
compression_level = info[1]->Uint32Value();
} else if (info[1]->IsString()) {
if (info[1]->StrictEquals(Nan::New<String>("0").ToLocalChecked())) {
compression_level = 0;
} else {
uint32_t tmp = info[1]->Uint32Value();
if (tmp == 0) {
good = false;
} else {
compression_level = tmp;
}
}
} else {
good = false;
}
// Sync PNG, default
if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New<String>("image/png").ToLocalChecked())) {
uint32_t compression_level = parsePngCompressionLevel(info[1]);
uint32_t filter = parsePngFilter(info[1]);
// TODO palette and backgroundIndex

if (good) {
if (compression_level > 9) {
return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9].");
}
} else {
return Nan::ThrowTypeError("Compression level must be a number.");
}
closure_t closure;
status = closure_init(&closure, canvas, compression_level, filter);

if (status) {
closure_destroy(&closure);
return Nan::ThrowError(Canvas::Error(status));
}

if (!info[2]->IsUndefined()) {
if (info[2]->IsUint32()) {
filter = info[2]->Uint32Value();
} else {
return Nan::ThrowTypeError("Invalid filter value.");
}
Nan::TryCatch try_catch;
status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure);

if (try_catch.HasCaught()) {
closure_destroy(&closure);
try_catch.ReThrow();
} else if (status) {
closure_destroy(&closure);
Nan::ThrowError(Canvas::Error(status));
} else {
Local<Object> buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked();
closure_destroy(&closure);
info.GetReturnValue().Set(buf);
}
return;
}

// Async
if (info[0]->IsFunction()) {
// Async PNG
if (info[0]->IsFunction() &&
(info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New<String>("image/png").ToLocalChecked()))) {
uint32_t compression_level = parsePngCompressionLevel(info[2]);
uint32_t filter = parsePngFilter(info[2]);
// TODO palette

closure_t *closure = (closure_t *) malloc(sizeof(closure_t));
status = closure_init(closure, canvas, compression_level, filter);

Expand All @@ -317,34 +387,43 @@ NAN_METHOD(Canvas::ToBuffer) {
uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter);

return;
// Sync
} else {
closure_t closure;
status = closure_init(&closure, canvas, compression_level, filter);
}

// ensure closure is ok
if (status) {
closure_destroy(&closure);
return Nan::ThrowError(Canvas::Error(status));
}
#ifdef HAVE_JPEG
// JPEG
Local<Value> jpegStr = Nan::New<String>("image/jpeg").ToLocalChecked();
if (info[0]->StrictEquals(jpegStr) ||
(info[0]->IsFunction() && info[1]->StrictEquals(jpegStr))) {
Local<Value> maybeOptsObj = info[0]->IsFunction() ? info[1] : info[0];
uint32_t quality = parseJpegQuality(maybeOptsObj);
uint32_t chromaSamp = parseJpegChromaSampFactor(maybeOptsObj);

Nan::TryCatch try_catch;
status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure);
unsigned char *outbuff = NULL;
unsigned long outsize = 0;
write_to_jpeg_buffer(canvas->surface(), quality, false, chromaSamp, chromaSamp, &outbuff, &outsize);

if (try_catch.HasCaught()) {
closure_destroy(&closure);
try_catch.ReThrow();
return;
} else if (status) {
closure_destroy(&closure);
return Nan::ThrowError(Canvas::Error(status));
if (info[0]->IsFunction()) {
Local<Value> argv[1] = { try_catch.Exception() };
info[0].As<Function>()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 1, argv);
} else {
try_catch.ReThrow();
}
} else {
Local<Object> buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked();
closure_destroy(&closure);
info.GetReturnValue().Set(buf);
return;
char *signedOutBuff = reinterpret_cast<char*>(outbuff);
Local<Object> buf = Nan::CopyBuffer(signedOutBuff, outsize).ToLocalChecked();
if (info[0]->IsFunction()) {
Local<Value> argv[2] = { Nan::Null(), buf };
info[0].As<Function>()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 2, argv);
} else {
info.GetReturnValue().Set(buf);
}
}

return;
}
#endif
}

/*
Expand Down

0 comments on commit 101083e

Please sign in to comment.