From 101083e60cc33e16fb44d785137fff10564453b1 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Thu, 19 Apr 2018 22:15:24 -0700 Subject: [PATCH] Make toBuffer more versatile Now works for image/jpeg. PNG filter and ZLIB compression options are now named instead of positional. --- Readme.md | 115 +++++++++++++++++-------- src/Canvas.cc | 201 ++++++++++++++++++++++++++++++-------------- src/JPEGStream.h | 39 ++++++--- test/canvas.test.js | 201 ++++++++++++++++++++++++++------------------ 4 files changed, 363 insertions(+), 193 deletions(-) diff --git a/Readme.md b/Readme.md index 8a9d87e59..f5510131c 100644 --- a/Readme.md +++ b/Readme.md @@ -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 @@ -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({ @@ -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: diff --git a/src/Canvas.cc b/src/Canvas.cc index 5a64870fd..69bdd3438 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -224,18 +224,90 @@ Canvas::ToBufferAsyncAfter(uv_work_t *req) { free(closure); } +inline static uint32_t parsePngCompressionLevel(Local arg) { + // See quote below from spec section 4.12.5.5. + if (arg->IsObject()) { + Local obj = arg->ToObject(); + Local 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 arg) { + if (arg->IsObject()) { + Local obj = arg->ToObject(); + Local cLevelStr = Nan::New("filter").ToLocalChecked(); + if (obj->Has(cLevelStr)) { + return obj->Get(cLevelStr)->Uint32Value(); + } + } + return PNG_ALL_FILTERS; +} + +inline static uint32_t parseJpegQuality(Local 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 obj = arg->ToObject(); + Local 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(100.0 * quality); + } + } + return 75; // spec doesn't say what the default should be +} + +inline static uint32_t parseJpegChromaSampFactor(Local arg) { + if (arg->IsObject()) { + Local obj = arg->ToObject(); + Local chromaStr = Nan::New("chromaSubsampling").ToLocalChecked(); + if (obj->Has(chromaStr)) { + Local 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(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()); @@ -246,8 +318,8 @@ NAN_METHOD(Canvas::ToBuffer) { return; } - if (info.Length() >= 1 && info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { - // Return raw ARGB data -- just a memcpy() + // Raw ARGB data -- just a memcpy() + if (info[0]->StrictEquals(Nan::New("raw").ToLocalChecked())) { cairo_surface_t *surface = canvas->surface(); cairo_surface_flush(surface); const unsigned char *data = cairo_image_surface_get_data(surface); @@ -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("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("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 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("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); @@ -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 jpegStr = Nan::New("image/jpeg").ToLocalChecked(); + if (info[0]->StrictEquals(jpegStr) || + (info[0]->IsFunction() && info[1]->StrictEquals(jpegStr))) { + Local 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 argv[1] = { try_catch.Exception() }; + info[0].As()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 1, argv); + } else { + try_catch.ReThrow(); + } } else { - Local buf = Nan::CopyBuffer((char *)closure.data, closure.len).ToLocalChecked(); - closure_destroy(&closure); - info.GetReturnValue().Set(buf); - return; + char *signedOutBuff = reinterpret_cast(outbuff); + Local buf = Nan::CopyBuffer(signedOutBuff, outsize).ToLocalChecked(); + if (info[0]->IsFunction()) { + Local argv[2] = { Nan::Null(), buf }; + info[0].As()->Call(Isolate::GetCurrent()->GetCurrentContext()->Global(), 2, argv); + } else { + info.GetReturnValue().Set(buf); + } } + + return; } +#endif } /* diff --git a/src/JPEGStream.h b/src/JPEGStream.h index ff6c3f6d0..9e294ec66 100644 --- a/src/JPEGStream.h +++ b/src/JPEGStream.h @@ -100,34 +100,27 @@ jpeg_closure_dest(j_compress_ptr cinfo, closure_t * closure, int bufsize){ cinfo->dest->free_in_buffer = dest->bufsize; } -void -write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor, closure_t *closure){ +void encode_jpeg(jpeg_compress_struct cinfo, cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor) { int w = cairo_image_surface_get_width(surface); int h = cairo_image_surface_get_height(surface); - struct jpeg_compress_struct cinfo; - struct jpeg_error_mgr jerr; - JSAMPROW slr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); cinfo.in_color_space = JCS_RGB; cinfo.input_components = 3; cinfo.image_width = w; cinfo.image_height = h; jpeg_set_defaults(&cinfo); if (progressive) - jpeg_simple_progression(&cinfo); - jpeg_set_quality(&cinfo, quality, (quality<25)?0:1); + jpeg_simple_progression(&cinfo); + jpeg_set_quality(&cinfo, quality, (quality < 25) ? 0 : 1); cinfo.comp_info[0].h_samp_factor = chromaHSampFactor; cinfo.comp_info[0].v_samp_factor = chromaVSampFactor; - jpeg_closure_dest(&cinfo, closure, bufsize); - + JSAMPROW slr; jpeg_start_compress(&cinfo, TRUE); unsigned char *dst; - unsigned int *src = (unsigned int *) cairo_image_surface_get_data(surface); + unsigned int *src = (unsigned int *)cairo_image_surface_get_data(surface); int sl = 0; - dst = (unsigned char *) malloc(w * 3); + dst = (unsigned char *)malloc(w * 3); while (sl < h) { unsigned char *dp = dst; int x = 0; @@ -148,4 +141,24 @@ write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool pr jpeg_destroy_compress(&cinfo); } +void +write_to_jpeg_stream(cairo_surface_t *surface, int bufsize, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor, closure_t *closure){ + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); // TODO don't write to stderr + jpeg_create_compress(&cinfo); + jpeg_closure_dest(&cinfo, closure, bufsize); + encode_jpeg(cinfo, surface, quality, progressive, chromaHSampFactor, chromaVSampFactor); +} + +void +write_to_jpeg_buffer(cairo_surface_t *surface, int quality, bool progressive, int chromaHSampFactor, int chromaVSampFactor, unsigned char** outbuff, unsigned long* outsize) { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); // TODO don't write to stderr + jpeg_create_compress(&cinfo); + jpeg_mem_dest(&cinfo, outbuff, outsize); + encode_jpeg(cinfo, surface, quality, progressive, chromaHSampFactor, chromaVSampFactor); +} + #endif diff --git a/test/canvas.test.js b/test/canvas.test.js index ed4e3694a..566d7ab30 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -451,100 +451,133 @@ describe('Canvas', function () { assert.equal('end', ctx.textAlign); }); - it('Canvas#toBuffer()', function () { - var buf = createCanvas(200,200).toBuffer(); - assert.equal('PNG', buf.slice(1,4).toString()); - }); - - it('Canvas#toBuffer() async', function (done) { - createCanvas(200, 200).toBuffer(function(err, buf){ - assert.ok(!err); + describe('#toBuffer', function () { + it('Canvas#toBuffer()', function () { + var buf = createCanvas(200,200).toBuffer(); assert.equal('PNG', buf.slice(1,4).toString()); - done(); - }); - }); - - describe('#toBuffer("raw")', function() { - var canvas = createCanvas(11, 10) - , ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, 11, 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; - - var endianness = os.endianness(); - - 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('Canvas#toBuffer("image/png")', function () { + var buf = createCanvas(200,200).toBuffer('image/png'); + assert.equal('PNG', buf.slice(1,4).toString()); }); - it('draws green', function() { - assertPixel(0xff00ff00, 0, 5, 'first green pixel'); - assertPixel(0xff00ff00, 4, 9, 'last green pixel'); + it('Canvas#toBuffer("image/jpeg")', function () { + var buf = createCanvas(200,200).toBuffer('image/jpeg'); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); }); - it('draws black', function() { - assertPixel(0xff000000, 5, 5, 'first black pixel'); - assertPixel(0xff000000, 8, 9, 'last black pixel'); + it('Canvas#toBuffer("image/jpeg", 0.95)', function () { + var buf = createCanvas(200,200).toBuffer('image/jpeg', 0.95); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); }); - it('leaves undrawn pixels black, transparent', function() { - assertPixel(0x0, 9, 5, 'first undrawn pixel'); - assertPixel(0x0, 9, 9, 'last undrawn pixel'); + it('Canvas#toBuffer(callback)', function (done) { + createCanvas(200, 200).toBuffer(function(err, buf){ + assert.ok(!err); + assert.equal('PNG', buf.slice(1,4).toString()); + done(); + }); }); - 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'); + it('Canvas#toBuffer(callback, "image/jpeg")', function () { + var buf = createCanvas(200,200).toBuffer(function (err, buff) { + assert.ok(!err); + assert.equal(buf[0], 0xff); + assert.equal(buf[1], 0xd8); + assert.equal(buf[buf.byteLength - 2], 0xff); + assert.equal(buf[buf.byteLength - 1], 0xd9); + }, 'image/jpeg'); + }); + + describe('#toBuffer("raw")', function() { + var canvas = createCanvas(11, 10) + , ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, 11, 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; + + var endianness = os.endianness(); + + 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'); + }); }); });