Skip to content

Commit 16de515

Browse files
committed
Indexed PNG encoding
1 parent 18c05f1 commit 16de515

9 files changed

Lines changed: 195 additions & 18 deletions

File tree

History.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Unreleased / patch
44
* Port has_lib.sh to javascript (#872)
55
* Support canvas.getContext("2d", {alpha: boolean}) and
66
canvas.getContext("2d", {pixelFormat: "..."})
7+
* Support indexed PNG encoding.
78

89
1.6.0 / 2016-10-16
910
==================

Readme.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ img.dataMode = Image.MODE_MIME | Image.MODE_IMAGE; // Both are tracked
111111

112112
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.
113113

114-
### Canvas#pngStream()
114+
### Canvas#pngStream(options)
115115

116116
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.
117117

@@ -131,6 +131,22 @@ stream.on('end', function(){
131131

132132
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()`.
133133

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

136152
You can likewise create a `JPEGStream` by calling `canvas.jpegStream()` with
@@ -331,7 +347,9 @@ These additional pixel formats have experimental support:
331347
`RGBA32` because transparency does not need to be calculated.
332348
* `A8` Each pixel is 8 bits. This format can either be used for creating
333349
grayscale images (treating each byte as an alpha value), or for creating
334-
indexed PNGs (treating each byte as a palette index).
350+
indexed PNGs (treating each byte as a palette index) (see [the example using
351+
alpha values with `fillStyle`](examples/indexed-png-alpha.js) and [the
352+
example using `imageData`](examples/indexed-png-image-data.js)).
335353
* `RGB16_565` Each pixel is 16 bits, with red in the upper 5 bits, green in the
336354
middle 6 bits, and blue in the lower 5 bits, in native platform endianness.
337355
Some hardware devices and frame buffers use this format. Note that PNG does
@@ -363,7 +381,7 @@ Notes and caveats:
363381

364382
* `A1` and `RGB30` do not yet support `getImageData` or `putImageData`. Have a
365383
use case and/or opinion on working with these formats? Open an issue and let
366-
us know!
384+
us know! (See #935.)
367385

368386
* `A1`, `A8`, `RGB30` and `RGB16_565` with shadow blurs may crash or not render
369387
properly.

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: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,25 +130,35 @@ Canvas.prototype.getContext = function (contextType, contextAttributes) {
130130
/**
131131
* Create a `PNGStream` for `this` canvas.
132132
*
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.
133138
* @return {PNGStream}
134139
* @api public
135140
*/
136141

137142
Canvas.prototype.pngStream =
138-
Canvas.prototype.createPNGStream = function(){
139-
return new PNGStream(this);
143+
Canvas.prototype.createPNGStream = function(options){
144+
return new PNGStream(this, false, options);
140145
};
141146

142147
/**
143148
* Create a synchronous `PNGStream` for `this` canvas.
144149
*
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.
145155
* @return {PNGStream}
146156
* @api public
147157
*/
148158

149159
Canvas.prototype.syncPNGStream =
150-
Canvas.prototype.createSyncPNGStream = function(){
151-
return new PNGStream(this, true);
160+
Canvas.prototype.createSyncPNGStream = function(options){
161+
return new PNGStream(this, true, options);
152162
};
153163

154164
/**

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: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ streamPNG(void *c, const uint8_t *data, unsigned len) {
359359

360360
/*
361361
* 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)
362366
*/
363367

364368
NAN_METHOD(Canvas::StreamPNGSync) {
@@ -368,6 +372,11 @@ NAN_METHOD(Canvas::StreamPNGSync) {
368372
if (!info[0]->IsFunction())
369373
return Nan::ThrowTypeError("callback function required");
370374

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+
371380
if (info.Length() > 1 && !(info[1]->IsUndefined() && info[2]->IsUndefined())) {
372381
if (!info[1]->IsUndefined()) {
373382
bool good = true;
@@ -384,9 +393,32 @@ NAN_METHOD(Canvas::StreamPNGSync) {
384393
compression_level = tmp;
385394
}
386395
}
387-
} else {
388-
good = false;
389-
}
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+
}
390422

391423
if (good) {
392424
if (compression_level > 9) {
@@ -407,11 +439,13 @@ NAN_METHOD(Canvas::StreamPNGSync) {
407439
}
408440

409441

410-
Canvas *canvas = Nan::ObjectWrap::Unwrap<Canvas>(info.This());
411442
closure_t closure;
412443
closure.fn = Local<Function>::Cast(info[0]);
413444
closure.compression_level = compression_level;
414445
closure.filter = filter;
446+
closure.palette = paletteColors;
447+
closure.nPaletteColors = nPaletteColors;
448+
closure.backgroundIndex = backgroundIndex;
415449

416450
Nan::TryCatch try_catch;
417451

src/PNG.h

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,13 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ
156156
#endif
157157

158158
png_set_write_fn(png, closure, write_func, canvas_png_flush);
159+
// FIXME why is this not typed properly?
159160
png_set_compression_level(png, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->compression_level);
160161
png_set_filter(png, 0, ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->filter);
161162

162-
switch (cairo_image_surface_get_format(surface)) {
163+
cairo_format_t format = cairo_image_surface_get_format(surface);
164+
165+
switch (format) {
163166
case CAIRO_FORMAT_ARGB32:
164167
bpc = 8;
165168
png_color_type = PNG_COLOR_TYPE_RGB_ALPHA;
@@ -197,11 +200,40 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ
197200
return status;
198201
}
199202

203+
if ((format == CAIRO_FORMAT_A8 || format == CAIRO_FORMAT_A1) &&
204+
((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->palette != NULL) {
205+
png_color_type = PNG_COLOR_TYPE_PALETTE;
206+
}
207+
200208
png_set_IHDR(png, info, width, height, bpc, png_color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
201209

202-
white.gray = (1 << bpc) - 1;
203-
white.red = white.blue = white.green = white.gray;
204-
png_set_bKGD(png, info, &white);
210+
if (png_color_type == PNG_COLOR_TYPE_PALETTE) {
211+
size_t nColors = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->nPaletteColors;
212+
uint8_t* colors = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->palette;
213+
uint8_t backgroundIndex = ((closure_t *) ((canvas_png_write_closure_t *) closure)->closure)->backgroundIndex;
214+
png_colorp pngPalette = (png_colorp)png_malloc(png, nColors * sizeof(png_colorp));
215+
png_bytep transparency = (png_bytep)png_malloc(png, nColors * sizeof(png_bytep));
216+
for (i = 0; i < nColors; i++) {
217+
pngPalette[i].red = colors[4 * i];
218+
pngPalette[i].green = colors[4 * i + 1];
219+
pngPalette[i].blue = colors[4 * i + 2];
220+
transparency[i] = colors[4 * i + 3];
221+
}
222+
png_set_PLTE(png, info, pngPalette, nColors);
223+
png_set_tRNS(png, info, transparency, nColors, NULL);
224+
png_set_packing(png); // pack pixels
225+
// have libpng free palette and trans:
226+
png_data_freer(png, info, PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_PLTE | PNG_FREE_TRNS);
227+
png_color_16 bkg;
228+
bkg.index = backgroundIndex;
229+
png_set_bKGD(png, info, &bkg);
230+
}
231+
232+
if (png_color_type != PNG_COLOR_TYPE_PALETTE) {
233+
white.gray = (1 << bpc) - 1;
234+
white.red = white.blue = white.green = white.gray;
235+
png_set_bKGD(png, info, &white);
236+
}
205237

206238
/* We have to call png_write_info() before setting up the write
207239
* transformation, since it stores data internally in 'png'
@@ -210,7 +242,7 @@ static cairo_status_t canvas_write_png(cairo_surface_t *surface, png_rw_ptr writ
210242
png_write_info(png, info);
211243
if (png_color_type == PNG_COLOR_TYPE_RGB_ALPHA) {
212244
png_set_write_user_transform_fn(png, canvas_unpremultiply_data);
213-
} else if (cairo_image_surface_get_format(surface) == CAIRO_FORMAT_RGB16_565) {
245+
} else if (format == CAIRO_FORMAT_RGB16_565) {
214246
png_set_write_user_transform_fn(png, canvas_convert_565_to_888);
215247
} else if (png_color_type == PNG_COLOR_TYPE_RGB) {
216248
png_set_write_user_transform_fn(png, canvas_convert_data_to_bytes);

src/closure.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ typedef struct {
3434
cairo_status_t status;
3535
uint32_t compression_level;
3636
uint32_t filter;
37+
uint8_t *palette;
38+
size_t nPaletteColors;
39+
uint8_t backgroundIndex;
3740
} closure_t;
3841

3942
/*

0 commit comments

Comments
 (0)