Skip to content

Commit 25b03f3

Browse files
authored
Merge pull request #935 from zbjornson/formats
Support canvas.getContext("2d", {alpha: boolean, pixelFormat: string})
2 parents 41af8c0 + 8c41bb1 commit 25b03f3

23 files changed

Lines changed: 1052 additions & 220 deletions

History.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ Unreleased / patch
22
==================
33

44
* Port has_lib.sh to javascript (#872)
5+
* Support canvas.getContext("2d", {alpha: boolean}) and
6+
canvas.getContext("2d", {pixelFormat: "..."})
7+
* Support indexed PNG encoding.
58

69
1.6.0 / 2016-10-16
710
==================

Readme.md

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE; // Both are tracked
117117

118118
If image data is not tracked, and the Image is drawn to an image rather than a PDF canvas, the output will be junk. Enabling mime data tracking has no benefits (only a slow down) unless you are generating a PDF.
119119

120-
### Canvas#pngStream()
120+
### Canvas#pngStream(options)
121121

122122
To create a `PNGStream` simply call `canvas.pngStream()`, and the stream will start to emit _data_ events, finally emitting _end_ when finished. If an exception occurs the _error_ event is emitted.
123123

@@ -137,6 +137,22 @@ stream.on('end', function(){
137137

138138
Currently _only_ sync streaming is supported, however we plan on supporting async streaming as well (of course :) ). Until then the `Canvas#toBuffer(callback)` alternative is async utilizing `eio_custom()`.
139139

140+
To encode indexed PNGs from canvases with `pixelFormat: 'A8'` or `'A1'`, provide an options object:
141+
142+
```js
143+
var palette = new Uint8ClampedArray([
144+
//r g b a
145+
0, 50, 50, 255, // index 1
146+
10, 90, 90, 255, // index 2
147+
127, 127, 255, 255
148+
// ...
149+
]);
150+
canvas.pngStream({
151+
palette: palette,
152+
backgroundIndex: 0 // optional, defaults to 0
153+
})
154+
```
155+
140156
### Canvas#jpegStream() and Canvas#syncJPEGStream()
141157

142158
You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with
@@ -312,6 +328,76 @@ var canvas = new Canvas(200, 500, 'svg');
312328
fs.writeFile('out.svg', canvas.toBuffer());
313329
```
314330

331+
## Image pixel formats (experimental)
332+
333+
node-canvas has experimental support for additional pixel formats, roughly
334+
following the [Canvas color space proposal](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md).
335+
336+
```js
337+
var canvas = new Canvas(200, 200);
338+
var ctx = canvas.getContext('2d', {pixelFormat: 'A8'});
339+
```
340+
341+
By default, canvases are created in the `RGBA32` format, which corresponds to
342+
the native HTML Canvas behavior. Each pixel is 32 bits. The JavaScript APIs
343+
that involve pixel data (`getImageData`, `putImageData`) store the colors in
344+
the order {red, green, blue, alpha} without alpha pre-multiplication. (The C++
345+
API stores the colors in the order {alpha, red, green, blue} in native-[endian](https://en.wikipedia.org/wiki/Endianness)
346+
ordering, with alpha pre-multiplication.)
347+
348+
These additional pixel formats have experimental support:
349+
350+
* `RGB24` Like `RGBA32`, but the 8 alpha bits are always opaque. This format is
351+
always used if the `alpha` context attribute is set to false (i.e.
352+
`canvas.getContext('2d', {alpha: false})`). This format can be faster than
353+
`RGBA32` because transparency does not need to be calculated.
354+
* `A8` Each pixel is 8 bits. This format can either be used for creating
355+
grayscale images (treating each byte as an alpha value), or for creating
356+
indexed PNGs (treating each byte as a palette index) (see [the example using
357+
alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the
358+
example using `imageData`](examples/indexed-png-image-data.js)).
359+
* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the
360+
middle 6 bits, and blue in the lower 5 bits, in native platform endianness.
361+
Some hardware devices and frame buffers use this format. Note that PNG does
362+
not support this format; when creating a PNG, the image will be converted to
363+
24-bit RGB. This format is thus suboptimal for generating PNGs.
364+
`ImageData` instances for this mode use a `Uint16Array` instead of a `Uint8ClampedArray`.
365+
* `A1` Each pixel is 1 bit, and pixels are packed together into 32-bit
366+
quantities. The ordering of the bits matches the endianness of the
367+
platform: on a little-endian machine, the first pixel is the least-
368+
significant bit. This format can be used for creating single-color images.
369+
*Support for this format is incomplete, see note below.*
370+
* `RGB30` Each pixel is 30 bits, with red in the upper 10, green
371+
in the middle 10, and blue in the lower 10. (Requires Cairo 1.12 or later.)
372+
*Support for this format is incomplete, see note below.*
373+
374+
Notes and caveats:
375+
376+
* Using a non-default format can affect the behavior of APIs that involve pixel
377+
data:
378+
379+
* `context2d.createImageData` The size of the array returned depends on the
380+
number of bit per pixel for the underlying image data format, per the above
381+
descriptions.
382+
* `context2d.getImageData` The format of the array returned depends on the
383+
underlying image mode, per the above descriptions. Be aware of platform
384+
endianness, which can be determined using node.js's [`os.endianness()`](https://nodejs.org/api/os.html#os_os_endianness)
385+
function.
386+
* `context2d.putImageData` As above.
387+
388+
* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a
389+
use case and/or opinion on working with these formats? Open an issue and let
390+
us know! (See #935.)
391+
392+
* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render
393+
properly.
394+
395+
* The `ImageData(width, height)` and `ImageData(Uint8ClampedArray, width)`
396+
constructors assume 4 bytes per pixel. To create an `ImageData` instance with
397+
a different number of bytes per pixel, use
398+
`new ImageData(new Uint8ClampedArray(size), width, height)` or
399+
`new ImageData(new Uint16ClampedArray(size), width, height)`.
400+
315401
## Benchmarks
316402

317403
Although node-canvas is extremely new, and we have not even begun optimization yet it is already quite fast. For benchmarks vs other node canvas implementations view this [gist](https://gist.github.com/664922), or update the submodules and run `$ make benchmark` yourself.

examples/indexed-png-alpha.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
var Canvas = require('..')
2+
var fs = require('fs')
3+
var path = require('path')
4+
var canvas = new Canvas(200, 200)
5+
var ctx = canvas.getContext('2d', {pixelFormat: 'A8'})
6+
7+
// Matches the "fillStyle" browser test, made by using alpha fillStyle value
8+
var palette = new Uint8ClampedArray(37 * 4)
9+
var i, j
10+
var k = 0
11+
// First value is opaque white:
12+
palette[k++] = 255
13+
palette[k++] = 255
14+
palette[k++] = 255
15+
palette[k++] = 255
16+
for (i = 0; i < 6; i++) {
17+
for (j = 0; j < 6; j++) {
18+
palette[k++] = Math.floor(255 - 42.5 * i)
19+
palette[k++] = Math.floor(255 - 42.5 * j)
20+
palette[k++] = 0
21+
palette[k++] = 255
22+
}
23+
}
24+
for (i = 0; i < 6; i++) {
25+
for (j = 0; j < 6; j++) {
26+
var index = i * 6 + j + 1.5 // 0.5 to bias rounding
27+
var fraction = index / 255
28+
ctx.fillStyle = 'rgba(0,0,0,' + fraction + ')'
29+
ctx.fillRect(j * 25, i * 25, 25, 25)
30+
}
31+
}
32+
33+
canvas.createPNGStream({palette: palette})
34+
.pipe(fs.createWriteStream(path.join(__dirname, 'indexed2.png')))

examples/indexed-png-image-data.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
var Canvas = require('..')
2+
var fs = require('fs')
3+
var path = require('path')
4+
var canvas = new Canvas(200, 200)
5+
var ctx = canvas.getContext('2d', {pixelFormat: 'A8'})
6+
7+
// Matches the "fillStyle" browser test, made by manipulating imageData
8+
var palette = new Uint8ClampedArray(37 * 4)
9+
var k = 0
10+
var i, j
11+
// First value is opaque white:
12+
palette[k++] = 255
13+
palette[k++] = 255
14+
palette[k++] = 255
15+
palette[k++] = 255
16+
for (i = 0; i < 6; i++) {
17+
for (j = 0; j < 6; j++) {
18+
palette[k++] = Math.floor(255 - 42.5 * i)
19+
palette[k++] = Math.floor(255 - 42.5 * j)
20+
palette[k++] = 0
21+
palette[k++] = 255
22+
}
23+
}
24+
var idata = ctx.getImageData(0, 0, 200, 200)
25+
for (i = 0; i < 6; i++) {
26+
for (j = 0; j < 6; j++) {
27+
var index = j * 6 + i
28+
// fill rect:
29+
for (var xr = j * 25; xr < j * 25 + 25; xr++) {
30+
for (var yr = i * 25; yr < i * 25 + 25; yr++) {
31+
idata.data[xr * 200 + yr] = index + 1
32+
}
33+
}
34+
}
35+
}
36+
ctx.putImageData(idata, 0, 0)
37+
38+
canvas.createPNGStream({palette: palette})
39+
.pipe(fs.createWriteStream(path.join(__dirname, 'indexed.png')))

lib/canvas.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,15 @@ Canvas.prototype.inspect = function(){
112112
/**
113113
* Get a context object.
114114
*
115-
* @param {String} contextId
115+
* @param {String} contextType must be "2d"
116+
* @param {Object {alpha: boolean, pixelFormat: PIXEL_FORMAT} } contextAttributes Optional
116117
* @return {Context2d}
117118
* @api public
118119
*/
119120

120-
Canvas.prototype.getContext = function(contextId){
121-
if ('2d' == contextId) {
122-
var ctx = this._context2d || (this._context2d = new Context2d(this));
121+
Canvas.prototype.getContext = function (contextType, contextAttributes) {
122+
if ('2d' == contextType) {
123+
var ctx = this._context2d || (this._context2d = new Context2d(this, contextAttributes));
123124
this.context = ctx;
124125
ctx.canvas = this;
125126
return ctx;
@@ -129,25 +130,35 @@ Canvas.prototype.getContext = function(contextId){
129130
/**
130131
* Create a `PNGStream` for `this` canvas.
131132
*
133+
* @param {Object} options
134+
* @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding.
135+
* entries should be R-G-B-A values.
136+
* @param {Number} options.backgroundIndex Optional index of background color
137+
* for indexed PNGs. Defaults to 0.
132138
* @return {PNGStream}
133139
* @api public
134140
*/
135141

136142
Canvas.prototype.pngStream =
137-
Canvas.prototype.createPNGStream = function(){
138-
return new PNGStream(this);
143+
Canvas.prototype.createPNGStream = function(options){
144+
return new PNGStream(this, false, options);
139145
};
140146

141147
/**
142148
* Create a synchronous `PNGStream` for `this` canvas.
143149
*
150+
* @param {Object} options
151+
* @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding.
152+
* entries should be R-G-B-A values.
153+
* @param {Number} options.backgroundIndex Optional index of background color
154+
* for indexed PNGs. Defaults to 0.
144155
* @return {PNGStream}
145156
* @api public
146157
*/
147158

148159
Canvas.prototype.syncPNGStream =
149-
Canvas.prototype.createSyncPNGStream = function(){
150-
return new PNGStream(this, true);
160+
Canvas.prototype.createSyncPNGStream = function(options){
161+
return new PNGStream(this, true, options);
151162
};
152163

153164
/**

lib/context2d.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ Context2d.prototype.createImageData = function (width, height) {
274274
height = width.height
275275
width = width.width
276276
}
277-
278-
return new ImageData(width, height)
277+
var Bpp = this.canvas.stride / this.canvas.width;
278+
var nBytes = Bpp * width * height
279+
var arr;
280+
if (this.pixelFormat === "RGB16_565") {
281+
arr = new Uint16Array(nBytes / 2);
282+
} else {
283+
arr = new Uint8ClampedArray(nBytes);
284+
}
285+
return new ImageData(arr, width, height);
279286
}

lib/pngstream.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ var util = require('util');
2727
*
2828
* @param {Canvas} canvas
2929
* @param {Boolean} sync
30+
* @param {Object} options
31+
* @param {Uint8ClampedArray} options.palette Provide for indexed PNG encoding.
32+
* entries should be R-G-B-A values.
33+
* @param {Number} options.backgroundIndex Optional index of background color
34+
* for indexed PNGs. Defaults to 0.
3035
* @api public
3136
*/
3237

33-
var PNGStream = module.exports = function PNGStream(canvas, sync) {
38+
var PNGStream = module.exports = function PNGStream(canvas, sync, options) {
3439
if (!(this instanceof PNGStream)) {
3540
throw new TypeError("Class constructors cannot be invoked without 'new'");
3641
}
@@ -43,6 +48,7 @@ var PNGStream = module.exports = function PNGStream(canvas, sync) {
4348
: 'streamPNG';
4449
this.sync = sync;
4550
this.canvas = canvas;
51+
this.options = options || {};
4652

4753
// TODO: implement async
4854
if ('streamPNG' === method) method = 'streamPNGSync';
@@ -66,5 +72,5 @@ PNGStream.prototype._read = function _read() {
6672
} else {
6773
self.push(null);
6874
}
69-
});
75+
}, self.options);
7076
};

src/Canvas.cc

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ NAN_METHOD(Canvas::New) {
103103
backend = new ImageBackend(width, height);
104104
}
105105
else if (info[0]->IsObject()) {
106+
// TODO need to check if this is actually an instance of a Backend to avoid a fault
106107
backend = Nan::ObjectWrap::Unwrap<Backend>(info[0]->ToObject());
107108
}
108109
else {
@@ -304,6 +305,8 @@ NAN_METHOD(Canvas::ToBuffer) {
304305

305306
uv_work_t* req = new uv_work_t;
306307
req->data = closure;
308+
// Make sure the surface exists since we won't have an isolate context in the async block:
309+
canvas->surface();
307310
uv_queue_work(uv_default_loop(), req, ToBufferAsync, (uv_after_work_cb)ToBufferAsyncAfter);
308311

309312
return;
@@ -356,6 +359,10 @@ streamPNG(void *c, const uint8_t *data, unsigned len) {
356359

357360
/*
358361
* Stream PNG data synchronously.
362+
* TODO the compression level and filter args don't seem to be documented.
363+
* Maybe move them to named properties in the options object?
364+
* StreamPngSync(this, options: {palette?: Uint8ClampedArray})
365+
* StreamPngSync(this, compression_level?: uint32, filter?: uint32)
359366
*/
360367

361368
NAN_METHOD(Canvas::StreamPNGSync) {
@@ -365,6 +372,11 @@ NAN_METHOD(Canvas::StreamPNGSync) {
365372
if (!info[0]->IsFunction())
366373
return Nan::ThrowTypeError("callback function required");
367374

375+
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.This());
376+
uint8_t* paletteColors = NULL;
377+
size_t nPaletteColors = 0;
378+
uint8_t backgroundIndex = 0;
379+
368380
if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) {
369381
if (!info[1]->IsUndefined()) {
370382
bool good = true;
@@ -381,9 +393,32 @@ NAN_METHOD(Canvas::StreamPNGSync) {
381393
compression_level = tmp;
382394
}
383395
}
384-
} else {
385-
good = false;
386-
}
396+
} else if (info[1]->IsObject()) {
397+
// If canvas is A8 or A1 and options obj has Uint8ClampedArray palette,
398+
// encode as indexed PNG.
399+
cairo_format_t format = canvas->backend()->getFormat();
400+
if (format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) {
401+
Local<Object> attrs = info[1]->ToObject();
402+
Local<Value> palette = attrs->Get(Nan::New("palette").ToLocalChecked());
403+
if (palette->IsUint8ClampedArray()) {
404+
Local<Uint8ClampedArray> palette_ta = palette.As<Uint8ClampedArray>();
405+
nPaletteColors = palette_ta->Length();
406+
if (nPaletteColors % 4 != 0) {
407+
Nan::ThrowError("Palette length must be a multiple of 4.");
408+
}
409+
nPaletteColors /= 4;
410+
Nan::TypedArrayContents<uint8_t> _paletteColors(palette_ta);
411+
paletteColors = *_paletteColors;
412+
// Optional background color index:
413+
Local<Value> backgroundIndexVal = attrs->Get(Nan::New("backgroundIndex").ToLocalChecked());
414+
if (backgroundIndexVal->IsUint32()) {
415+
backgroundIndex = static_cast<uint8_t>(backgroundIndexVal->Uint32Value());
416+
}
417+
}
418+
}
419+
} else {
420+
good = false;
421+
}
387422

388423
if (good) {
389424
if (compression_level > 9) {
@@ -404,11 +439,13 @@ NAN_METHOD(Canvas::StreamPNGSync) {
404439
}
405440

406441

407-
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.This());
408442
closure_t closure;
409443
closure.fn = Local<Function>::Cast(info[0]);
410444
closure.compression_level = compression_level;
411445
closure.filter = filter;
446+
closure.palette = paletteColors;
447+
closure.nPaletteColors = nPaletteColors;
448+
closure.backgroundIndex = backgroundIndex;
412449

413450
Nan::TryCatch try_catch;
414451

0 commit comments

Comments
 (0)