Skip to content

Commit fc88d67

Browse files
committed
Make toBuffer more versatile
Now works for image/jpeg. PNG filter and ZLIB compression options are now named instead of positional.
1 parent 731989e commit fc88d67

File tree

4 files changed

+334
-193
lines changed

4 files changed

+334
-193
lines changed

Readme.md

+73-35
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,77 @@ loadImage('examples/images/lime-cat.jpg').then((image) => {
8080
})
8181
```
8282

83-
## Non-Standard API
83+
## Non-Standard APIs
8484

85-
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.
85+
node-canvas implements the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) as closely as possible.
86+
(See [Compatibility Status](https://github.com/Automattic/node-canvas/wiki/Compatibility-Status)
87+
for the current API compliance.) All non-standard APIs are documented below.
88+
89+
### Canvas#toBuffer()
90+
91+
Creates a [`Buffer`](https://nodejs.org/api/buffer.html) object representing the
92+
image contained in the canvas.
93+
94+
> `canvas.toBuffer((err: Error|null, result: Buffer) => void[, mimeType[, configArgument]]) => void`
95+
> `canvas.toBuffer([mimeType[, configArgument]]) => Buffer`
96+
97+
* **callback** If provided, the buffer will be provided in the callback instead
98+
of being returned by the function. Invoked with an error as the first argument
99+
if encoding failed, or the resulting buffer as the second argument if it
100+
succeeded. Not supported for mimeType `raw` (there is no async work to do).
101+
*Note*: Currently the callback function is invoked synchronously for `image/jpeg`.
102+
* **mimeType** A string indicating the image format. Valid options are `image/png`,
103+
`image/jpeg` (if node-canvas was built with JPEG support) and `raw` (returns
104+
unencoded ARGB32 data in native-endian byte order, top-to-bottom). Defaults to
105+
`image/png`. If the canvas is a PDF or SVG canvas, this argument is ignored.
106+
* **configArgument** For `image/jpeg`, a number between 0 and 1 indicating
107+
image quality (equivalent to the `qualityArgument` of `canvas.toBlob()`,
108+
defaults to 0.75). For `image/png`, an object specifying the ZLIB compression
109+
level (between 0 and 9, defaults to 6) and/or the PNG filter (defaults to
110+
`PNG_FILTER_NONE`): `{compressionLevel: 6, filter: Canvas.PNG_FILTER_NONE}`.
111+
112+
**Return value**
113+
114+
If no callback is provided, a [`Buffer`](https://nodejs.org/api/buffer.html).
115+
If a callback is provided, none.
116+
117+
**Examples**
118+
119+
```javascript
120+
// Default: buf contains a PNG-encoded image
121+
const buf = canvas.toBuffer()
122+
123+
// PNG-encoded, zlib compression level 3 for faster but bigger
124+
const buf2 = canvas.toBuffer('image/png', {compressionLevel: 3, filter: Canvas.PNG_FILTER_NONE})
125+
126+
// JPEG-encoded, 50% quality
127+
const buf3 = canvas.toBuffer('image/jpeg', 0.5)
128+
129+
// Asynchronous PNG
130+
canvas.toBuffer((err, buf) => {
131+
if (err) throw err; // encoding failed
132+
// buf is PNG-encoded image
133+
})
134+
135+
canvas.toBuffer((err, buf) => {
136+
if (err) throw err; // encoding failed
137+
// buf is JPEG-encoded image at 95% quality
138+
// Note that this callback is currently called synchronously.
139+
}, 'image/jpeg', 0.95)
140+
141+
// ARGB32 pixel values, native-endian
142+
const buf4 = canvas.toBuffer('raw')
143+
const {stride, width} = canvas
144+
// In memory, this is `canvas.height * canvas.stride` bytes long.
145+
// The top row of pixels, in ARGB order, left-to-right, is:
146+
const topPixelsARGBLeftToRight = buf4.slice(0, width * 4)
147+
// And the third row is:
148+
const row3 = buf4.slice(2 * stride, 2 * stride + width * 4)
149+
150+
// SVG and PDF canvases ignore the mimeType argument
151+
const myCanvas = createCanvas(w, h, 'pdf')
152+
myCanvas.toBuffer() // returns a buffer containing a PDF-encoded canvas
153+
```
86154

87155
### Image#src=Buffer
88156

@@ -157,14 +225,14 @@ canvas.pngStream({
157225
})
158226
```
159227

160-
### Canvas#jpegStream() and Canvas#syncJPEGStream()
228+
### Canvas#jpegStream()
161229

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

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

169237
```javascript
170238
var stream = canvas.jpegStream({
@@ -175,36 +243,6 @@ var stream = canvas.jpegStream({
175243
});
176244
```
177245

178-
### Canvas#toBuffer()
179-
180-
A call to `Canvas#toBuffer()` will return a node `Buffer` instance containing image data.
181-
182-
```javascript
183-
// PNG Buffer, default settings
184-
var buf = canvas.toBuffer();
185-
186-
// PNG Buffer, zlib compression level 3 (from 0-9), faster but bigger
187-
var buf2 = canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE);
188-
189-
// ARGB32 Buffer, native-endian
190-
var buf3 = canvas.toBuffer('raw');
191-
var stride = canvas.stride;
192-
// In memory, this is `canvas.height * canvas.stride` bytes long.
193-
// The top row of pixels, in ARGB order, left-to-right, is:
194-
var topPixelsARGBLeftToRight = buf3.slice(0, canvas.width * 4);
195-
var row3 = buf3.slice(2 * canvas.stride, 2 * canvas.stride + canvas.width * 4);
196-
```
197-
198-
### Canvas#toBuffer() async
199-
200-
Optionally we may pass a callback function to `Canvas#toBuffer()`, and this process will be performed asynchronously, and will `callback(err, buf)`.
201-
202-
```javascript
203-
canvas.toBuffer(function(err, buf){
204-
205-
});
206-
```
207-
208246
### Canvas#toDataURL() sync and async
209247

210248
The following syntax patterns are supported:

src/Canvas.cc

+118-61
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,69 @@ Canvas::ToBufferAsyncAfter(uv_work_t *req) {
224224
free(closure);
225225
}
226226

227+
inline static uint32_t parsePngCompressionLevel(Local<Value> arg) {
228+
// See quote below from spec section 4.12.5.5.
229+
if (arg->IsObject()) {
230+
Local<Object> obj = arg->ToObject();
231+
Local<Value> cLevelStr = Nan::New("compressionLevel").ToLocalChecked();
232+
if (obj->Has(cLevelStr)) {
233+
uint32_t compression_level = obj->Get(cLevelStr)->Uint32Value();
234+
if (compression_level >= 0 && compression_level <= 9) return compression_level;
235+
}
236+
}
237+
return 6;
238+
}
239+
240+
inline static uint32_t parsePngFilter(Local<Value> arg) {
241+
if (arg->IsObject()) {
242+
Local<Object> obj = arg->ToObject();
243+
Local<Value> cLevelStr = Nan::New("filter").ToLocalChecked();
244+
if (obj->Has(cLevelStr)) {
245+
return obj->Get(cLevelStr)->Uint32Value();
246+
}
247+
}
248+
return PNG_ALL_FILTERS;
249+
}
250+
251+
inline static uint32_t parseJpegQuality(Local<Value> arg) {
252+
// "If Type(quality) is not Number, or if quality is outside that range, the
253+
// user agent must use its default quality value, as if the quality argument
254+
// had not been given." - 4.12.5.5
255+
if (arg->IsNumber()) {
256+
double quality = arg->NumberValue();
257+
if (quality >= 0.0 && quality <= 1.0) return static_cast<uint32_t>(100.0 * quality);
258+
}
259+
return 75; // spec doesn't say what the default should be
260+
}
261+
227262
/*
228-
* Convert PNG data to a node::Buffer, async when a
229-
* callback function is passed.
263+
* Converts/encodes data to a Buffer. Async when a callback function is passed.
264+
265+
* PDF/SVG canvases:
266+
() => Buffer
267+
268+
* ARGB data:
269+
("raw") => Buffer
270+
((err: null|Error, buffer) => any, "raw") // ? There's no async work to do here.
271+
272+
* PNG-encoded
273+
() => Buffer
274+
(undefined|"image/png", {compressionLevel?: number, filter?: number}) => Buffer
275+
((err: null|Error, buffer) => any)
276+
((err: null|Error, buffer) => any, undefined|"image/png", {compressionLevel?: number, filter?: number})
277+
278+
* JPEG-encoded
279+
("image/jpeg") => Buffer
280+
("image/jpeg", quality) => Buffer
281+
((err: null|Error, buffer) => any, "image/jpeg")
282+
((err: null|Error, buffer) => any, "image/jpeg", quality)
230283
*/
231284

232285
NAN_METHOD(Canvas::ToBuffer) {
233286
cairo_status_t status;
234-
uint32_t compression_level = 6;
235-
uint32_t filter = PNG_ALL_FILTERS;
236287
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.This());
237288

238-
// TODO: async / move this out
289+
// Vector canvases, sync only
239290
const string name = canvas->backend()->getName();
240291
if (name == "pdf" || name == "svg") {
241292
cairo_surface_finish(canvas->surface());
@@ -246,8 +297,8 @@ NAN_METHOD(Canvas::ToBuffer) {
246297
return;
247298
}
248299

249-
if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New<String>("raw").ToLocalChecked())) {
250-
// Return raw ARGB data -- just a memcpy()
300+
// Raw ARGB data -- just a memcpy()
301+
if (info[0]->StrictEquals(Nan::New<String>("raw").ToLocalChecked())) {
251302
cairo_surface_t *surface = canvas->surface();
252303
cairo_surface_flush(surface);
253304
const unsigned char *data = cairo_image_surface_get_data(surface);
@@ -256,46 +307,44 @@ NAN_METHOD(Canvas::ToBuffer) {
256307
return;
257308
}
258309

259-
if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) {
260-
if (!info[1]->IsUndefined()) {
261-
bool good = true;
262-
if (info[1]->IsNumber()) {
263-
compression_level = info[1]->Uint32Value();
264-
} else if (info[1]->IsString()) {
265-
if (info[1]->StrictEquals(Nan::New<String>("0").ToLocalChecked())) {
266-
compression_level = 0;
267-
} else {
268-
uint32_t tmp = info[1]->Uint32Value();
269-
if (tmp == 0) {
270-
good = false;
271-
} else {
272-
compression_level = tmp;
273-
}
274-
}
275-
} else {
276-
good = false;
277-
}
310+
// Sync PNG, default
311+
if (info[0]->IsUndefined() || info[0]->StrictEquals(Nan::New<String>("image/png").ToLocalChecked())) {
312+
uint32_t compression_level = parsePngCompressionLevel(info[1]);
313+
uint32_t filter = parsePngFilter(info[1]);
278314

279-
if (good) {
280-
if (compression_level > 9) {
281-
return Nan::ThrowRangeError("Allowed compression levels lie in the range [0, 9].");
282-
}
283-
} else {
284-
return Nan::ThrowTypeError("Compression level must be a number.");
285-
}
315+
closure_t closure;
316+
status = closure_init(&closure, canvas, compression_level, filter);
317+
318+
// ensure closure is ok
319+
if (status) {
320+
closure_destroy(&closure);
321+
return Nan::ThrowError(Canvas::Error(status));
286322
}
287323

288-
if (!info[2]->IsUndefined()) {
289-
if (info[2]->IsUint32()) {
290-
filter = info[2]->Uint32Value();
291-
} else {
292-
return Nan::ThrowTypeError("Invalid filter value.");
293-
}
324+
Nan::TryCatch try_catch;
325+
status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure);
326+
327+
if (try_catch.HasCaught()) {
328+
closure_destroy(&closure);
329+
try_catch.ReThrow();
330+
return;
331+
} else if (status) {
332+
closure_destroy(&closure);
333+
return Nan::ThrowError(Canvas::Error(status));
334+
} else {
335+
Local<Object> buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked();
336+
closure_destroy(&closure);
337+
info.GetReturnValue().Set(buf);
338+
return;
294339
}
295340
}
296341

297-
// Async
298-
if (info[0]->IsFunction()) {
342+
// Async PNG
343+
if (info[0]->IsFunction() &&
344+
(info[1]->IsUndefined() || info[1]->StrictEquals(Nan::New<String>("image/png").ToLocalChecked()))) {
345+
uint32_t compression_level = parsePngCompressionLevel(info[2]);
346+
uint32_t filter = parsePngFilter(info[2]);
347+
299348
closure_t *closure = (closure_t *) malloc(sizeof(closure_t));
300349
status = closure_init(closure, canvas, compression_level, filter);
301350

@@ -317,34 +366,42 @@ NAN_METHOD(Canvas::ToBuffer) {
317366
uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter);
318367

319368
return;
320-
// Sync
321-
} else {
322-
closure_t closure;
323-
status = closure_init(&closure, canvas, compression_level, filter);
369+
}
324370

325-
// ensure closure is ok
326-
if (status) {
327-
closure_destroy(&closure);
328-
return Nan::ThrowError(Canvas::Error(status));
329-
}
371+
#ifdef HAVE_JPEG
372+
// JPEG
373+
Local<Value> jpegStr = Nan::New<String>("image/jpeg").ToLocalChecked();
374+
if (info[0]->StrictEquals(jpegStr) ||
375+
(info[0]->IsFunction() && info[1]->StrictEquals(jpegStr))) {
376+
uint32_t quality = parseJpegQuality(info[0]->IsFunction() ? info[1] : info[0]);
330377

331378
Nan::TryCatch try_catch;
332-
status = canvas_write_to_png_stream(canvas->surface(), toBuffer, &closure);
379+
unsigned char *outbuff = NULL;
380+
unsigned long outsize = 0;
381+
// TODO 2, 2 are chromaHSampFactor/chromaVSampFactor. Accept from arguments.
382+
write_to_jpeg_buffer(canvas->surface(), quality, false, 2, 2, &outbuff, &outsize);
333383

334384
if (try_catch.HasCaught()) {
335-
closure_destroy(&closure);
336-
try_catch.ReThrow();
337-
return;
338-
} else if (status) {
339-
closure_destroy(&closure);
340-
return Nan::ThrowError(Canvas::Error(status));
385+
if (info[0]->IsFunction()) {
386+
Local<Value> argv[1] = { try_catch.Exception() };
387+
info[0].As<Function>()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 1, argv);
388+
} else {
389+
try_catch.ReThrow();
390+
}
341391
} else {
342-
Local<Object> buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked();
343-
closure_destroy(&closure);
344-
info.GetReturnValue().Set(buf);
345-
return;
392+
char *signedOutBuff = reinterpret_cast<char*>(outbuff);
393+
Local<Object> buf = Nan::CopyBuffer(signedOutBuff, outsize).ToLocalChecked();
394+
if (info[0]->IsFunction()) {
395+
Local<Value> argv[2] = { Nan::Null(), buf };
396+
info[0].As<Function>()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 2, argv);
397+
} else {
398+
info.GetReturnValue().Set(buf);
399+
}
346400
}
401+
402+
return;
347403
}
404+
#endif
348405
}
349406

350407
/*

0 commit comments

Comments
 (0)